story-generator/src/components/exercises/SentenceCompletion.tsx
Lucas Santana 9840fe76b0 feat: aprimora interface do exercício de formação de palavras
- Adiciona barra de progresso e feedback visual
- Implementa lista de palavras encontradas
- Melhora interatividade e estados visuais
- Adiciona validação de palavras repetidas
- Otimiza transições e animações
- Mantém consistência com outros exercícios

type: feat
scope: exercises
breaking: false
2025-01-01 10:09:59 -03:00

178 lines
5.1 KiB
TypeScript

import React, { useState } from 'react';
import { supabase } from '../../lib/supabase';
import { Loader2 } from 'lucide-react';
interface SentenceCompletionProps {
story: {
id: string;
content: {
pages: Array<{
text: string;
}>;
};
};
}
export function SentenceCompletion({ story }: SentenceCompletionProps) {
const [currentSentence, setCurrentSentence] = useState(0);
const [userAnswer, setUserAnswer] = useState('');
const [score, setScore] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [exerciseSentences, setExerciseSentences] = useState<Array<{
sentence: string;
answer: string;
}>>([]);
const [isLoading, setIsLoading] = useState(true);
// Carregar palavras e preparar sentenças
React.useEffect(() => {
const loadExerciseWords = async () => {
try {
const { data: words, error } = await supabase
.from('story_exercise_words')
.select('*')
.eq('story_id', story.id)
.eq('exercise_type', 'completion');
if (error) throw error;
// Extrair todas as sentenças do texto
const allSentences = story.content.pages
.map(page => page.text.split(/[.!?]+/))
.flat()
.filter(Boolean)
.map(sentence => sentence.trim());
// Preparar exercícios com as palavras do banco
const exercises = allSentences
.filter(sentence =>
words?.some(word =>
sentence.toLowerCase().includes(word.word.toLowerCase())
)
)
.map(sentence => {
const word = words?.find(w =>
sentence.toLowerCase().includes(w.word.toLowerCase())
);
return {
sentence: sentence.replace(
new RegExp(word?.word || '', 'i'),
'_____'
),
answer: word?.word || ''
};
})
.filter(exercise => exercise.answer); // Remover exercícios sem resposta
setExerciseSentences(exercises);
setIsLoading(false);
} catch (error) {
console.error('Erro ao carregar palavras:', error);
setIsLoading(false);
}
};
loadExerciseWords();
}, [story.id, story.content.pages]);
if (isLoading) {
return (
<div className="max-w-2xl mx-auto p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-6" />
<div className="h-64 bg-gray-200 rounded" />
</div>
</div>
);
}
if (!exerciseSentences.length) {
return (
<div className="max-w-2xl mx-auto p-6">
<p className="text-gray-600">
Não foi possível gerar exercícios para este texto.
</p>
</div>
);
}
const currentExercise = exerciseSentences[currentSentence];
const handleSubmit = async () => {
setIsSubmitting(true);
try {
const isCorrect = userAnswer.toLowerCase() === currentExercise.answer.toLowerCase();
if (isCorrect) {
setScore(prev => prev + 10);
}
// Avançar para próxima sentença
if (currentSentence < exerciseSentences.length - 1) {
setCurrentSentence(prev => prev + 1);
setUserAnswer('');
}
} catch (error) {
console.error('Erro ao verificar resposta:', error);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="max-w-2xl mx-auto p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Complete a Frase
</h2>
{/* Progresso */}
<div className="mb-6">
<div className="flex justify-between text-sm text-gray-500">
<span>Questão {currentSentence + 1} de {exerciseSentences.length}</span>
<span>{score} pontos</span>
</div>
<div className="mt-2 h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-600 transition-all"
style={{ width: `${((currentSentence + 1) / exerciseSentences.length) * 100}%` }}
/>
</div>
</div>
{/* Sentença atual */}
<div className="mb-8">
<p className="text-xl leading-relaxed text-gray-700">
{currentExercise.sentence}
</p>
</div>
{/* Campo de resposta */}
<div className="mb-8">
<input
type="text"
value={userAnswer}
onChange={(e) => setUserAnswer(e.target.value)}
placeholder="Digite a palavra que completa a frase..."
className="w-full px-4 py-3 text-lg border border-gray-300 rounded-lg
focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
{/* Botão de verificação */}
<button
onClick={handleSubmit}
disabled={!userAnswer || isSubmitting}
className="w-full py-3 bg-purple-600 text-white rounded-lg font-medium
hover:bg-purple-700 disabled:bg-gray-300 disabled:cursor-not-allowed
transition-colors"
>
{isSubmitting ? (
<Loader2 className="w-5 h-5 animate-spin mx-auto" />
) : (
'Verificar'
)}
</button>
</div>
);
}