-
- Qual palavra começa com o mesmo som que {currentWord.word}?
-
+
+
+
Qual palavra começa com o mesmo som?
+
{currentWord.word}
+
-
- {options.map((option) => (
-
- ))}
-
+
+ {options.map((option) => (
+
+ ))}
);
diff --git a/src/components/phonics/exercises/ExerciseFactory.tsx b/src/components/phonics/exercises/ExerciseFactory.tsx
index 420dfaf..2d5fe60 100644
--- a/src/components/phonics/exercises/ExerciseFactory.tsx
+++ b/src/components/phonics/exercises/ExerciseFactory.tsx
@@ -1,20 +1,21 @@
-import type { PhonicsExerciseType, PhonicsWord } from "@/types/phonics";
import { RhymeExercise } from "./RhymeExercise";
import { AlliterationExercise } from "./AlliterationExercise";
import { SyllablesExercise } from "./SyllablesExercise";
-import { SoundMatchExercise } from "./SoundMatchExercise";
+import { InitialSoundExercise } from "./InitialSoundExercise";
+import { FinalSoundExercise } from "./FinalSoundExercise";
+import type { PhonicsWord } from "@/types/phonics";
interface ExerciseFactoryProps {
- type: PhonicsExerciseType;
+ type_id: string;
currentWord: PhonicsWord;
- options: any; // Tipo específico para cada exercício
+ options: PhonicsWord[];
onAnswer: (word: string, isCorrect: boolean) => void;
disabled?: boolean;
}
-export function ExerciseFactory({ type, currentWord, options, onAnswer, disabled }: ExerciseFactoryProps) {
- switch (type) {
- case 'rhyme':
+export function ExerciseFactory({ type_id, currentWord, options, onAnswer, disabled }: ExerciseFactoryProps) {
+ switch (type_id) {
+ case '1': // Rima
return (
);
-
- case 'alliteration':
+ case '2': // Aliteração
return (
);
-
- case 'syllables':
+ case '3': // Sílabas
return (
- );
-
- case 'initial_sound':
- return (
-
);
-
- case 'final_sound':
+ case '4': // Som Inicial
return (
-
+ );
+ case '5': // Som Final
+ return (
+
);
-
default:
- return (
-
- Tipo de exercício não implementado: {type}
-
- );
+ return null;
}
}
\ No newline at end of file
diff --git a/src/components/phonics/exercises/FinalSoundExercise.tsx b/src/components/phonics/exercises/FinalSoundExercise.tsx
new file mode 100644
index 0000000..c3f6eea
--- /dev/null
+++ b/src/components/phonics/exercises/FinalSoundExercise.tsx
@@ -0,0 +1,41 @@
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+import type { PhonicsWord } from "@/types/phonics";
+
+interface FinalSoundExerciseProps {
+ currentWord: PhonicsWord;
+ options: PhonicsWord[];
+ onAnswer: (word: string, isCorrect: boolean) => void;
+ disabled?: boolean;
+}
+
+export function FinalSoundExercise({ currentWord, options, onAnswer, disabled }: FinalSoundExerciseProps) {
+ return (
+
+
+
Qual palavra termina com o mesmo som?
+
{currentWord.word}
+
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/phonics/exercises/InitialSoundExercise.tsx b/src/components/phonics/exercises/InitialSoundExercise.tsx
new file mode 100644
index 0000000..4ee78d2
--- /dev/null
+++ b/src/components/phonics/exercises/InitialSoundExercise.tsx
@@ -0,0 +1,41 @@
+import { Button } from "@/components/ui/button";
+import { Card } from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+import type { PhonicsWord } from "@/types/phonics";
+
+interface InitialSoundExerciseProps {
+ currentWord: PhonicsWord;
+ options: PhonicsWord[];
+ onAnswer: (word: string, isCorrect: boolean) => void;
+ disabled?: boolean;
+}
+
+export function InitialSoundExercise({ currentWord, options, onAnswer, disabled }: InitialSoundExerciseProps) {
+ return (
+
+
+
Qual palavra começa com o mesmo som?
+
{currentWord.word}
+
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/phonics/exercises/RhymeExercise.tsx b/src/components/phonics/exercises/RhymeExercise.tsx
index d0ed69d..3f76f2e 100644
--- a/src/components/phonics/exercises/RhymeExercise.tsx
+++ b/src/components/phonics/exercises/RhymeExercise.tsx
@@ -1,41 +1,40 @@
import { Button } from "@/components/ui/button";
-import { BaseExercise, type BaseExerciseProps } from "./BaseExercise";
+import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
+import type { PhonicsWord } from "@/types/phonics";
-interface RhymeExerciseProps extends BaseExerciseProps {
- options: Array<{
- word: string;
- isRhyme: boolean;
- }>;
+interface RhymeExerciseProps {
+ currentWord: PhonicsWord;
+ options: PhonicsWord[];
+ onAnswer: (word: string, isCorrect: boolean) => void;
+ disabled?: boolean;
}
-export function RhymeExercise({ currentWord, onAnswer, options, disabled }: RhymeExerciseProps) {
+export function RhymeExercise({ currentWord, options, onAnswer, disabled }: RhymeExerciseProps) {
return (
-
-
-
-
-
- Qual palavra rima com {currentWord.word}?
-
+
+
+
Qual palavra rima com:
+
{currentWord.word}
+
-
- {options.map((option) => (
-
- ))}
-
+
+ {options.map((option) => (
+
+ ))}
);
diff --git a/src/components/phonics/exercises/SoundMatchExercise.tsx b/src/components/phonics/exercises/SoundMatchExercise.tsx
index 8e6728b..764a31d 100644
--- a/src/components/phonics/exercises/SoundMatchExercise.tsx
+++ b/src/components/phonics/exercises/SoundMatchExercise.tsx
@@ -42,6 +42,7 @@ export function SoundMatchExercise({
disabled && option.hasMatchingSound && "border-green-500 bg-green-50",
disabled && !option.hasMatchingSound && "border-red-500 bg-red-50"
)}
+ trackingId={`sound-match-option-${option.word}`}
>
{option.word}
diff --git a/src/components/phonics/exercises/SyllablesExercise.tsx b/src/components/phonics/exercises/SyllablesExercise.tsx
index b05d85c..904f216 100644
--- a/src/components/phonics/exercises/SyllablesExercise.tsx
+++ b/src/components/phonics/exercises/SyllablesExercise.tsx
@@ -1,80 +1,40 @@
-import { useState } from "react";
import { Button } from "@/components/ui/button";
-import { BaseExercise, type BaseExerciseProps } from "./BaseExercise";
+import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
+import type { PhonicsWord } from "@/types/phonics";
-interface SyllablesExerciseProps extends BaseExerciseProps {
- syllables: string[];
- correctOrder: number[];
+interface SyllablesExerciseProps {
+ currentWord: PhonicsWord;
+ options: PhonicsWord[];
+ onAnswer: (word: string, isCorrect: boolean) => void;
+ disabled?: boolean;
}
-export function SyllablesExercise({
- currentWord,
- onAnswer,
- syllables,
- correctOrder,
- disabled
-}: SyllablesExerciseProps) {
- const [selectedSyllables, setSelectedSyllables] = useState
([]);
-
- const handleSyllableClick = (index: number) => {
- if (selectedSyllables.includes(index)) {
- setSelectedSyllables(prev => prev.filter(i => i !== index));
- } else {
- setSelectedSyllables(prev => [...prev, index]);
- }
- };
-
- const handleCheck = () => {
- const isCorrect = selectedSyllables.every(
- (syllableIndex, position) => syllableIndex === correctOrder[position]
- );
- onAnswer(currentWord.word, isCorrect);
- };
-
+export function SyllablesExercise({ currentWord, options, onAnswer, disabled }: SyllablesExerciseProps) {
return (
-
-
-
-
-
- Selecione as sílabas na ordem correta
-
+
+
+
Quantas sílabas tem a palavra?
+
{currentWord.word}
+
-
- {syllables.map((syllable, index) => (
-
- ))}
-
-
- {selectedSyllables.length > 0 && !disabled && (
-
-
-
- )}
-
- {selectedSyllables.length > 0 && (
-
- {syllables.filter((_, i) => selectedSyllables.includes(i)).join("-")}
-
- )}
+
+ {options.map((option) => (
+
+ ))}
);
diff --git a/src/hooks/phonics/useExerciseAttempt.ts b/src/hooks/phonics/useExerciseAttempt.ts
index c875f95..e3fdd65 100644
--- a/src/hooks/phonics/useExerciseAttempt.ts
+++ b/src/hooks/phonics/useExerciseAttempt.ts
@@ -1,14 +1,23 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
-import type { PhonicsAttempt } from '@/types/phonics';
+import type { StudentPhonicsAttempt, PhonicsWord } from '@/types/phonics';
+
+interface ExerciseWordResponse {
+ word: PhonicsWord;
+}
+
+interface ExerciseWordWithOptions {
+ word: PhonicsWord;
+ options: PhonicsWord[];
+}
export function useExerciseAttempt() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: async (attempt: Omit
) => {
+ mutationFn: async (attempt: Omit) => {
const { data, error } = await supabase
- .from('phonics_student_attempts')
+ .from('student_phonics_attempts')
.insert(attempt)
.select()
.single();
@@ -18,7 +27,7 @@ export function useExerciseAttempt() {
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
- queryKey: ['phonics-progress', variables.studentId]
+ queryKey: ['phonics-progress', variables.student_id]
});
}
});
@@ -28,17 +37,55 @@ export function useExerciseWords(exerciseId: string) {
return useQuery({
queryKey: ['exercise-words', exerciseId],
queryFn: async () => {
- const { data, error } = await supabase
+ // Primeiro, busca a palavra correta e suas opções
+ const { data: exerciseWords, error: exerciseWordsError } = await supabase
.from('phonics_exercise_words')
- .select(`
- *,
- word:phonics_words(*)
- `)
+ .select('word:phonics_words!inner(*)')
.eq('exercise_id', exerciseId)
+ .eq('is_correct_answer', true)
.order('order_index', { ascending: true });
+ if (exerciseWordsError) throw exerciseWordsError;
+
+ // Para cada palavra correta, busca suas opções
+ const wordsWithOptions = await Promise.all(
+ ((exerciseWords as unknown) as ExerciseWordResponse[]).map(async (exerciseWord) => {
+ const { data: options, error: optionsError } = await supabase
+ .from('phonics_exercise_words')
+ .select('word:phonics_words!inner(*)')
+ .eq('exercise_id', exerciseId)
+ .neq('word.id', exerciseWord.word.id)
+ .limit(3);
+
+ if (optionsError) throw optionsError;
+
+ const optionWords = ((options as unknown) as ExerciseWordResponse[]).map(o => o.word);
+
+ return {
+ word: exerciseWord.word,
+ options: [exerciseWord.word, ...optionWords].sort(() => Math.random() - 0.5)
+ } satisfies ExerciseWordWithOptions;
+ })
+ );
+
+ return wordsWithOptions;
+ },
+ enabled: !!exerciseId
+ });
+}
+
+export function useExerciseAttempts(exerciseId: string) {
+ return useQuery({
+ queryKey: ['exercise-attempts', exerciseId],
+ queryFn: async () => {
+ const { data, error } = await supabase
+ .from('student_phonics_attempts')
+ .select('*')
+ .eq('exercise_id', exerciseId);
+
if (error) throw error;
- return data;
- }
+ return data as StudentPhonicsAttempt[];
+ },
+ enabled: !!exerciseId
});
}
\ No newline at end of file
diff --git a/src/hooks/phonics/usePhonicsExercises.ts b/src/hooks/phonics/usePhonicsExercises.ts
index f03c72d..35a4487 100644
--- a/src/hooks/phonics/usePhonicsExercises.ts
+++ b/src/hooks/phonics/usePhonicsExercises.ts
@@ -10,9 +10,16 @@ export function usePhonicsExercises(categoryId?: string) {
.from('phonics_exercises')
.select(`
*,
- category:phonics_exercise_categories(name),
- type:phonics_exercise_types(name),
- words:phonics_exercise_words(
+ category:phonics_categories(
+ id,
+ name,
+ description,
+ level
+ ),
+ words:phonics_exercise_words!inner(
+ id,
+ is_correct_answer,
+ order_index,
word:phonics_words(*)
)
`)
@@ -36,7 +43,7 @@ export function usePhonicsCategories() {
queryKey: ['phonics-categories'],
queryFn: async () => {
const { data, error } = await supabase
- .from('phonics_exercise_categories')
+ .from('phonics_categories')
.select('*')
.order('order_index', { ascending: true });
diff --git a/src/hooks/phonics/usePhonicsProgress.ts b/src/hooks/phonics/usePhonicsProgress.ts
index 2101dc7..1186eb4 100644
--- a/src/hooks/phonics/usePhonicsProgress.ts
+++ b/src/hooks/phonics/usePhonicsProgress.ts
@@ -1,6 +1,6 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
-import type { StudentPhonicsProgress } from '@/types/phonics';
+import type { StudentPhonicsProgress, UpdateProgressParams } from '@/types/phonics';
export function usePhonicsProgress(studentId: string) {
return useQuery({
@@ -22,57 +22,49 @@ export function useUpdatePhonicsProgress() {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: async ({
- studentId,
- exerciseId,
- score,
- timeSpent
- }: {
- studentId: string;
- exerciseId: string;
- score: number;
- timeSpent: number;
- }) => {
- // Primeiro, registra a tentativa
- const { data: attemptData, error: attemptError } = await supabase
- .from('student_phonics_attempts')
- .insert({
- student_id: studentId,
- exercise_id: exerciseId,
- score,
- time_spent_seconds: timeSpent
- })
- .select()
+ mutationFn: async (params: UpdateProgressParams) => {
+ // Primeiro, busca o progresso atual para calcular os valores acumulados
+ const { data: currentProgress } = await supabase
+ .from('student_phonics_progress')
+ .select('total_time_spent_seconds, correct_answers_count, total_answers_count')
+ .eq('student_id', params.student_id)
+ .eq('exercise_id', params.exercise_id)
.single();
- if (attemptError) throw attemptError;
+ // Calcula os novos valores acumulados
+ const total_time_spent_seconds = (currentProgress?.total_time_spent_seconds || 0) + params.time_spent_seconds;
+ const correct_answers_count = (currentProgress?.correct_answers_count || 0) + params.correct_answers_count;
+ const total_answers_count = (currentProgress?.total_answers_count || 0) + params.total_answers_count;
- // Depois, atualiza ou cria o progresso
- const { data: progressData, error: progressError } = await supabase
+ // Atualiza o progresso
+ const { data, error } = await supabase
.from('student_phonics_progress')
.upsert({
- student_id: studentId,
- exercise_id: exerciseId,
- attempts: 1,
- best_score: score,
- last_score: score,
- completed: score >= 0.7,
- completed_at: score >= 0.7 ? new Date().toISOString() : null,
- stars: Math.ceil(score * 3),
- xp_earned: Math.ceil(score * 100)
+ student_id: params.student_id,
+ exercise_id: params.exercise_id,
+ best_score: params.best_score,
+ last_score: params.last_score,
+ completed: params.completed,
+ completed_at: params.completed ? new Date().toISOString() : null,
+ stars: params.stars,
+ xp_earned: params.xp_earned,
+ attempts: 'COALESCE(attempts, 0) + 1',
+ total_time_spent_seconds,
+ correct_answers_count,
+ total_answers_count,
+ last_attempt_at: new Date().toISOString(),
+ updated_at: new Date().toISOString()
}, {
- onConflict: 'student_id,exercise_id',
- ignoreDuplicates: false
+ onConflict: 'student_id,exercise_id'
})
.select()
.single();
- if (progressError) throw progressError;
-
- return progressData;
+ if (error) throw error;
+ return data as StudentPhonicsProgress;
},
- onSuccess: (_, { studentId }) => {
- queryClient.invalidateQueries({ queryKey: ['phonics-progress', studentId] });
+ onSuccess: (_, { student_id }) => {
+ queryClient.invalidateQueries({ queryKey: ['phonics-progress', student_id] });
}
});
}
\ No newline at end of file
diff --git a/src/pages/student-dashboard/PhonicsPage.tsx b/src/pages/student-dashboard/PhonicsPage.tsx
index a00f205..c5521bb 100644
--- a/src/pages/student-dashboard/PhonicsPage.tsx
+++ b/src/pages/student-dashboard/PhonicsPage.tsx
@@ -24,7 +24,7 @@ export function PhonicsPage() {
return (
setSelectedExercise(undefined)}
/>
diff --git a/src/pages/student-dashboard/PhonicsProgressPage.tsx b/src/pages/student-dashboard/PhonicsProgressPage.tsx
index 8df42a6..7a7c7db 100644
--- a/src/pages/student-dashboard/PhonicsProgressPage.tsx
+++ b/src/pages/student-dashboard/PhonicsProgressPage.tsx
@@ -15,7 +15,7 @@ export function PhonicsProgressPage() {
const totalExercises = exercises.length;
const completedExercises = progress.filter(p => p.completed).length;
const totalStars = progress.reduce((acc, p) => acc + p.stars, 0);
- const totalXP = progress.reduce((acc, p) => acc + p.xpEarned, 0);
+ const totalXP = progress.reduce((acc, p) => acc + p.xp_earned, 0);
const completionRate = (completedExercises / totalExercises) * 100;
return (
@@ -74,8 +74,8 @@ export function PhonicsProgressPage() {
{exercises.map((exercise) => {
- const exerciseProgress = progress.find(p => p.exerciseId === exercise.id);
- const progressValue = exerciseProgress ? exerciseProgress.bestScore * 100 : 0;
+ const exerciseProgress = progress.find(p => p.exercise_id === exercise.id);
+ const progressValue = exerciseProgress ? exerciseProgress.best_score * 100 : 0;
return (
diff --git a/src/types/phonics.ts b/src/types/phonics.ts
index b8001b1..546bceb 100644
--- a/src/types/phonics.ts
+++ b/src/types/phonics.ts
@@ -1,9 +1,5 @@
-export interface PhonicsExerciseType {
- id: string;
- name: string;
- description: string | null;
- created_at: string;
-}
+// Tipos de Exercícios
+export type PhonicsExerciseType = 'rhyme' | 'alliteration' | 'syllables' | 'initial_sound' | 'final_sound';
export interface PhonicsExerciseCategory {
id: string;
@@ -28,6 +24,18 @@ export interface PhonicsExercise {
required_score: number;
created_at: string;
updated_at: string;
+ category?: {
+ id: string;
+ name: string;
+ description: string | null;
+ level: number;
+ };
+ words?: Array<{
+ id: string;
+ is_correct_answer: boolean;
+ order_index: number | null;
+ word: PhonicsWord;
+ }>;
}
export interface PhonicsWord {
@@ -47,6 +55,58 @@ export interface PhonicsExerciseWord {
created_at: string;
}
+export interface StudentPhonicsAttemptAnswer {
+ id: string;
+ attempt_id: string;
+ word_id: string;
+ is_correct: boolean;
+ answer_text: string | null;
+ time_taken_seconds?: number;
+ created_at: string;
+}
+
+export interface StudentPhonicsAttempt {
+ id: string;
+ student_id: string;
+ exercise_id: string;
+ score: number;
+ time_spent_seconds: number | null;
+ answers: StudentPhonicsAttemptAnswer[];
+ created_at: string;
+}
+
+export interface StudentPhonicsProgress {
+ id: string;
+ student_id: string;
+ exercise_id: string;
+ best_score: number;
+ last_score: number;
+ completed: boolean;
+ completed_at: string | null;
+ stars: number;
+ xp_earned: number;
+ attempts: number;
+ total_time_spent_seconds: number;
+ correct_answers_count: number;
+ total_answers_count: number;
+ last_attempt_at: string | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface UpdateProgressParams {
+ student_id: string;
+ exercise_id: string;
+ best_score: number;
+ last_score: number;
+ completed: boolean;
+ stars: number;
+ xp_earned: number;
+ time_spent_seconds: number;
+ correct_answers_count: number;
+ total_answers_count: number;
+}
+
export interface MediaType {
id: string;
name: string;
@@ -64,39 +124,6 @@ export interface PhonicsExerciseMedia {
created_at: string;
}
-export interface StudentPhonicsProgress {
- id: string;
- student_id: string;
- exercise_id: string;
- attempts: number;
- best_score: number;
- last_score: number;
- completed: boolean;
- completed_at: string | null;
- stars: number;
- xp_earned: number;
- created_at: string;
- updated_at: string;
-}
-
-export interface StudentPhonicsAttempt {
- id: string;
- student_id: string;
- exercise_id: string;
- score: number;
- time_spent_seconds: number | null;
- created_at: string;
-}
-
-export interface StudentPhonicsAttemptAnswer {
- id: string;
- attempt_id: string;
- word_id: string;
- is_correct: boolean;
- answer_text: string | null;
- created_at: string;
-}
-
export interface AchievementType {
id: string;
name: string;