mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
feat: Adicionando separação de sílabas
This commit is contained in:
parent
229a1bffbb
commit
ea5c5e87f1
20
CHANGELOG.md
20
CHANGELOG.md
@ -51,6 +51,9 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
||||
|
||||
### Modificado
|
||||
- N/A (primeira versão)
|
||||
- Todas as páginas principais para usar texto adaptativo
|
||||
- Componentes de exercícios para suportar transformação de texto
|
||||
- Movido controle de sílabas para a página de histórias
|
||||
|
||||
### Removido
|
||||
- N/A (primeira versão)
|
||||
@ -58,17 +61,16 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
||||
## [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
|
||||
- Suporte a texto maiúsculo para alfabetização infantil
|
||||
- Componente de alternância de caixa de texto
|
||||
- Sistema de persistência de preferências
|
||||
- Destaque silábico interativo para apoio à decodificaçã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
|
||||
- Todas as páginas principais para usar texto adaptativo
|
||||
- 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
|
||||
- Nova coluna na tabela students
|
||||
- Hook para gerenciamento de estado
|
||||
- Otimizações de performance
|
||||
|
||||
15
docs/accessibility-features.md
Normal file
15
docs/accessibility-features.md
Normal file
@ -0,0 +1,15 @@
|
||||
## Destaque Silábico
|
||||
|
||||
**Objetivo:**
|
||||
Facilitar a identificação das sílabas durante a leitura
|
||||
|
||||
**Como usar:**
|
||||
1. Clique no botão "Sílabas" ao lado do título da história
|
||||
2. Todas as palavras serão divididas em sílabas
|
||||
3. Sílabas destacadas em fundo amarelo
|
||||
4. Clique novamente para desativar
|
||||
|
||||
**Benefícios:**
|
||||
- Auxilia na decodificação fonêmica
|
||||
- Promove consciência silábica
|
||||
- Facilita a leitura de palavras complexas
|
||||
@ -7,6 +7,8 @@ import { ExerciseFactory } from "./exercises/ExerciseFactory";
|
||||
import { Timer } from "lucide-react";
|
||||
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AdaptiveText } from '../ui/adaptive-text';
|
||||
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||
|
||||
interface ExercisePlayerProps {
|
||||
exercise: PhonicsExercise;
|
||||
@ -33,6 +35,7 @@ export function ExercisePlayer({
|
||||
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
||||
|
||||
const updateProgress = useUpdatePhonicsProgress();
|
||||
const { isUpperCase } = useUppercasePreference();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -150,6 +153,12 @@ export function ExercisePlayer({
|
||||
Exercício {currentStep + 1} de {correctWords.length}
|
||||
</div>
|
||||
|
||||
<AdaptiveText
|
||||
text={exercise.instructions}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-lg"
|
||||
/>
|
||||
|
||||
<ExerciseFactory
|
||||
type_id={exercise.type_id}
|
||||
currentWord={currentWord.word}
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
|
||||
|
||||
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
text: string;
|
||||
isUpperCase: boolean;
|
||||
as?: keyof JSX.IntrinsicElements;
|
||||
preserveWhitespace?: boolean;
|
||||
highlightSyllables?: boolean;
|
||||
}
|
||||
|
||||
export const AdaptiveText = React.memo(({
|
||||
@ -13,6 +15,7 @@ export const AdaptiveText = React.memo(({
|
||||
isUpperCase,
|
||||
as: Component = 'span',
|
||||
preserveWhitespace = false,
|
||||
highlightSyllables = false,
|
||||
className,
|
||||
...props
|
||||
}: AdaptiveTextProps) => {
|
||||
@ -31,7 +34,9 @@ export const AdaptiveText = React.memo(({
|
||||
),
|
||||
...props
|
||||
},
|
||||
transformedText
|
||||
highlightSyllables ? (
|
||||
<SyllableHighlighter text={transformedText} />
|
||||
) : transformedText
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
29
src/features/syllables/components/SyllableHighlighter.tsx
Normal file
29
src/features/syllables/components/SyllableHighlighter.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { splitIntoSyllables } from '../utils/syllableSplitter';
|
||||
|
||||
interface SyllableHighlighterProps {
|
||||
text: string;
|
||||
highlightColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function SyllableHighlighter({
|
||||
text,
|
||||
highlightColor = 'bg-yellow-100',
|
||||
className
|
||||
}: SyllableHighlighterProps) {
|
||||
const syllables = splitIntoSyllables(text);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{syllables.map((syllable, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={`px-1 rounded ${highlightColor}`}
|
||||
>
|
||||
{syllable}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/features/syllables/hooks/useSyllables.ts
Normal file
10
src/features/syllables/hooks/useSyllables.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export function useSyllables() {
|
||||
const [isHighlighted, setIsHighlighted] = useState(false);
|
||||
|
||||
return {
|
||||
isHighlighted,
|
||||
toggleHighlight: () => setIsHighlighted(!isHighlighted)
|
||||
};
|
||||
}
|
||||
7
src/features/syllables/utils/syllableSplitter.test.ts
Normal file
7
src/features/syllables/utils/syllableSplitter.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { splitIntoSyllables } from './syllableSplitter';
|
||||
|
||||
test('Separação silábica básica', () => {
|
||||
expect(splitIntoSyllables('casa')).toEqual(['ca', 'sa']);
|
||||
expect(splitIntoSyllables('banana')).toEqual(['ba', 'na', 'na']);
|
||||
expect(splitIntoSyllables('computador')).toEqual(['com', 'pu', 'ta', 'dor']);
|
||||
});
|
||||
22
src/features/syllables/utils/syllableSplitter.ts
Normal file
22
src/features/syllables/utils/syllableSplitter.ts
Normal file
@ -0,0 +1,22 @@
|
||||
const VOWELS = new Set(['a', 'e', 'i', 'o', 'u', 'á', 'é', 'í', 'ó', 'ú', 'â', 'ê', 'ô', 'ã', 'õ']);
|
||||
|
||||
export function splitIntoSyllables(word: string): string[] {
|
||||
const syllables: string[] = [];
|
||||
let currentSyllable = '';
|
||||
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
const char = word[i].toLowerCase();
|
||||
currentSyllable += word[i];
|
||||
|
||||
if (VOWELS.has(char)) {
|
||||
if (i < word.length - 1 && !VOWELS.has(word[i+1].toLowerCase())) {
|
||||
syllables.push(currentSyllable);
|
||||
currentSyllable = '';
|
||||
} else if (i === word.length - 1) {
|
||||
syllables.push(currentSyllable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return syllables.length > 0 ? syllables : [word];
|
||||
}
|
||||
@ -65,7 +65,10 @@ export function CreateStoryPage() {
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
||||
{error}
|
||||
<AdaptiveText
|
||||
text={error}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -110,7 +110,11 @@ export function ExercisePage() {
|
||||
return (
|
||||
<div className="container mx-auto p-6">
|
||||
<div className="text-center">
|
||||
<p className="text-red-600 mb-4">{error}</p>
|
||||
<AdaptiveText
|
||||
text={error}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-red-600 mb-4"
|
||||
/>
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="text-purple-600 hover:text-purple-700"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ArrowLeft, ArrowRight, Volume2, Share2, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2 } from 'lucide-react';
|
||||
import { ArrowLeft, ArrowRight, Volume2, Share2, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2, TextSelect } from 'lucide-react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||
@ -12,6 +12,7 @@ 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';
|
||||
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
|
||||
|
||||
interface StoryRecording {
|
||||
id: string;
|
||||
@ -288,9 +289,11 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
||||
<div className="mt-4">
|
||||
<h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5>
|
||||
<div className="p-4 bg-gray-50 rounded-lg">
|
||||
<p className="text-sm text-gray-600 whitespace-pre-wrap">
|
||||
{recording.transcription || 'Transcrição não disponível'}
|
||||
</p>
|
||||
<AdaptiveText
|
||||
text={recording.transcription || 'Transcrição não disponível'}
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-sm text-gray-600 whitespace-pre-wrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -391,6 +394,7 @@ export function StoryPage() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||
const { isHighlighted, toggleHighlight } = useSyllables();
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchStory = async () => {
|
||||
@ -617,6 +621,15 @@ export function StoryPage() {
|
||||
onToggle={toggleUppercase}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={toggleHighlight}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors text-gray-600 hover:text-gray-900 hover:bg-gray-100 border border-gray-200"
|
||||
>
|
||||
<TextSelect className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">
|
||||
{isHighlighted ? 'Desativar Sílabas' : 'Ativar Sílabas'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
|
||||
@ -656,6 +669,7 @@ export function StoryPage() {
|
||||
<AdaptiveParagraph
|
||||
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||
isUpperCase={isUpperCase}
|
||||
highlightSyllables={isHighlighted}
|
||||
className="text-xl leading-relaxed text-gray-700 mb-8"
|
||||
/>
|
||||
|
||||
|
||||
@ -11,16 +11,21 @@ import {
|
||||
Menu,
|
||||
X,
|
||||
ChevronLeft,
|
||||
ChevronRight
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../hooks/useAuth';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { TextCaseToggle } from '../../components/ui/text-case-toggle';
|
||||
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||
import { useSession } from '../../hooks/useSession';
|
||||
|
||||
export function StudentDashboardLayout() {
|
||||
const navigate = useNavigate();
|
||||
const { signOut } = useAuth();
|
||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||
|
||||
const handleLogout = async () => {
|
||||
await signOut();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user