mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 22:07:52 +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
|
### Modificado
|
||||||
- N/A (primeira versão)
|
- 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
|
### Removido
|
||||||
- N/A (primeira versão)
|
- 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
|
## [1.1.0] - 2024-05-20
|
||||||
|
|
||||||
### Adicionado
|
### Adicionado
|
||||||
- Novo componente `TextCaseToggle` para alternar entre maiúsculas/minúsculas
|
- Suporte a texto maiúsculo para alfabetização infantil
|
||||||
- Componentes de texto adaptativo (`AdaptiveText`, `AdaptiveTitle`, `AdaptiveParagraph`)
|
- Componente de alternância de caixa de texto
|
||||||
- Hook `useUppercasePreference` para gerenciar preferência do usuário
|
- Sistema de persistência de preferências
|
||||||
- Suporte a texto em maiúsculas para crianças em fase de alfabetização
|
- Destaque silábico interativo para apoio à decodificação
|
||||||
|
|
||||||
### Modificado
|
### Modificado
|
||||||
- Páginas de histórias e exercícios para usar o novo sistema de texto
|
- Todas as páginas principais para usar texto adaptativo
|
||||||
- Cabeçalho das páginas principal com controle de caixa de texto
|
|
||||||
- Componentes de exercícios para suportar transformação de texto
|
- Componentes de exercícios para suportar transformação de texto
|
||||||
|
|
||||||
### Técnico
|
### Técnico
|
||||||
- Adicionada coluna `uppercase_text_preferences` na tabela students
|
- Nova coluna na tabela students
|
||||||
- Sistema de persistência de preferências via Supabase
|
- Hook para gerenciamento de estado
|
||||||
- Otimizações de performance com memoização de componentes
|
- 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 { Timer } from "lucide-react";
|
||||||
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
|
import type { PhonicsExercise, UpdateProgressParams } from "@/types/phonics";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { AdaptiveText } from '../ui/adaptive-text';
|
||||||
|
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||||
|
|
||||||
interface ExercisePlayerProps {
|
interface ExercisePlayerProps {
|
||||||
exercise: PhonicsExercise;
|
exercise: PhonicsExercise;
|
||||||
@ -33,6 +35,7 @@ export function ExercisePlayer({
|
|||||||
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
const [lastAnswerCorrect, setLastAnswerCorrect] = useState<boolean | null>(null);
|
||||||
|
|
||||||
const updateProgress = useUpdatePhonicsProgress();
|
const updateProgress = useUpdatePhonicsProgress();
|
||||||
|
const { isUpperCase } = useUppercasePreference();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@ -150,6 +153,12 @@ export function ExercisePlayer({
|
|||||||
Exercício {currentStep + 1} de {correctWords.length}
|
Exercício {currentStep + 1} de {correctWords.length}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AdaptiveText
|
||||||
|
text={exercise.instructions}
|
||||||
|
isUpperCase={isUpperCase}
|
||||||
|
className="text-lg"
|
||||||
|
/>
|
||||||
|
|
||||||
<ExerciseFactory
|
<ExerciseFactory
|
||||||
type_id={exercise.type_id}
|
type_id={exercise.type_id}
|
||||||
currentWord={currentWord.word}
|
currentWord={currentWord.word}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '../../lib/utils';
|
import { cn } from '../../lib/utils';
|
||||||
|
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
|
||||||
|
|
||||||
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
|
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
text: string;
|
text: string;
|
||||||
isUpperCase: boolean;
|
isUpperCase: boolean;
|
||||||
as?: keyof JSX.IntrinsicElements;
|
as?: keyof JSX.IntrinsicElements;
|
||||||
preserveWhitespace?: boolean;
|
preserveWhitespace?: boolean;
|
||||||
|
highlightSyllables?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AdaptiveText = React.memo(({
|
export const AdaptiveText = React.memo(({
|
||||||
@ -13,6 +15,7 @@ export const AdaptiveText = React.memo(({
|
|||||||
isUpperCase,
|
isUpperCase,
|
||||||
as: Component = 'span',
|
as: Component = 'span',
|
||||||
preserveWhitespace = false,
|
preserveWhitespace = false,
|
||||||
|
highlightSyllables = false,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: AdaptiveTextProps) => {
|
}: AdaptiveTextProps) => {
|
||||||
@ -31,7 +34,9 @@ export const AdaptiveText = React.memo(({
|
|||||||
),
|
),
|
||||||
...props
|
...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 && (
|
{error && (
|
||||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
||||||
{error}
|
<AdaptiveText
|
||||||
|
text={error}
|
||||||
|
isUpperCase={isUpperCase}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -110,7 +110,11 @@ export function ExercisePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto p-6">
|
<div className="container mx-auto p-6">
|
||||||
<div className="text-center">
|
<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
|
<button
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
className="text-purple-600 hover:text-purple-700"
|
className="text-purple-600 hover:text-purple-700"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
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 { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components/ui/adaptive-text';
|
||||||
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||||
import { useSession } from '../../hooks/useSession';
|
import { useSession } from '../../hooks/useSession';
|
||||||
|
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
|
||||||
|
|
||||||
interface StoryRecording {
|
interface StoryRecording {
|
||||||
id: string;
|
id: string;
|
||||||
@ -288,9 +289,11 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
|||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5>
|
<h5 className="text-sm font-medium text-gray-900 mb-2">Transcrição</h5>
|
||||||
<div className="p-4 bg-gray-50 rounded-lg">
|
<div className="p-4 bg-gray-50 rounded-lg">
|
||||||
<p className="text-sm text-gray-600 whitespace-pre-wrap">
|
<AdaptiveText
|
||||||
{recording.transcription || 'Transcrição não disponível'}
|
text={recording.transcription || 'Transcrição não disponível'}
|
||||||
</p>
|
isUpperCase={isUpperCase}
|
||||||
|
className="text-sm text-gray-600 whitespace-pre-wrap"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -391,6 +394,7 @@ export function StoryPage() {
|
|||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||||
|
const { isHighlighted, toggleHighlight } = useSyllables();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchStory = async () => {
|
const fetchStory = async () => {
|
||||||
@ -617,6 +621,15 @@ export function StoryPage() {
|
|||||||
onToggle={toggleUppercase}
|
onToggle={toggleUppercase}
|
||||||
isLoading={isLoading}
|
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
|
<button
|
||||||
onClick={handleShare}
|
onClick={handleShare}
|
||||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
|
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
|
<AdaptiveParagraph
|
||||||
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||||
isUpperCase={isUpperCase}
|
isUpperCase={isUpperCase}
|
||||||
|
highlightSyllables={isHighlighted}
|
||||||
className="text-xl leading-relaxed text-gray-700 mb-8"
|
className="text-xl leading-relaxed text-gray-700 mb-8"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -11,16 +11,21 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
X,
|
X,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
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() {
|
export function StudentDashboardLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { signOut } = useAuth();
|
const { signOut } = useAuth();
|
||||||
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
const [isCollapsed, setIsCollapsed] = React.useState(false);
|
||||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
|
||||||
|
const { session } = useSession();
|
||||||
|
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await signOut();
|
await signOut();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user