feat: Adiciona toggle de texto maiúsculo para apoio à alfabetização

- Implementa componente TextCaseToggle para alternância de caixa
- Cria sistema de texto adaptativo com componentes AdaptiveText
- Adiciona hook useUppercasePreference para gerenciar estado
- Integra funcionalidade em todas as páginas principais
- Persiste preferência do usuário no banco de dados
This commit is contained in:
Lucas Santana 2025-01-23 15:29:08 -03:00
parent 7880ce8dda
commit e4c225ebd7
5 changed files with 101 additions and 16 deletions

View File

@ -54,3 +54,21 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
### Removido
- N/A (primeira versão)
## [1.1.0] - 2024-05-20
### Adicionado
- Novo componente `TextCaseToggle` para alternar entre maiúsculas/minúsculas
- Componentes de texto adaptativo (`AdaptiveText`, `AdaptiveTitle`, `AdaptiveParagraph`)
- Hook `useUppercasePreference` para gerenciar preferência do usuário
- Suporte a texto em maiúsculas para crianças em fase de alfabetização
### Modificado
- Páginas de histórias e exercícios para usar o novo sistema de texto
- Cabeçalho das páginas principal com controle de caixa de texto
- Componentes de exercícios para suportar transformação de texto
### Técnico
- Adicionada coluna `uppercase_text_preferences` na tabela students
- Sistema de persistência de preferências via Supabase
- Otimizações de performance com memoização de componentes

View File

@ -3,12 +3,17 @@ import { ArrowLeft, Sparkles } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { StoryGenerator } from '../../components/story/StoryGenerator';
import { useSession } from '../../hooks/useSession';
import { TextCaseToggle } from '../../components/ui/text-case-toggle';
import { AdaptiveTitle, AdaptiveParagraph, AdaptiveText } from '../../components/ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
export function CreateStoryPage() {
const navigate = useNavigate();
const { session } = useSession();
const [error, setError] = React.useState<string | null>(null);
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
if (!session) {
return (
<div className="text-center py-12">
@ -39,11 +44,23 @@ export function CreateStoryPage() {
<Sparkles className="h-6 w-6 text-purple-600" />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">Criar Nova História</h1>
<p className="text-gray-600">
Vamos criar uma história personalizada baseada nos seus interesses
</p>
<AdaptiveTitle
text="Criar Nova História"
isUpperCase={isUpperCase}
className="text-2xl font-bold text-gray-900"
/>
<AdaptiveParagraph
text="Vamos criar uma história personalizada baseada nos seus interesses"
isUpperCase={isUpperCase}
className="text-gray-600"
/>
</div>
<TextCaseToggle
isUpperCase={isUpperCase}
onToggle={toggleUppercase}
isLoading={isLoading}
className="ml-auto"
/>
</div>
{error && (
@ -59,10 +76,19 @@ export function CreateStoryPage() {
Como funciona?
</h3>
<ol className="text-sm text-purple-700 space-y-2">
<li>1. Conte-nos sobre seus interesses e preferências</li>
<li>2. Escolha personagens e cenários para sua história</li>
<li>3. Nossa IA criará uma história única e personalizada</li>
<li>4. Você poderá ler e praticar com sua nova história</li>
{[
'Conte-nos sobre seus interesses e preferências',
'Escolha personagens e cenários para sua história',
'Nossa IA criará uma história única e personalizada',
'Você poderá ler e praticar com sua nova história'
].map((text, index) => (
<AdaptiveText
key={index}
text={`${index + 1}. ${text}`}
isUpperCase={isUpperCase}
as="li"
/>
))}
</ol>
</div>
</div>

View File

@ -5,6 +5,10 @@ import { WordFormation } from '../../components/exercises/WordFormation';
import { SentenceCompletion } from '../../components/exercises/SentenceCompletion';
import { PronunciationPractice } from '../../components/exercises/PronunciationPractice';
import { ArrowLeft } from 'lucide-react';
import { TextCaseToggle } from '../../components/ui/text-case-toggle';
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components/ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSession } from '../../hooks/useSession';
interface ExerciseWord {
word: string;
@ -31,6 +35,8 @@ export function ExercisePage() {
const [exerciseData, setExerciseData] = React.useState<any>(null);
const [exerciseWords, setExerciseWords] = React.useState<ExerciseWord[]>([]);
const [error, setError] = React.useState<string | null>(null);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
React.useEffect(() => {
const loadExerciseData = async () => {
@ -136,7 +142,9 @@ export function ExercisePage() {
case 'word-formation':
return (
<WordFormation
words={exerciseWords.map(w => w.word)}
words={exerciseWords.map(w =>
isUpperCase ? w.word.toUpperCase() : w.word
)}
storyId={storyId as string}
studentId={exerciseData.story.student_id}
/>
@ -170,8 +178,24 @@ export function ExercisePage() {
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
>
<ArrowLeft className="w-5 h-5" />
Voltar para história
<AdaptiveText
text="Voltar para história"
isUpperCase={isUpperCase}
/>
</button>
<div className="flex justify-between items-center">
<AdaptiveTitle
text={`Exercício: ${type?.replace('-', ' ')}`}
isUpperCase={isUpperCase}
className="text-xl font-bold"
/>
<TextCaseToggle
isUpperCase={isUpperCase}
onToggle={toggleUppercase}
isLoading={isLoading}
/>
</div>
</div>
{/* Exercício */}

View File

@ -8,6 +8,10 @@ import { StoryMetrics } from '../../components/story/StoryMetrics';
import { convertWebmToMp3 } from '../../utils/audioConverter';
import * as Dialog from '@radix-ui/react-dialog';
import { ExerciseSuggestions } from '../../components/learning/ExerciseSuggestions';
import { TextCaseToggle } from '../../components/ui/text-case-toggle';
import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components/ui/adaptive-text';
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
import { useSession } from '../../hooks/useSession';
interface StoryRecording {
id: string;
@ -385,6 +389,8 @@ export function StoryPage() {
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const { session } = useSession();
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
React.useEffect(() => {
const fetchStory = async () => {
@ -602,10 +608,15 @@ export function StoryPage() {
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
>
<ArrowLeft className="h-5 w-5" />
Voltar para histórias
<AdaptiveText text="Voltar para histórias" isUpperCase={isUpperCase} />
</button>
<div className="flex items-center gap-4">
<TextCaseToggle
isUpperCase={isUpperCase}
onToggle={toggleUppercase}
isLoading={isLoading}
/>
<button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
@ -635,12 +646,18 @@ export function StoryPage() {
)}
<div className="p-8">
<h1 className="text-3xl font-bold text-gray-900 mb-6">{story?.title}</h1>
<AdaptiveTitle
text={story?.title || ''}
isUpperCase={isUpperCase}
className="text-3xl font-bold text-gray-900 mb-6"
/>
{/* Texto da página atual */}
<p className="text-xl leading-relaxed text-gray-700 mb-8">
{story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
</p>
<AdaptiveParagraph
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
isUpperCase={isUpperCase}
className="text-xl leading-relaxed text-gray-700 mb-8"
/>
{/* Gravador de áudio */}
<AudioRecorder

View File

@ -64,7 +64,7 @@ export function StudentDashboardLayout() {
{!isCollapsed && <span>Minhas Histórias</span>}
</NavLink>
<NavLink
{/* <NavLink
to="/aluno/conquistas"
onClick={handleNavigation}
className={({ isActive }) =>