1import { useState, useEffect } from 'react';
2import { useApp } from '../../contexts/AppContext';
3import { ClassroomGroup } from '../../types';
4import { groupService } from '../../services/firestoreService';
5import { classroomIntegrationService } from '../../services/classroomIntegrationService';
6import { getStorage, ref, getDownloadURL } from 'firebase/storage';
7
8interface StaffIdentifyExerciseProps {
9 type: 'identify';
10 clef: 'treble' | 'bass' | 'alto';
11 onAnswer: (correct: boolean) => void;
12}
13
14export default function StaffIdentifyExercise({ clef }: StaffIdentifyExerciseProps) {
15 const { state, dispatch } = useApp();
16
17 // États pour l'exercice étudiant
18
19 // États pour le créateur d'exercices (mode enseignant)
20 const [showExample, setShowExample] = useState(false);
21 const [exampleQuestion, setExampleQuestion] = useState<{ correctAnswer: string } | null>(null);
22 const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
23 const [isGenerating, setIsGenerating] = useState(false);
24 const [minNoteImageUrl, setMinNoteImageUrl] = useState<string>('');
25 const [maxNoteImageUrl, setMaxNoteImageUrl] = useState<string>('');
26
27
28 // Charger les groupes au montage du composant
29 useEffect(() => {
30 const loadGroups = async () => {
31 try {
32 // Récupérer l'ID de l'enseignant connecté
33 const teacherId = state.user?.uid || 'default-teacher';
34
35 // Charger les groupes de l'enseignant
36 const groups = await groupService.getGroupsByTeacher(teacherId);
37
38 // Trier les groupes par ordre croissant selon leur nom
39 const sortedGroups = groups.sort((a, b) => {
40 // Extraire les numéros des noms de groupes (ex: "Mus212" -> 212)
41 const getGroupNumber = (name: string) => {
42 const match = name.match(/\d+/);
43 return match ? parseInt(match[0]) : 0;
44 };
45
46 const numA = getGroupNumber(a.name);
47 const numB = getGroupNumber(b.name);
48
49 if (numA !== numB) {
50 return numA - numB; // Tri numérique
51 }
52
53 return a.name.localeCompare(b.name); // Tri alphabétique si même numéro
54 });
55
56 // Remplacer complètement la liste des groupes
57 dispatch({ type: 'LOAD_GROUPS', payload: sortedGroups });
58
59 } catch {
60 // Erreur silencieuse
61 }
62 };
63
64 loadGroups();
65 }, [dispatch, state.user?.uid]);
66
67
68 // Configuration de l'exercice
69 const [exerciseConfig, setExerciseConfig] = useState({
70 title: '',
71 numberOfQuestions: 10,
72 timeLimit: 0, // 0 = pas de limite de temps
73 timeMode: 'minuteur' as 'minuteur' | 'countdown', // minuteur = temps total, countdown = temps par question
74 includeSharps: true,
75 includeFlats: true,
76 includeNaturals: true,
77 clef: clef,
78 notation: 'français' as 'français' | 'anglais',
79 startDate: '', // Date d'apparition
80 endDate: '', // Date de fin
81 noteRange: {
82 min: clef === 'treble' ? 'fa' : 'do', // Fa3 pour clé de sol, Do2 pour clé de fa
83 max: clef === 'treble' ? 'do' : 'si' // Do6 pour clé de sol, Si4 pour clé de fa
84 },
85 octaveRange: {
86 min: clef === 'treble' ? 3 : 2, // 3 pour clé de sol, 2 pour clé de fa
87 max: clef === 'treble' ? 6 : 4 // 6 pour clé de sol, 4 pour clé de fa
88 }
89 });
90
91 // Fonction pour convertir la notation française vers allemande
92
93 // Fonction pour générer le nom de fichier d'image
94
95 // Mettre à jour les octaves quand la clé change
96 useEffect(() => {
97 setExerciseConfig(prev => ({
98 ...prev,
99 noteRange: {
100 min: prev.clef === 'treble' ? 'fa' : 'do',
101 max: prev.clef === 'treble' ? 'do' : 'si'
102 },
103 octaveRange: {
104 min: prev.clef === 'treble' ? 3 : 2,
105 max: prev.clef === 'treble' ? 6 : 4
106 }
107 }));
108 }, [exerciseConfig.clef]);
109
110 // Charger les images des notes min/max depuis Firebase Storage
111 useEffect(() => {
112 const loadNoteImages = async () => {
113 try {
114 const storage = getStorage();
115
116 // Charger l'image de la note minimale
117 const minNoteName = exerciseConfig.noteRange.min === 'do' ? 'c' :
118 exerciseConfig.noteRange.min === 're' ? 'd' :
119 exerciseConfig.noteRange.min === 'mi' ? 'e' :
120 exerciseConfig.noteRange.min === 'fa' ? 'f' :
121 exerciseConfig.noteRange.min === 'sol' ? 'g' :
122 exerciseConfig.noteRange.min === 'la' ? 'a' : 'b';
123
124 const minNoteRef = ref(storage, `music-theory-app/notes/${exerciseConfig.clef}/${minNoteName}${exerciseConfig.octaveRange.min}-${exerciseConfig.clef}-finalbar-small.png`);
125 const minNoteUrl = await getDownloadURL(minNoteRef);
126 setMinNoteImageUrl(minNoteUrl);
127
128 // Charger l'image de la note maximale
129 const maxNoteName = exerciseConfig.noteRange.max === 'do' ? 'c' :
130 exerciseConfig.noteRange.max === 're' ? 'd' :
131 exerciseConfig.noteRange.max === 'mi' ? 'e' :
132 exerciseConfig.noteRange.max === 'fa' ? 'f' :
133 exerciseConfig.noteRange.max === 'sol' ? 'g' :
134 exerciseConfig.noteRange.max === 'la' ? 'a' : 'b';
135
136 const maxNoteRef = ref(storage, `music-theory-app/notes/${exerciseConfig.clef}/${maxNoteName}${exerciseConfig.octaveRange.max}-${exerciseConfig.clef}-finalbar-small.png`);
137 const maxNoteUrl = await getDownloadURL(maxNoteRef);
138 setMaxNoteImageUrl(maxNoteUrl);
139
140 } catch (error) {
141 console.error('Erreur lors du chargement des images:', error);
142 }
143 };
144
145 loadNoteImages();
146 }, [exerciseConfig.clef, exerciseConfig.noteRange, exerciseConfig.octaveRange]);
147
148 const generateExampleQuestion = () => {
149 // Générer une question d'exemple avec clavier et choix de réponse
150 const randomNote = exerciseConfig.includeNaturals ? 'do' :
151 exerciseConfig.includeSharps ? 'do#' : 'dob';
152
153 setExampleQuestion({
154 correctAnswer: randomNote
155 });
156
157 setShowExample(true);
158 };
159
160 // Fonction pour générer et envoyer l'exercice
161 const handleGenerateExercise = async () => {
162 if (selectedGroups.length === 0) {
163 alert('Veuillez sélectionner au moins un groupe.');
164 return;
165 }
166
167 setIsGenerating(true);
168
169 try {
170 // Créer l'exercice avec la configuration
171 const exercise = {
172 id: `exercise_${Date.now()}`,
173 type: 'identify-staff-notes',
174 config: {
175 ...exerciseConfig,
176 title: exerciseConfig.title || 'Identification de notes sur la portée'
177 },
178 teacherId: state.user?.uid,
179 groups: selectedGroups,
180 createdAt: new Date().toISOString(),
181 status: 'active'
182 };
183
184 // Créer le devoir dans Google Classroom pour chaque groupe
185 const classroomResults = [];
186 for (const groupId of selectedGroups) {
187 const group = state.groups.find(g => g.id === groupId);
188 if (group && group.courseId) {
189 try {
190 // Créer le devoir dans Google Classroom
191 const assignmentId = await classroomIntegrationService.createAssignment(group, exercise);
192
193 classroomResults.push({
194 groupName: group.name,
195 assignmentId: assignmentId,
196 success: true
197 });
198 } catch (error) {
199 classroomResults.push({
200 groupName: group.name,
201 error: error instanceof Error ? error.message : 'Erreur inconnue',
202 success: false
203 });
204 }
205 } else {
206 classroomResults.push({
207 groupName: groupId,
208 error: 'Groupe non trouvé ou sans ID Classroom',
209 success: false
210 });
211 }
212 }
213
214 // Sauvegarder l'exercice dans Firebase
215 // TODO: Implémenter la sauvegarde Firebase
216
217 // Afficher les résultats
218 const successCount = classroomResults.filter(r => r.success).length;
219 const totalCount = classroomResults.length;
220
221 if (successCount > 0) {
222 alert(`✅ Exercice "${exerciseConfig.title}" créé avec succès !
223
224📊 Résultats:
225- ${successCount}/${totalCount} groupe(s) traité(s) avec succès
226- ${totalCount - successCount} erreur(s)
227
228🎯 Les élèves peuvent maintenant voir l'exercice dans Google Classroom.`);
229 } else {
230 alert(`❌ Erreur lors de la création des devoirs Classroom.
231
232Vérifiez que:
233- Les groupes sont bien connectés à Google Classroom
234- Vous avez les permissions nécessaires
235- L'API Classroom est activée`);
236 }
237
238 } catch (error) {
239 alert('Erreur lors de la génération de l\'exercice: ' + (error instanceof Error ? error.message : 'Erreur inconnue'));
240 } finally {
241 setIsGenerating(false);
242 }
243 };
244
245 return (
246 <div className="max-w-4xl mx-auto px-4 py-8">
247 <div className="bg-white rounded-xl shadow-sm p-8">
248 <h2 className="text-2xl font-bold text-center mb-8">
249 Créateur d'exercices - Identifier les Notes
250 </h2>
251
252 <div className="bg-gray-50 rounded-lg p-6 mb-6">
253 <h3 className="text-lg font-semibold text-center mb-4">Configuration de l'exercice</h3>
254
255 <div className="space-y-6">
256 {/* Titre de l'exercice */}
257 <div>
258 <label className="block text-sm font-medium text-gray-700 mb-2">
259 Titre de l'exercice
260 </label>
261 <input
262 type="text"
263 placeholder="Ex: Reconnaissance de notes - Clé de Sol"
264 value={exerciseConfig.title}
265 onChange={(e) => setExerciseConfig(prev => ({ ...prev, title: e.target.value }))}
266 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
267 />
268 </div>
269
270 {/* Ligne 1: Nombre de questions + Limite de temps */}
271 <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
272 <div>
273 <label className="block text-sm font-medium text-gray-700 mb-2">
274 Nombre de questions
275 </label>
276 <input
277 type="number"
278 min="1"
279 max="100"
280 value={exerciseConfig.numberOfQuestions}
281 onChange={(e) => setExerciseConfig(prev => ({ ...prev, numberOfQuestions: parseInt(e.target.value) }))}
282 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
283 />
284 </div>
285
286 <div>
287 <label className="block text-sm font-medium text-gray-700 mb-2">
288 Mode de temps
289 </label>
290 <select
291 value={exerciseConfig.timeMode}
292 onChange={(e) => setExerciseConfig(prev => ({ ...prev, timeMode: e.target.value as 'minuteur' | 'countdown' }))}
293 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
294 >
295 <option value="minuteur">Minuteur</option>
296 <option value="countdown">Compte à rebours</option>
297 </select>
298 </div>
299 </div>
300
301 {/* Ligne 2: Limite de temps (si countdown) + Clé */}
302 <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
303 {exerciseConfig.timeMode === 'countdown' && (
304 <div>
305 <label className="block text-sm font-medium text-gray-700 mb-2">
306 Limite de temps (minutes)
307 </label>
308 <input
309 type="number"
310 min="1"
311 max="10"
312 value={exerciseConfig.timeLimit}
313 onChange={(e) => setExerciseConfig(prev => ({ ...prev, timeLimit: parseInt(e.target.value) }))}
314 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
315 placeholder="Ex: 5"
316 />
317 </div>
318 )}
319
320 <div>
321 <label className="block text-sm font-medium text-gray-700 mb-2">
322 Clé
323 </label>
324 <select
325 value={exerciseConfig.clef}
326 onChange={(e) => {
327 const newClef = e.target.value as 'treble' | 'bass';
328 setExerciseConfig(prev => ({
329 ...prev,
330 clef: newClef
331 }));
332 }}
333 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
334 >
335 <option value="treble">Clé de Sol (octaves 3-6)</option>
336 <option value="bass">Clé de Fa (octaves 2-4)</option>
337 </select>
338 </div>
339 </div>
340
341 {/* Ligne 3: Notation + Types de notes */}
342 <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
343 <div>
344 <label className="block text-sm font-medium text-gray-700 mb-2">
345 Notation
346 </label>
347 <select
348 value={exerciseConfig.notation}
349 onChange={(e) => {
350 const notation = e.target.value as 'français' | 'anglais';
351 setExerciseConfig(prev => ({ ...prev, notation }));
352 }}
353 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
354 >
355 <option value="français">Français (do, ré, mi)</option>
356 <option value="anglais">Anglais (C, D, E)</option>
357 </select>
358 </div>
359
360 <div>
361 <label className="block text-sm font-medium text-gray-700 mb-2">
362 Types de notes
363 </label>
364 <div className="space-y-3">
365 <label className="flex items-center">
366 <input
367 type="checkbox"
368 checked={exerciseConfig.includeNaturals}
369 onChange={(e) => setExerciseConfig(prev => ({ ...prev, includeNaturals: e.target.checked }))}
370 className="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
371 />
372 <span className="text-sm font-medium text-gray-700">
373 Notes naturelles
374 </span>
375 </label>
376 <label className="flex items-center">
377 <input
378 type="checkbox"
379 checked={exerciseConfig.includeSharps}
380 onChange={(e) => setExerciseConfig(prev => ({ ...prev, includeSharps: e.target.checked }))}
381 className="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
382 />
383 <span className="text-sm font-medium text-gray-700">
384 Notes dièses
385 </span>
386 </label>
387 <label className="flex items-center">
388 <input
389 type="checkbox"
390 checked={exerciseConfig.includeFlats}
391 onChange={(e) => setExerciseConfig(prev => ({ ...prev, includeFlats: e.target.checked }))}
392 className="mr-3 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
393 />
394 <span className="text-sm font-medium text-gray-700">
395 Notes bémols
396 </span>
397 </label>
398 </div>
399 </div>
400 </div>
401 </div>
402 </div>
403
404 {/* Intervalle de notes */}
405 <div className="bg-gray-50 rounded-lg p-6 mb-6">
406 <h3 className="text-lg font-semibold text-center mb-4">Intervalle de notes</h3>
407
408 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
409 {/* Note la plus grave */}
410 <div className="flex flex-col items-center">
411 <label className="block text-sm font-medium text-gray-700 mb-2">Note la plus grave</label>
412 <select
413 value={`${exerciseConfig.noteRange.min} ${exerciseConfig.octaveRange.min}`}
414 onChange={(e) => {
415 const [note, octave] = e.target.value.split(' ');
416 setExerciseConfig(prev => ({
417 ...prev,
418 noteRange: { ...prev.noteRange, min: note },
419 octaveRange: { ...prev.octaveRange, min: parseInt(octave) }
420 }));
421 }}
422 className="w-32 p-2 border border-gray-300 rounded-md mb-4"
423 >
424 {exerciseConfig.clef === 'treble' ? (
425 <>
426 <option value="fa 3">Fa 3</option>
427 <option value="sol 3">Sol 3</option>
428 <option value="la 3">La 3</option>
429 <option value="si 3">Si 3</option>
430 <option value="do 4">Do 4</option>
431 <option value="re 4">Ré 4</option>
432 <option value="mi 4">Mi 4</option>
433 <option value="fa 4">Fa 4</option>
434 <option value="sol 4">Sol 4</option>
435 <option value="la 4">La 4</option>
436 <option value="si 4">Si 4</option>
437 <option value="do 5">Do 5</option>
438 <option value="re 5">Ré 5</option>
439 <option value="mi 5">Mi 5</option>
440 <option value="fa 5">Fa 5</option>
441 <option value="sol 5">Sol 5</option>
442 <option value="la 5">La 5</option>
443 <option value="si 5">Si 5</option>
444 <option value="do 6">Do 6</option>
445 </>
446 ) : (
447 <>
448 <option value="do 2">Do 2</option>
449 <option value="re 2">Ré 2</option>
450 <option value="mi 2">Mi 2</option>
451 <option value="fa 2">Fa 2</option>
452 <option value="sol 2">Sol 2</option>
453 <option value="la 2">La 2</option>
454 <option value="si 2">Si 2</option>
455 <option value="do 3">Do 3</option>
456 <option value="re 3">Ré 3</option>
457 <option value="mi 3">Mi 3</option>
458 <option value="fa 3">Fa 3</option>
459 <option value="sol 3">Sol 3</option>
460 <option value="la 3">La 3</option>
461 <option value="si 3">Si 3</option>
462 <option value="do 4">Do 4</option>
463 <option value="re 4">Ré 4</option>
464 <option value="mi 4">Mi 4</option>
465 <option value="fa 4">Fa 4</option>
466 <option value="sol 4">Sol 4</option>
467 <option value="la 4">La 4</option>
468 <option value="si 4">Si 4</option>
469 </>
470 )}
471 </select>
472 <div className="bg-white p-3 rounded-lg shadow-sm border">
473 {minNoteImageUrl ? (
474 <img
475 src={minNoteImageUrl}
476 alt={`${exerciseConfig.noteRange.min} ${exerciseConfig.octaveRange.min}`}
477 className="w-20 h-20 object-contain"
478 />
479 ) : (
480 <div className="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">
481 Chargement...
482 </div>
483 )}
484 </div>
485 </div>
486
487 {/* Note la plus aiguë */}
488 <div className="flex flex-col items-center">
489 <label className="block text-sm font-medium text-gray-700 mb-2">Note la plus aiguë</label>
490 <select
491 value={`${exerciseConfig.noteRange.max} ${exerciseConfig.octaveRange.max}`}
492 onChange={(e) => {
493 const [note, octave] = e.target.value.split(' ');
494 setExerciseConfig(prev => ({
495 ...prev,
496 noteRange: { ...prev.noteRange, max: note },
497 octaveRange: { ...prev.octaveRange, max: parseInt(octave) }
498 }));
499 }}
500 className="w-32 p-2 border border-gray-300 rounded-md mb-4"
501 >
502 {exerciseConfig.clef === 'treble' ? (
503 <>
504 <option value="fa 3">Fa 3</option>
505 <option value="sol 3">Sol 3</option>
506 <option value="la 3">La 3</option>
507 <option value="si 3">Si 3</option>
508 <option value="do 4">Do 4</option>
509 <option value="re 4">Ré 4</option>
510 <option value="mi 4">Mi 4</option>
511 <option value="fa 4">Fa 4</option>
512 <option value="sol 4">Sol 4</option>
513 <option value="la 4">La 4</option>
514 <option value="si 4">Si 4</option>
515 <option value="do 5">Do 5</option>
516 <option value="re 5">Ré 5</option>
517 <option value="mi 5">Mi 5</option>
518 <option value="fa 5">Fa 5</option>
519 <option value="sol 5">Sol 5</option>
520 <option value="la 5">La 5</option>
521 <option value="si 5">Si 5</option>
522 <option value="do 6">Do 6</option>
523 </>
524 ) : (
525 <>
526 <option value="do 2">Do 2</option>
527 <option value="re 2">Ré 2</option>
528 <option value="mi 2">Mi 2</option>
529 <option value="fa 2">Fa 2</option>
530 <option value="sol 2">Sol 2</option>
531 <option value="la 2">La 2</option>
532 <option value="si 2">Si 2</option>
533 <option value="do 3">Do 3</option>
534 <option value="re 3">Ré 3</option>
535 <option value="mi 3">Mi 3</option>
536 <option value="fa 3">Fa 3</option>
537 <option value="sol 3">Sol 3</option>
538 <option value="la 3">La 3</option>
539 <option value="si 3">Si 3</option>
540 <option value="do 4">Do 4</option>
541 <option value="re 4">Ré 4</option>
542 <option value="mi 4">Mi 4</option>
543 <option value="fa 4">Fa 4</option>
544 <option value="sol 4">Sol 4</option>
545 <option value="la 4">La 4</option>
546 <option value="si 4">Si 4</option>
547 </>
548 )}
549 </select>
550 <div className="bg-white p-3 rounded-lg shadow-sm border">
551 {maxNoteImageUrl ? (
552 <img
553 src={maxNoteImageUrl}
554 alt={`${exerciseConfig.noteRange.max} ${exerciseConfig.octaveRange.max}`}
555 className="w-20 h-20 object-contain"
556 />
557 ) : (
558 <div className="w-20 h-20 bg-gray-200 rounded flex items-center justify-center text-gray-500 text-xs">
559 Chargement...
560 </div>
561 )}
562 </div>
563 </div>
564 </div>
565 </div>
566
567 {/* Sélection des groupes et dates */}
568 <div className="bg-gray-50 rounded-lg p-6 mb-6">
569 <h3 className="text-lg font-semibold text-center mb-4">Sélection des groupes et dates</h3>
570
571 <div className="space-y-6">
572 {/* Sélection des groupes */}
573 <div>
574 <label className="block text-sm font-medium text-gray-700 mb-2">
575 Sélectionner les groupes d'élèves
576 </label>
577
578 <select
579 value={selectedGroups[0] || ''}
580 onChange={(e) => {
581 setSelectedGroups(e.target.value ? [e.target.value] : []);
582 }}
583 className="w-full p-3 border border-gray-300 rounded-md"
584 >
585 <option value="">Sélectionner un groupe</option>
586 {state.groups.length > 0 ? (
587 state.groups.map((group: ClassroomGroup) => (
588 <option key={group.id} value={group.id}>
589 {group.name} ({group.students.length} élèves)
590 </option>
591 ))
592 ) : (
593 <option disabled>Aucun groupe disponible</option>
594 )}
595 </select>
596
597 <p className="text-sm text-gray-600 mt-2">
598 Chaque élève des groupes sélectionnés recevra une copie de cet exercice dans son dashboard.
599 </p>
600 </div>
601
602 {/* Dates de début et fin */}
603 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
604 <div>
605 <label className="block text-sm font-medium text-gray-700 mb-2">
606 Date d'apparition (optionnel)
607 </label>
608 <input
609 type="datetime-local"
610 value={exerciseConfig.startDate}
611 onChange={(e) => setExerciseConfig(prev => ({ ...prev, startDate: e.target.value }))}
612 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
613 />
614 <p className="text-xs text-gray-500 mt-1">
615 L'exercice sera visible à partir de cette date
616 </p>
617 </div>
618
619 <div>
620 <label className="block text-sm font-medium text-gray-700 mb-2">
621 Date de fin (optionnel)
622 </label>
623 <input
624 type="datetime-local"
625 value={exerciseConfig.endDate}
626 onChange={(e) => setExerciseConfig(prev => ({ ...prev, endDate: e.target.value }))}
627 className="w-full p-3 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
628 />
629 <p className="text-xs text-gray-500 mt-1">
630 L'exercice ne sera plus accessible après cette date
631 </p>
632 </div>
633 </div>
634 </div>
635 </div>
636
637 {/* Boutons */}
638 <div className="flex justify-center pt-4 space-x-4">
639 <button
640 onClick={generateExampleQuestion}
641 className="px-6 py-3 rounded-lg bg-blue-500 text-white hover:bg-blue-600 transition-colors font-medium"
642 >
643 Voir un exemple
644 </button>
645
646 <button
647 onClick={handleGenerateExercise}
648 disabled={selectedGroups.length === 0}
649 className={`px-8 py-3 rounded-lg transition-colors font-medium ${
650 selectedGroups.length === 0
651 ? 'bg-gray-400 text-gray-600 cursor-not-allowed'
652 : 'bg-green-500 text-white hover:bg-green-600'
653 }`}
654 >
655 {isGenerating ? 'Génération en cours...' : 'Générer l\'exercice'}
656 </button>
657 </div>
658
659 {/* Affichage de l'exemple */}
660 {showExample && exampleQuestion && (
661 <div className="mt-8">
662 <div className="bg-yellow-50 rounded-lg p-6 border-2 border-yellow-200 relative">
663 {/* X de fermeture en haut à droite */}
664 <button
665 onClick={() => setShowExample(false)}
666 className="absolute top-2 right-2 w-8 h-8 flex items-center justify-center text-gray-500 hover:text-gray-700 hover:bg-gray-200 rounded-full transition-colors"
667 aria-label="Fermer l'exemple"
668 >
669 <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
670 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
671 </svg>
672 </button>
673
674 <h3 className="text-lg font-semibold text-center mb-4">Exemple de question</h3>
675 <p className="text-center text-gray-600 mb-4">
676 Quelle note correspond à cette touche mise en évidence ?
677 </p>
678
679 {/* Clavier de piano */}
680 <div className="flex justify-center mb-6">
681 <div className="relative mx-auto" style={{ width: 'fit-content' }}>
682 {/* Touches blanches */}
683 <div className="flex">
684 {Array.from({ length: 7 }, (_, index) => (
685 <div
686 key={index}
687 className={`w-10 h-32 border-2 border-gray-200 bg-gradient-to-b from-white to-gray-50
688 relative shadow-md transition-all duration-200
689 ${index === 0
690 ? 'bg-gradient-to-b from-yellow-200 to-yellow-300 border-yellow-400 shadow-lg ring-2 ring-yellow-200'
691 : 'hover:from-gray-50 hover:to-gray-100'
692 }`}
693 style={{
694 borderRadius: '0 0 8px 8px'
695 }}
696 >
697 {/* Ombre intérieure pour effet 3D */}
698 <div className={`absolute inset-0 rounded-b-lg ${
699 index === 0
700 ? 'bg-gradient-to-t from-yellow-400/20 to-transparent'
701 : 'bg-gradient-to-t from-gray-200/30 to-transparent'
702 }`}></div>
703 </div>
704 ))}
705 </div>
706
707 {/* Touches noires */}
708 <div className="absolute top-0 flex pointer-events-none">
709 {/* C# - première touche noire */}
710 <div className="w-7 h-20 bg-gradient-to-b from-gray-800 to-gray-900 absolute shadow-lg" style={{ left: '28px', borderRadius: '0 0 6px 6px' }}>
711 <div className="absolute inset-0 rounded-b-lg bg-gradient-to-t from-black/30 to-transparent"></div>
712 </div>
713 {/* D# - deuxième touche noire */}
714 <div className="w-7 h-20 bg-gradient-to-b from-gray-800 to-gray-900 absolute shadow-lg" style={{ left: '68px', borderRadius: '0 0 6px 6px' }}>
715 <div className="absolute inset-0 rounded-b-lg bg-gradient-to-t from-black/30 to-transparent"></div>
716 </div>
717 {/* F# - troisième touche noire */}
718 <div className="w-7 h-20 bg-gradient-to-b from-gray-800 to-gray-900 absolute shadow-lg" style={{ left: '148px', borderRadius: '0 0 6px 6px' }}>
719 <div className="absolute inset-0 rounded-b-lg bg-gradient-to-t from-black/30 to-transparent"></div>
720 </div>
721 {/* G# - quatrième touche noire */}
722 <div className="w-7 h-20 bg-gradient-to-b from-gray-800 to-gray-900 absolute shadow-lg" style={{ left: '188px', borderRadius: '0 0 6px 6px' }}>
723 <div className="absolute inset-0 rounded-b-lg bg-gradient-to-t from-black/30 to-transparent"></div>
724 </div>
725 {/* A# - cinquième touche noire */}
726 <div className="w-7 h-20 bg-gradient-to-b from-gray-800 to-gray-900 absolute shadow-lg" style={{ left: '228px', borderRadius: '0 0 6px 6px' }}>
727 <div className="absolute inset-0 rounded-b-lg bg-gradient-to-t from-black/30 to-transparent"></div>
728 </div>
729 </div>
730 </div>
731 </div>
732
733 {/* Section des choix de réponse */}
734 <div className="bg-gradient-to-br from-blue-50 to-white p-6 rounded-2xl shadow-lg mb-8 border border-blue-100">
735 <h4 className="text-center mb-4 font-semibold text-blue-800 text-lg">Choix de réponse</h4>
736 <p className="text-center mb-4 text-blue-600">L'étudiant devra sélectionner la bonne réponse :</p>
737
738 <div className="space-y-3 max-w-md mx-auto">
739 {/* Ligne 1: Notes dièses */}
740 {exerciseConfig.includeSharps && (
741 <div>
742 <div className="grid grid-cols-7 gap-2">
743 {(() => {
744 const sharpNotes = exerciseConfig.notation === 'français'
745 ? ['do#', 'ré#', 'mi#', 'fa#', 'sol#', 'la#', 'si#']
746 : ['C#', 'D#', 'E#', 'F#', 'G#', 'A#', 'B#'];
747 return sharpNotes.map((note, index) => (
748 <button
749 key={index}
750 className={`p-2 rounded-lg border-2 transition-colors text-sm ${
751 note === exampleQuestion.correctAnswer
752 ? 'bg-green-100 border-green-300 text-green-800'
753 : 'bg-gray-100 border-gray-300 text-gray-800 hover:bg-gray-200'
754 }`}
755 >
756 {note}
757 </button>
758 ));
759 })()}
760 </div>
761 </div>
762 )}
763
764 {/* Ligne 2: Notes naturelles */}
765 {exerciseConfig.includeNaturals && (
766 <div>
767 <div className="grid grid-cols-7 gap-2">
768 {(() => {
769 const naturalNotes = exerciseConfig.notation === 'français'
770 ? ['do', 'ré', 'mi', 'fa', 'sol', 'la', 'si']
771 : ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
772 return naturalNotes.map((note, index) => (
773 <button
774 key={index}
775 className={`p-2 rounded-lg border-2 transition-colors text-sm ${
776 note === exampleQuestion.correctAnswer
777 ? 'bg-green-100 border-green-300 text-green-800'
778 : 'bg-gray-100 border-gray-300 text-gray-800 hover:bg-gray-200'
779 }`}
780 >
781 {note}
782 </button>
783 ));
784 })()}
785 </div>
786 </div>
787 )}
788
789 {/* Ligne 3: Notes bémols */}
790 {exerciseConfig.includeFlats && (
791 <div>
792 <div className="grid grid-cols-7 gap-2">
793 {(() => {
794 const flatNotes = exerciseConfig.notation === 'français'
795 ? ['dob', 'réb', 'mib', 'fab', 'solb', 'lab', 'sib']
796 : ['Cb', 'Db', 'Eb', 'Fb', 'Gb', 'Ab', 'Bb'];
797 return flatNotes.map((note, index) => (
798 <button
799 key={index}
800 className={`p-2 rounded-lg border-2 transition-colors text-sm ${
801 note === exampleQuestion.correctAnswer
802 ? 'bg-green-100 border-green-300 text-green-800'
803 : 'bg-gray-100 border-gray-300 text-gray-800 hover:bg-gray-200'
804 }`}
805 >
806 {note}
807 </button>
808 ));
809 })()}
810 </div>
811 </div>
812 )}
813 </div>
814
815 <div className="mt-4 p-3 bg-green-50 rounded-lg border border-green-200">
816 <p className="text-sm text-green-800 text-center font-medium">
817 <strong>Réponse correcte :</strong> {exampleQuestion.correctAnswer}
818 </p>
819 </div>
820 </div>
821 </div>
822 </div>
823 )}
824 </div>
825 </div>
826 );
827}
828Powered by SnippSync • Always fresh, always in sync
Create snippets that automatically sync with your GitHub repositories. Never worry about outdated documentation again.