mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
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
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:
parent
350a66bb9e
commit
f37f8f2f6d
@ -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)
|
||||
|
||||
@ -45,6 +45,7 @@ export function AudioPlayer({ word, disabled }: AudioPlayerProps) {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
trackingId="play-word-audio"
|
||||
variant="ghost"
|
||||
size="lg"
|
||||
className="gap-2"
|
||||
|
||||
@ -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"}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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] });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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 })[];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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] });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user