mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
Compare commits
6 Commits
a0cfccc14d
...
62594f5e62
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62594f5e62 | ||
|
|
e154dd2372 | ||
|
|
ea5c5e87f1 | ||
|
|
229a1bffbb | ||
|
|
e4c225ebd7 | ||
|
|
7880ce8dda |
20
CHANGELOG.md
20
CHANGELOG.md
@ -51,6 +51,26 @@ 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)
|
||||
|
||||
## [1.1.0] - 2024-05-20
|
||||
|
||||
### Adicionado
|
||||
- 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
|
||||
- Todas as páginas principais para usar texto adaptativo
|
||||
- Componentes de exercícios para suportar transformação de texto
|
||||
|
||||
### Técnico
|
||||
- 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}
|
||||
|
||||
94
src/components/ui/adaptive-text.tsx
Normal file
94
src/components/ui/adaptive-text.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
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(({
|
||||
text,
|
||||
isUpperCase,
|
||||
as: Component = 'span',
|
||||
preserveWhitespace = false,
|
||||
highlightSyllables = false,
|
||||
className,
|
||||
...props
|
||||
}: AdaptiveTextProps) => {
|
||||
// Transformar o texto mantendo espaços em branco se necessário
|
||||
const transformedText = React.useMemo(() => {
|
||||
const transformed = isUpperCase ? text.toUpperCase() : text;
|
||||
return preserveWhitespace ? transformed : transformed.trim();
|
||||
}, [text, isUpperCase, preserveWhitespace]);
|
||||
|
||||
return React.createElement(
|
||||
Component,
|
||||
{
|
||||
className: cn(
|
||||
'transition-colors duration-200',
|
||||
className
|
||||
),
|
||||
...props
|
||||
},
|
||||
highlightSyllables ? (
|
||||
<SyllableHighlighter text={transformedText} />
|
||||
) : transformedText
|
||||
);
|
||||
});
|
||||
|
||||
AdaptiveText.displayName = 'AdaptiveText';
|
||||
|
||||
// Variantes específicas para diferentes contextos
|
||||
export const AdaptiveTitle = ({
|
||||
className,
|
||||
...props
|
||||
}: AdaptiveTextProps) => (
|
||||
<AdaptiveText
|
||||
as="h1"
|
||||
className={cn(
|
||||
'text-2xl font-bold text-gray-900',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AdaptiveParagraph = ({
|
||||
className,
|
||||
...props
|
||||
}: AdaptiveTextProps) => (
|
||||
<AdaptiveText
|
||||
as="p"
|
||||
className={cn(
|
||||
'text-base text-gray-700 leading-relaxed',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
export const AdaptiveLabel = ({
|
||||
className,
|
||||
...props
|
||||
}: AdaptiveTextProps) => (
|
||||
<AdaptiveText
|
||||
as="span"
|
||||
className={cn(
|
||||
'text-sm font-medium text-gray-600',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
// Hook para memoização de textos longos
|
||||
export function useAdaptiveText(text: string, isUpperCase: boolean) {
|
||||
return React.useMemo(
|
||||
() => isUpperCase ? text.toUpperCase() : text,
|
||||
[text, isUpperCase]
|
||||
);
|
||||
}
|
||||
40
src/components/ui/text-case-toggle.tsx
Normal file
40
src/components/ui/text-case-toggle.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { Loader2, Type } from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface TextCaseToggleProps {
|
||||
isUpperCase: boolean;
|
||||
onToggle: () => void;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function TextCaseToggle({
|
||||
isUpperCase,
|
||||
onToggle,
|
||||
isLoading = false,
|
||||
className
|
||||
}: TextCaseToggleProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onToggle}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'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',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
className
|
||||
)}
|
||||
title={isUpperCase ? 'Mudar para minúsculas' : 'Mudar para maiúsculas'}
|
||||
>
|
||||
<Type className="h-4 w-4" />
|
||||
<span className="text-sm font-medium select-none">
|
||||
{isUpperCase ? 'Aa' : 'AA'}
|
||||
</span>
|
||||
{isLoading && (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
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)
|
||||
};
|
||||
}
|
||||
8
src/features/syllables/utils/syllableSplitter.test.ts
Normal file
8
src/features/syllables/utils/syllableSplitter.test.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { splitIntoSyllables } from './syllableSplitter';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
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];
|
||||
}
|
||||
74
src/hooks/useUppercasePreference.ts
Normal file
74
src/hooks/useUppercasePreference.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { supabase } from '../lib/supabase';
|
||||
|
||||
interface UseUppercasePreferenceReturn {
|
||||
isUpperCase: boolean;
|
||||
toggleUppercase: () => Promise<void>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export function useUppercasePreference(studentId?: string): UseUppercasePreferenceReturn {
|
||||
const [isUpperCase, setIsUpperCase] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadPreference = async () => {
|
||||
if (!studentId) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, error: fetchError } = await supabase
|
||||
.from('students')
|
||||
.select('uppercase_text_preferences')
|
||||
.eq('id', studentId)
|
||||
.single();
|
||||
|
||||
if (fetchError) throw fetchError;
|
||||
|
||||
setIsUpperCase(data?.uppercase_text_preferences ?? false);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Erro ao carregar preferência de texto:', err);
|
||||
setError('Não foi possível carregar sua preferência de texto');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadPreference();
|
||||
}, [studentId]);
|
||||
|
||||
const toggleUppercase = async () => {
|
||||
if (!studentId || isLoading) return;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
const { error: updateError } = await supabase
|
||||
.from('students')
|
||||
.update({ uppercase_text_preferences: !isUpperCase })
|
||||
.eq('id', studentId);
|
||||
|
||||
if (updateError) throw updateError;
|
||||
|
||||
setIsUpperCase(!isUpperCase);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
console.error('Erro ao atualizar preferência de texto:', err);
|
||||
setError('Não foi possível atualizar sua preferência de texto');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
isUpperCase,
|
||||
toggleUppercase,
|
||||
isLoading,
|
||||
error
|
||||
};
|
||||
}
|
||||
@ -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,16 +44,31 @@ 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 && (
|
||||
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
|
||||
{error}
|
||||
<AdaptiveText
|
||||
text={error}
|
||||
isUpperCase={isUpperCase}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -59,10 +79,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>
|
||||
|
||||
@ -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 () => {
|
||||
@ -104,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"
|
||||
@ -136,7 +146,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 +182,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 */}
|
||||
|
||||
@ -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';
|
||||
@ -8,6 +8,12 @@ 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 { useSession } from '../../hooks/useSession';
|
||||
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
|
||||
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||
|
||||
|
||||
interface StoryRecording {
|
||||
id: string;
|
||||
@ -36,6 +42,9 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
||||
const [isConverting, setIsConverting] = React.useState(false);
|
||||
const [mp3Url, setMp3Url] = React.useState<string | null>(null);
|
||||
const [conversionError, setConversionError] = React.useState<string | null>(null);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase } = useUppercasePreference(session?.user?.id);
|
||||
const { isHighlighted } = useSyllables();
|
||||
|
||||
// Verificar suporte ao formato WebM
|
||||
React.useEffect(() => {
|
||||
@ -284,9 +293,12 @@ 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"
|
||||
highlightSyllables={isHighlighted}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -385,6 +397,10 @@ 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);
|
||||
const { isHighlighted, toggleHighlight } = useSyllables();
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchStory = async () => {
|
||||
@ -602,10 +618,24 @@ 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={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"
|
||||
@ -635,12 +665,19 @@ 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}
|
||||
highlightSyllables={isHighlighted}
|
||||
className="text-xl leading-relaxed text-gray-700 mb-8"
|
||||
/>
|
||||
|
||||
{/* Gravador de áudio */}
|
||||
<AudioRecorder
|
||||
|
||||
@ -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();
|
||||
@ -64,7 +69,7 @@ export function StudentDashboardLayout() {
|
||||
{!isCollapsed && <span>Minhas Histórias</span>}
|
||||
</NavLink>
|
||||
|
||||
<NavLink
|
||||
{/* <NavLink
|
||||
to="/aluno/conquistas"
|
||||
onClick={handleNavigation}
|
||||
className={({ isActive }) =>
|
||||
@ -123,7 +128,7 @@ export function StudentDashboardLayout() {
|
||||
<Trophy className="h-5 w-5" />
|
||||
{!isCollapsed && <span>Progresso Fônico</span>}
|
||||
</NavLink>
|
||||
|
||||
*/}
|
||||
<NavLink
|
||||
to="/aluno/configuracoes"
|
||||
onClick={handleNavigation}
|
||||
|
||||
@ -77,7 +77,7 @@ export function StudentDashboardPage() {
|
||||
.eq('student_id', session.user.id)
|
||||
.eq('story_pages.page_number', 1) // Garante que pegamos a primeira página
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(3);
|
||||
.limit(6);
|
||||
|
||||
if (error) throw error;
|
||||
setRecentStories(data || []);
|
||||
@ -252,10 +252,7 @@ export function StudentDashboardPage() {
|
||||
{story.cover && (
|
||||
<div className="relative aspect-video">
|
||||
<img
|
||||
src={supabase.storage
|
||||
.from('story-images')
|
||||
.getPublicUrl(story.cover.image_url).data.publicUrl +
|
||||
`?width=400&height=300&quality=80&format=webp`}
|
||||
src={`${story.cover.image_url}?width=400&height=300&quality=80&format=webp`}
|
||||
alt={story.title}
|
||||
className="w-full h-48 object-cover"
|
||||
loading="lazy"
|
||||
|
||||
@ -25,8 +25,14 @@ export function StudentStoriesPage() {
|
||||
|
||||
const query = supabase
|
||||
.from('stories')
|
||||
.select('*')
|
||||
.eq('student_id', session.user.id);
|
||||
.select(`
|
||||
*,
|
||||
pages:story_pages (
|
||||
image_url
|
||||
)
|
||||
`)
|
||||
.eq('student_id', session.user.id)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (statusFilter !== 'all') {
|
||||
query.eq('status', statusFilter);
|
||||
@ -197,16 +203,21 @@ export function StudentStoriesPage() {
|
||||
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden cursor-pointer hover:shadow-md transition"
|
||||
onClick={() => navigate(`/aluno/historias/${story.id}`)}
|
||||
>
|
||||
{story.cover && (
|
||||
{!story.cover?.image_url && (
|
||||
<div className="bg-gray-100 aspect-video flex items-center justify-center">
|
||||
<BookOpen className="h-12 w-12 text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
{story.cover?.image_url && (
|
||||
<div className="relative aspect-video">
|
||||
<img
|
||||
src={supabase.storage
|
||||
.from('story-images')
|
||||
.getPublicUrl(story.cover.image_url).data.publicUrl +
|
||||
`?width=400&height=300&quality=80&format=webp`}
|
||||
alt={story.title}
|
||||
src={story.cover.image_url}
|
||||
alt={`Capa da história: ${story.title}`}
|
||||
className="w-full h-48 object-cover"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user