SnippSync

Always Fresh Code

StaffIdentifyExercise.tsx

musiquetheorie
Lines 0-827
Auto-synced 71 days ago
This snippet automatically syncs with your GitHub repository
SnippSync
TS
StaffIdentifyExercise.tsx
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,, 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}
828

Powered by SnippSync • Always fresh, always in sync

Create Your Own Auto-Synced Snippets

Keep Your Documentation Always Fresh

Create snippets that automatically sync with your GitHub repositories. Never worry about outdated documentation again.

Auto-sync with GitHub
Beautiful embeds
Share anywhere