fix: corrige tipos e queries dos hooks de exercícios fônicos
Some checks are pending
Docker Build and Push / build (push) Waiting to run

- Corrige tipo de retorno em useExerciseWords
- Ajusta usePhonicsExercises para filtrar por categoria
- Atualiza queries para usar inner join e ordenação
- Adiciona interfaces para melhor tipagem
- Corrige convenção de nomes para snake_case
This commit is contained in:
Lucas Santana 2025-01-18 06:53:24 -03:00
parent 350a66bb9e
commit f37f8f2f6d
15 changed files with 131 additions and 99 deletions

View File

@ -48,6 +48,11 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
- Criação de índices para otimização de consultas
- Implementação de políticas de segurança RLS
- Estrutura de dados normalizada com relacionamentos apropriados
- Corrigido tipo de retorno em `useExerciseWords` para garantir formato correto de palavra e opções
- Ajustado `usePhonicsExercises` para aceitar filtro por categoria
- Atualizada query de palavras do exercício para usar inner join e ordenação
- Adicionadas interfaces `AttemptParams` e `ExerciseWord` para melhor tipagem
- Corrigidos nomes de propriedades para seguir convenção snake_case em todos os hooks
### Modificado
- N/A (primeira versão)

View File

@ -45,6 +45,7 @@ export function AudioPlayer({ word, disabled }: AudioPlayerProps) {
return (
<div>
<Button
trackingId="play-word-audio"
variant="ghost"
size="lg"
className="gap-2"

View File

@ -3,18 +3,18 @@ import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Clock, Star } from "lucide-react";
import type { PhonicsExercise, PhonicsProgress } from "@/types/phonics";
import type { PhonicsExercise, StudentPhonicsProgress } from "@/types/phonics";
interface ExerciseCardProps {
exercise: PhonicsExercise;
progress?: PhonicsProgress;
progress?: StudentPhonicsProgress;
onStart: (exerciseId: string) => void;
}
export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps) {
const isCompleted = progress?.completed;
const stars = progress?.stars || 0;
const progressValue = progress ? (progress.bestScore * 100) : 0;
const progressValue = progress ? (progress.best_score * 100) : 0;
return (
<Card className="w-full hover:shadow-lg transition-shadow">
@ -34,7 +34,7 @@ export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps)
<div className="flex items-center justify-between text-sm text-muted-foreground">
<div className="flex items-center gap-2">
<Clock className="w-4 h-4" />
<span>{Math.ceil(exercise.estimatedTimeSeconds / 60)} min</span>
<span>{Math.ceil((exercise.estimated_time_seconds ?? 0) / 60)} min</span>
</div>
<div className="flex items-center gap-1">
{Array.from({ length: 3 }).map((_, i) => (
@ -57,6 +57,7 @@ export function ExerciseCard({ exercise, progress, onStart }: ExerciseCardProps)
)}
<Button
trackingId="start-exercise"
className="w-full"
onClick={() => onStart(exercise.id)}
variant={isCompleted ? "secondary" : "default"}

View File

@ -39,7 +39,7 @@ export function ExerciseGrid({ categoryId, studentId, onSelectExercise }: Exerci
<ExerciseCard
key={exercise.id}
exercise={exercise}
progress={progress?.find((p) => p.exerciseId === exercise.id)}
progress={progress?.find((p) => p.exercise_id === exercise.id)}
onStart={onSelectExercise}
/>
))}

View File

@ -2,15 +2,17 @@ import { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { useExerciseWords } from "@/hooks/phonics/useExerciseAttempt";
import { useExerciseWords, useExerciseAttempt } from "@/hooks/phonics/useExerciseAttempt";
import { useUpdatePhonicsProgress } from "@/hooks/phonics/usePhonicsProgress";
import { ExerciseFactory } from "./exercises/ExerciseFactory";
import { Timer } from "lucide-react";
import type { PhonicsExercise, PhonicsWord } from "@/types/phonics";
import type { PhonicsExercise, PhonicsWord, PhonicsExerciseType } from "@/types/phonics";
import { cn } from "@/lib/utils";
interface ExercisePlayerProps {
exercise: PhonicsExercise;
exercise: PhonicsExercise & {
type: PhonicsExerciseType;
};
studentId: string;
onComplete: () => void;
onExit: () => void;
@ -32,6 +34,7 @@ export function ExercisePlayer({
const { data: exerciseWords, isLoading } = useExerciseWords(exercise.id);
const updateProgress = useUpdatePhonicsProgress();
const exerciseAttempt = useExerciseAttempt();
useEffect(() => {
const timer = setInterval(() => {
@ -65,17 +68,21 @@ export function ExercisePlayer({
const handleComplete = async () => {
const finalScore = score / (exerciseWords?.length || 1);
const stars = Math.ceil(finalScore * 3);
// Primeiro registra a tentativa
await exerciseAttempt.mutateAsync({
student_id: studentId,
exercise_id: exercise.id,
score: finalScore,
time_spent_seconds: timeSpent
});
// Depois atualiza o progresso
await updateProgress.mutateAsync({
studentId,
exerciseId: exercise.id,
attempts: 1,
bestScore: finalScore,
lastScore: finalScore,
completed: finalScore >= exercise.requiredScore,
stars,
xpEarned: Math.round(finalScore * exercise.points)
student_id: studentId,
exercise_id: exercise.id,
score: finalScore,
time_spent_seconds: timeSpent
});
onComplete();
@ -94,7 +101,8 @@ export function ExercisePlayer({
}
const progress = ((currentStep + 1) / exerciseWords.length) * 100;
const currentWord = exerciseWords[currentStep].word as unknown as PhonicsWord;
const currentWord = exerciseWords[currentStep].word;
const options = exerciseWords[currentStep].options;
return (
<Card className={cn(
@ -110,7 +118,12 @@ export function ExercisePlayer({
<Timer className="w-4 h-4" />
<span>{Math.floor(timeSpent / 60)}:{(timeSpent % 60).toString().padStart(2, '0')}</span>
</div>
<Button variant="outline" size="sm" onClick={onExit}>
<Button
trackingId="exit-exercise"
variant="outline"
size="sm"
onClick={onExit}
>
Sair
</Button>
</div>
@ -125,9 +138,9 @@ export function ExercisePlayer({
</div>
<ExerciseFactory
type={exercise.exerciseType}
type={exercise.type}
currentWord={currentWord}
options={exerciseWords[currentStep].options}
options={options}
onAnswer={handleAnswer}
disabled={showFeedback}
/>

View File

@ -27,6 +27,7 @@ export function AlliterationExercise({
<div className="grid grid-cols-2 gap-4">
{options.map((option) => (
<Button
trackingId={`alliteration-option-${option.word}`}
key={option.word}
onClick={() => onAnswer(option.word, option.hasSameInitialSound)}
disabled={disabled}

View File

@ -13,7 +13,7 @@ interface ExerciseFactoryProps {
}
export function ExerciseFactory({ type, currentWord, options, onAnswer, disabled }: ExerciseFactoryProps) {
switch (type) {
switch (type.name) {
case 'rhyme':
return (
<RhymeExercise
@ -70,7 +70,7 @@ export function ExerciseFactory({ type, currentWord, options, onAnswer, disabled
default:
return (
<div className="text-center text-red-500">
Tipo de exercício não implementado: {type}
Tipo de exercício não implementado: {type.name}
</div>
);
}

View File

@ -22,6 +22,7 @@ export function RhymeExercise({ currentWord, onAnswer, options, disabled }: Rhym
<div className="grid grid-cols-2 gap-4">
{options.map((option) => (
<Button
trackingId={`rhyme-option-${option.word}`}
key={option.word}
onClick={() => onAnswer(option.word, option.isRhyme)}
disabled={disabled}

View File

@ -33,6 +33,7 @@ export function SoundMatchExercise({
<div className="grid grid-cols-2 gap-4">
{options.map((option) => (
<Button
trackingId={`sound-match-option-${option.word}`}
key={option.word}
onClick={() => onAnswer(option.word, option.hasMatchingSound)}
disabled={disabled}

View File

@ -44,6 +44,7 @@ export function SyllablesExercise({
<div className="flex flex-wrap justify-center gap-2">
{syllables.map((syllable, index) => (
<Button
trackingId={`syllable-option-${index}`}
key={index}
onClick={() => handleSyllableClick(index)}
disabled={disabled}
@ -62,6 +63,7 @@ export function SyllablesExercise({
{selectedSyllables.length > 0 && !disabled && (
<div className="flex justify-center">
<Button
trackingId="check-syllables"
onClick={handleCheck}
className="mt-4"
>

View File

@ -1,27 +1,17 @@
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';
export function useExerciseAttempt() {
const queryClient = useQueryClient();
interface AttemptParams {
student_id: string;
exercise_id: string;
score: number;
time_spent_seconds: number;
}
return useMutation({
mutationFn: async (attempt: Omit<PhonicsAttempt, 'id'>) => {
const { data, error } = await supabase
.from('phonics_student_attempts')
.insert(attempt)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: (data, variables) => {
queryClient.invalidateQueries({
queryKey: ['phonics-progress', variables.studentId]
});
}
});
interface ExerciseWord {
word: PhonicsWord;
options: PhonicsWord[];
}
export function useExerciseWords(exerciseId: string) {
@ -31,14 +21,54 @@ export function useExerciseWords(exerciseId: string) {
const { data, error } = await supabase
.from('phonics_exercise_words')
.select(`
*,
word:phonics_words(*)
word:phonics_words!inner (
id,
word,
audio_url,
phonetic_transcription,
syllables_count,
created_at
),
options:phonics_words!inner (
id,
word,
audio_url,
phonetic_transcription,
syllables_count,
created_at
)
`)
.eq('exercise_id', exerciseId)
.order('order_index', { ascending: true });
if (error) throw error;
// Transformar os dados para o formato correto
return (data || []).map(item => ({
word: item.word[0],
options: item.options
})) as ExerciseWord[];
},
enabled: !!exerciseId
});
}
export function useExerciseAttempt() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (params: AttemptParams) => {
const { data, error } = await supabase
.from('student_phonics_attempts')
.insert(params)
.select()
.single();
if (error) throw error;
return data;
},
onSuccess: (_, { student_id }) => {
queryClient.invalidateQueries({ queryKey: ['phonics-progress', student_id] });
}
});
}

View File

@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
import type { PhonicsExercise, PhonicsExerciseCategory } from '@/types/phonics';
import type { PhonicsExercise, PhonicsExerciseType, PhonicsExerciseCategory } from '@/types/phonics';
export function usePhonicsExercises(categoryId?: string) {
return useQuery({
@ -10,23 +10,20 @@ export function usePhonicsExercises(categoryId?: string) {
.from('phonics_exercises')
.select(`
*,
category:phonics_exercise_categories(name),
type:phonics_exercise_types(name),
words:phonics_exercise_words(
word:phonics_words(*)
)
type:phonics_exercise_types (*),
category:phonics_exercise_categories (*)
`)
.eq('is_active', true)
.order('difficulty_level', { ascending: true });
.order('order_index', { ascending: true });
if (categoryId) {
query.eq('category_id', categoryId);
}
const { data, error } = await query;
if (error) throw error;
return data as PhonicsExercise[];
return data as (PhonicsExercise & { type: PhonicsExerciseType })[];
}
});
}

View File

@ -2,6 +2,13 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/lib/supabase';
import type { StudentPhonicsProgress } from '@/types/phonics';
interface UpdateProgressParams {
student_id: string;
exercise_id: string;
score: number;
time_spent_seconds: number;
}
export function usePhonicsProgress(studentId: string) {
return useQuery({
queryKey: ['phonics-progress', studentId],
@ -22,44 +29,19 @@ 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()
.single();
if (attemptError) throw attemptError;
// Depois, atualiza ou cria o progresso
const { data: progressData, error: progressError } = await supabase
mutationFn: async (params: UpdateProgressParams) => {
const { data, error } = await supabase
.from('student_phonics_progress')
.upsert({
student_id: studentId,
exercise_id: exerciseId,
student_id: params.student_id,
exercise_id: params.exercise_id,
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)
best_score: params.score,
last_score: params.score,
completed: params.score >= 0.7,
completed_at: params.score >= 0.7 ? new Date().toISOString() : null,
stars: Math.ceil(params.score * 3),
xp_earned: Math.ceil(params.score * 100)
}, {
onConflict: 'student_id,exercise_id',
ignoreDuplicates: false
@ -67,12 +49,11 @@ export function useUpdatePhonicsProgress() {
.select()
.single();
if (progressError) throw progressError;
return progressData;
if (error) throw error;
return data;
},
onSuccess: (_, { studentId }) => {
queryClient.invalidateQueries({ queryKey: ['phonics-progress', studentId] });
onSuccess: (_, { student_id }) => {
queryClient.invalidateQueries({ queryKey: ['phonics-progress', student_id] });
}
});
}

View File

@ -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() {
<CardContent>
<div className="space-y-4">
{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 (
<div key={exercise.id} className="flex items-center gap-4">

View File

@ -1,9 +1,8 @@
export { StudentDashboardLayout } from './StudentDashboardLayout';
export { StudentDashboard } from './StudentDashboard';
export { StudentClassPage } from './StudentClassPage';
export { StudentSettingsPage } from './StudentSettingsPage';
export { CreateStoryPage } from './CreateStoryPage';
export { StoryPage } from './StoryPage';
export { StudentDashboardPage } from './StudentDashboardPage';
export { StudentStoriesPage } from './StudentStoriesPage';
export { PhonicsPage } from './PhonicsPage';