mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
173 lines
5.3 KiB
TypeScript
173 lines
5.3 KiB
TypeScript
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 { useUpdatePhonicsProgress } from "@/hooks/phonics/usePhonicsProgress";
|
|
import { ExerciseFactory } from "./exercises/ExerciseFactory";
|
|
import { Timer } from "lucide-react";
|
|
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface ExercisePlayerProps {
|
|
exercise: PhonicsExercise;
|
|
student_id: string;
|
|
onComplete: (result: {
|
|
score: number;
|
|
stars: number;
|
|
xp_earned: number;
|
|
completed: boolean;
|
|
}) => void;
|
|
onExit: () => void;
|
|
}
|
|
|
|
export function ExercisePlayer({
|
|
exercise,
|
|
student_id,
|
|
onComplete,
|
|
onExit
|
|
}: ExercisePlayerProps) {
|
|
const [currentStep, setCurrentStep] = useState(0);
|
|
const [score, setScore] = useState(0);
|
|
const [timeSpent, setTimeSpent] = useState(0);
|
|
const [showFeedback, setShowFeedback] = useState(false);
|
|
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
|
|
|
const updateProgress = useUpdatePhonicsProgress();
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setTimeSpent((prev) => prev + 1);
|
|
}, 1000);
|
|
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
const handleAnswer = async (word: string, isCorrect: boolean) => {
|
|
setLastAnswerCorrect(isCorrect);
|
|
setShowFeedback(true);
|
|
|
|
if (isCorrect) {
|
|
setScore((prev) => prev + 1);
|
|
}
|
|
|
|
// Aguardar feedback antes de prosseguir
|
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
setShowFeedback(false);
|
|
|
|
// Filtra apenas as palavras corretas
|
|
const correctWords = exercise.words?.filter(w => w.is_correct_answer) || [];
|
|
|
|
if (currentStep < correctWords.length - 1) {
|
|
setCurrentStep((prev) => prev + 1);
|
|
} else {
|
|
handleComplete();
|
|
}
|
|
};
|
|
|
|
const handleComplete = async () => {
|
|
// Filtra apenas as palavras corretas
|
|
const correctWords = exercise.words?.filter(w => w.is_correct_answer) || [];
|
|
const finalScore = score / correctWords.length;
|
|
const stars = Math.ceil(finalScore * 3);
|
|
const xp_earned = Math.round(finalScore * exercise.points);
|
|
const completed = finalScore >= exercise.required_score;
|
|
|
|
const updateParams: UpdateProgressParams = {
|
|
student_id,
|
|
exercise_id: exercise.id,
|
|
best_score: finalScore,
|
|
last_score: finalScore,
|
|
completed,
|
|
stars,
|
|
xp_earned,
|
|
time_spent_seconds: timeSpent,
|
|
correct_answers_count: score,
|
|
total_answers_count: correctWords.length
|
|
};
|
|
|
|
await updateProgress.mutateAsync(updateParams);
|
|
onComplete({
|
|
score: finalScore,
|
|
stars,
|
|
xp_earned,
|
|
completed
|
|
});
|
|
};
|
|
|
|
if (!exercise.words?.length) {
|
|
return (
|
|
<Card className="w-full max-w-2xl mx-auto">
|
|
<CardContent className="py-8">
|
|
<div className="text-center text-muted-foreground">
|
|
Carregando exercício...
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Filtra apenas as palavras corretas e ordena por order_index
|
|
const correctWords = exercise.words
|
|
.filter(w => w.is_correct_answer)
|
|
.sort((a, b) => (a.order_index || 0) - (b.order_index || 0));
|
|
|
|
// Pega a palavra atual
|
|
const currentWord = correctWords[currentStep];
|
|
|
|
// Pega as opções (incluindo a palavra correta)
|
|
const options = exercise.words
|
|
.filter(w => w.order_index === currentWord.order_index)
|
|
.map(w => w.word)
|
|
.sort(() => Math.random() - 0.5);
|
|
|
|
const progress = ((currentStep + 1) / correctWords.length) * 100;
|
|
|
|
return (
|
|
<Card className={cn(
|
|
"w-full max-w-2xl mx-auto transition-colors duration-500",
|
|
showFeedback && lastAnswerCorrect && "bg-green-50",
|
|
showFeedback && !lastAnswerCorrect && "bg-red-50"
|
|
)}>
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<CardTitle>{exercise.title}</CardTitle>
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<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} trackingId="exercise-player-exit">
|
|
Sair do Exercício
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<Progress value={progress} className="h-2" />
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
<div className="text-center text-muted-foreground mb-8">
|
|
Exercício {currentStep + 1} de {correctWords.length}
|
|
</div>
|
|
|
|
<ExerciseFactory
|
|
type_id={exercise.type_id}
|
|
currentWord={currentWord.word}
|
|
options={options}
|
|
onAnswer={handleAnswer}
|
|
disabled={showFeedback}
|
|
/>
|
|
|
|
{showFeedback && (
|
|
<div className={cn(
|
|
"text-center text-lg font-medium py-4 rounded-lg",
|
|
lastAnswerCorrect ? "text-green-600" : "text-red-600"
|
|
)}>
|
|
{lastAnswerCorrect ? "Muito bem!" : "Tente novamente na próxima!"}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|