mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-16 21:37:51 +00:00
feat: adicionando controles de texto
This commit is contained in:
parent
59a7adfeee
commit
dd9e2f4dd3
@ -1,46 +1,129 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ChevronUp, ChevronDown, Play, Pause } from 'lucide-react';
|
||||
|
||||
interface WordHighlighterProps {
|
||||
text: string; // Texto completo
|
||||
highlightedWords: string[]; // Palavras para destacar (ex: palavras difíceis)
|
||||
difficultWords: string[]; // Palavras que o aluno teve dificuldade
|
||||
onWordClick: (word: string) => void; // Função para quando clicar na palavra
|
||||
highlightSpeed?: number; // palavras por minuto
|
||||
initialFontSize?: number;
|
||||
}
|
||||
|
||||
export function WordHighlighter({
|
||||
text,
|
||||
highlightedWords,
|
||||
difficultWords,
|
||||
onWordClick
|
||||
onWordClick,
|
||||
highlightSpeed = 60,
|
||||
initialFontSize = 18
|
||||
}: WordHighlighterProps) {
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentWordIndex, setCurrentWordIndex] = useState(0);
|
||||
const [fontSize, setFontSize] = useState(initialFontSize);
|
||||
|
||||
// Divide o texto em palavras mantendo a pontuação
|
||||
const words = text.split(/(\s+)/).filter(word => word.trim().length > 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPlaying) return;
|
||||
|
||||
const intervalTime = (60 / highlightSpeed) * 1000;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentWordIndex((prevIndex) => {
|
||||
if (prevIndex >= words.length - 1) {
|
||||
setIsPlaying(false);
|
||||
return prevIndex;
|
||||
}
|
||||
return prevIndex + 1;
|
||||
});
|
||||
}, intervalTime);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isPlaying, highlightSpeed, words.length]);
|
||||
|
||||
const handleFontSizeChange = (delta: number) => {
|
||||
setFontSize(prev => Math.min(Math.max(12, prev + delta), 32));
|
||||
};
|
||||
|
||||
const togglePlayPause = () => {
|
||||
if (!isPlaying) {
|
||||
setCurrentWordIndex(0);
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="leading-relaxed text-lg space-y-4">
|
||||
{words.map((word, i) => {
|
||||
// Remove pontuação para comparação
|
||||
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/, '');
|
||||
|
||||
// Determina o estilo baseado no tipo da palavra
|
||||
const isHighlighted = highlightedWords.includes(cleanWord);
|
||||
const isDifficult = difficultWords.includes(cleanWord);
|
||||
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
onClick={() => onWordClick(word)}
|
||||
className={`
|
||||
inline-block mx-1 px-1 rounded cursor-pointer transition-all
|
||||
hover:scale-110
|
||||
${isHighlighted ? 'bg-yellow-200 hover:bg-yellow-300' : ''}
|
||||
${isDifficult ? 'bg-red-100 hover:bg-red-200' : ''}
|
||||
hover:bg-gray-100
|
||||
`}
|
||||
title="Clique para ver mais informações"
|
||||
<div className="space-y-4">
|
||||
{/* Controles */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleFontSizeChange(-2)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100"
|
||||
aria-label="Diminuir fonte"
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
</button>
|
||||
<span className="text-sm font-medium">{fontSize}px</span>
|
||||
<button
|
||||
onClick={() => handleFontSizeChange(2)}
|
||||
className="p-2 rounded-lg hover:bg-gray-100"
|
||||
aria-label="Aumentar fonte"
|
||||
>
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={togglePlayPause}
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-purple-100 text-purple-700 hover:bg-purple-200"
|
||||
>
|
||||
{isPlaying ? (
|
||||
<>
|
||||
<Pause className="h-4 w-4" />
|
||||
Pausar Leitura
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
Iniciar Leitura
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Texto */}
|
||||
<div
|
||||
className="leading-relaxed space-y-4"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
>
|
||||
{words.map((word, i) => {
|
||||
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/, '');
|
||||
const isHighlighted = highlightedWords.includes(cleanWord);
|
||||
const isDifficult = difficultWords.includes(cleanWord);
|
||||
const isCurrentWord = i === currentWordIndex && isPlaying;
|
||||
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
onClick={() => onWordClick(word)}
|
||||
className={`
|
||||
inline-block mx-1 px-1 rounded cursor-pointer transition-all
|
||||
hover:scale-110
|
||||
${isHighlighted ? 'bg-yellow-200 hover:bg-yellow-300' : ''}
|
||||
${isDifficult ? 'bg-red-100 hover:bg-red-200' : ''}
|
||||
${isCurrentWord ? 'bg-purple-200 scale-110' : ''}
|
||||
hover:bg-gray-100
|
||||
`}
|
||||
title="Clique para ver mais informações"
|
||||
>
|
||||
{word}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { SyllableHighlighter } from '../../features/syllables/components/SyllableHighlighter';
|
||||
import { formatTextWithSyllables } from '../../features/syllables/utils/syllableSplitter';
|
||||
|
||||
interface AdaptiveTextProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||
text: string;
|
||||
@ -19,11 +20,8 @@ export const AdaptiveText = React.memo(({
|
||||
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]);
|
||||
const formattedText = formatTextWithSyllables(text, highlightSyllables);
|
||||
const finalText = isUpperCase ? formattedText.toUpperCase() : formattedText;
|
||||
|
||||
return React.createElement(
|
||||
Component,
|
||||
@ -34,9 +32,7 @@ export const AdaptiveText = React.memo(({
|
||||
),
|
||||
...props
|
||||
},
|
||||
highlightSyllables ? (
|
||||
<SyllableHighlighter text={transformedText} />
|
||||
) : transformedText
|
||||
finalText
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
250
src/components/ui/text-controls.tsx
Normal file
250
src/components/ui/text-controls.tsx
Normal file
@ -0,0 +1,250 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Type,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
Play,
|
||||
Pause,
|
||||
Timer,
|
||||
ArrowLeftRight,
|
||||
MoveVertical
|
||||
} from 'lucide-react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface TextControlsProps {
|
||||
isUpperCase: boolean;
|
||||
onToggleUpperCase: () => void;
|
||||
isSyllablesEnabled: boolean;
|
||||
onToggleSyllables: () => void;
|
||||
fontSize: number;
|
||||
onFontSizeChange: (size: number) => void;
|
||||
readingSpeed: number;
|
||||
onReadingSpeedChange: (speed: number) => void;
|
||||
letterSpacing: number;
|
||||
onLetterSpacingChange: (spacing: number) => void;
|
||||
wordSpacing: number;
|
||||
onWordSpacingChange: (spacing: number) => void;
|
||||
lineHeight: number;
|
||||
onLineHeightChange: (height: number) => void;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
isHighlighting?: boolean;
|
||||
onToggleHighlight?: () => void;
|
||||
}
|
||||
|
||||
export function TextControls({
|
||||
isUpperCase,
|
||||
onToggleUpperCase,
|
||||
isSyllablesEnabled,
|
||||
onToggleSyllables,
|
||||
fontSize,
|
||||
onFontSizeChange,
|
||||
readingSpeed,
|
||||
onReadingSpeedChange,
|
||||
letterSpacing,
|
||||
onLetterSpacingChange,
|
||||
wordSpacing,
|
||||
onWordSpacingChange,
|
||||
lineHeight,
|
||||
onLineHeightChange,
|
||||
isLoading = false,
|
||||
className,
|
||||
isHighlighting = false,
|
||||
onToggleHighlight
|
||||
}: TextControlsProps) {
|
||||
const handleFontSizeChange = (delta: number) => {
|
||||
const newSize = Math.min(Math.max(12, fontSize + delta), 32);
|
||||
onFontSizeChange(newSize);
|
||||
};
|
||||
|
||||
const handleReadingSpeedChange = (delta: number) => {
|
||||
const newSpeed = Math.min(Math.max(30, readingSpeed + delta), 300);
|
||||
onReadingSpeedChange(newSpeed);
|
||||
};
|
||||
|
||||
const handleLetterSpacingChange = (delta: number) => {
|
||||
const newSpacing = Math.min(Math.max(0, letterSpacing + delta), 10);
|
||||
onLetterSpacingChange(newSpacing);
|
||||
};
|
||||
|
||||
const handleWordSpacingChange = (delta: number) => {
|
||||
const newSpacing = Math.min(Math.max(0, wordSpacing + delta), 20);
|
||||
onWordSpacingChange(newSpacing);
|
||||
};
|
||||
|
||||
const handleLineHeightChange = (delta: number) => {
|
||||
const newHeight = Math.min(Math.max(1, lineHeight + delta), 3);
|
||||
onLineHeightChange(newHeight);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Primeira Seção: Controles Principais */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* Controle de Maiúsculas */}
|
||||
<button
|
||||
onClick={onToggleUpperCase}
|
||||
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'
|
||||
)}
|
||||
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: Minúsculas' : 'AA: MAIÚSCULAS'}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Controle de Sílabas */}
|
||||
<button
|
||||
onClick={onToggleSyllables}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors',
|
||||
'text-gray-600 hover:text-gray-900',
|
||||
'border border-gray-200',
|
||||
isSyllablesEnabled ? 'bg-purple-50 text-purple-600 border-purple-200' : 'hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
<span className="text-sm font-medium">Sí-la-bas</span>
|
||||
</button>
|
||||
|
||||
{/* Word Highlighter */}
|
||||
<button
|
||||
onClick={onToggleHighlight}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-1.5 rounded-lg transition-colors',
|
||||
'text-gray-600 hover:text-gray-900',
|
||||
'border border-gray-200',
|
||||
isHighlighting ? 'bg-blue-50 text-blue-600 border-blue-200' : 'hover:bg-gray-100'
|
||||
)}
|
||||
>
|
||||
{isHighlighting ? (
|
||||
<>
|
||||
<Pause className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Pausar</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4" />
|
||||
<span className="text-sm font-medium">Destacar Palavras</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Segunda Seção: Ajustes de Texto */}
|
||||
<div className="flex items-center gap-3 flex-wrap">
|
||||
{/* Controle de Tamanho da Fonte */}
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleFontSizeChange(-2)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-l-lg"
|
||||
aria-label="Diminuir fonte"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="text-sm font-medium px-2">{fontSize}px</span>
|
||||
<button
|
||||
onClick={() => handleFontSizeChange(2)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-r-lg"
|
||||
aria-label="Aumentar fonte"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controle de Velocidade de Leitura */}
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleReadingSpeedChange(-10)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-l-lg"
|
||||
aria-label="Diminuir velocidade"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<Timer className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">{readingSpeed} ppm</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleReadingSpeedChange(10)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-r-lg"
|
||||
aria-label="Aumentar velocidade"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controle de Espaçamento entre Letras */}
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleLetterSpacingChange(-0.5)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-l-lg"
|
||||
aria-label="Diminuir espaçamento entre letras"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<Type className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">{letterSpacing}px</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleLetterSpacingChange(0.5)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-r-lg"
|
||||
aria-label="Aumentar espaçamento entre letras"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controle de Espaçamento entre Palavras */}
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleWordSpacingChange(-1)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-l-lg"
|
||||
aria-label="Diminuir espaçamento entre palavras"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<ArrowLeftRight className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">{wordSpacing}px</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleWordSpacingChange(1)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-r-lg"
|
||||
aria-label="Aumentar espaçamento entre palavras"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controle de Altura da Linha */}
|
||||
<div className="flex items-center gap-1 border border-gray-200 rounded-lg">
|
||||
<button
|
||||
onClick={() => handleLineHeightChange(-0.1)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-l-lg"
|
||||
aria-label="Diminuir altura da linha"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
<div className="flex items-center gap-1 px-2">
|
||||
<MoveVertical className="h-4 w-4 text-gray-500" />
|
||||
<span className="text-sm font-medium">{lineHeight.toFixed(1)}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleLineHeightChange(0.1)}
|
||||
className="p-1.5 hover:bg-gray-100 rounded-r-lg"
|
||||
aria-label="Aumentar altura da linha"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -19,4 +19,17 @@ export function splitIntoSyllables(word: string): string[] {
|
||||
}
|
||||
|
||||
return syllables.length > 0 ? syllables : [word];
|
||||
}
|
||||
|
||||
export function highlightSyllables(text: string): string {
|
||||
const words = text.split(/\s+/);
|
||||
return words.map(word => {
|
||||
const syllables = splitIntoSyllables(word);
|
||||
return syllables.join('-');
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
export function formatTextWithSyllables(text: string, shouldHighlight: boolean): string {
|
||||
if (!shouldHighlight) return text;
|
||||
return highlightSyllables(text);
|
||||
}
|
||||
@ -13,6 +13,8 @@ import { AdaptiveText, AdaptiveTitle, AdaptiveParagraph } from '../../components
|
||||
import { useSession } from '../../hooks/useSession';
|
||||
import { useSyllables } from '../../features/syllables/hooks/useSyllables';
|
||||
import { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||
import { TextControls } from '../../components/ui/text-controls';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
|
||||
interface StoryRecording {
|
||||
@ -399,8 +401,83 @@ export function StoryPage() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const { session } = useSession();
|
||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
||||
const { isHighlighted, toggleHighlight } = useSyllables();
|
||||
const { isHighlighted: isSyllablesEnabled, toggleHighlight: toggleSyllables } = useSyllables();
|
||||
const [fontSize, setFontSize] = useState(18);
|
||||
const [readingSpeed, setReadingSpeed] = useState(120); // 120 palavras por minuto
|
||||
const [letterSpacing, setLetterSpacing] = useState(0);
|
||||
const [wordSpacing, setWordSpacing] = useState(0);
|
||||
const [lineHeight, setLineHeight] = useState(1.5);
|
||||
const [isHighlighting, setIsHighlighting] = useState(false);
|
||||
const [currentWordIndex, setCurrentWordIndex] = useState(-1);
|
||||
const highlightInterval = React.useRef<number | null>(null);
|
||||
|
||||
// Função para dividir o texto em palavras
|
||||
const getWords = (text: string) => text.split(/\s+/);
|
||||
|
||||
// Atualizar o useEffect do highlighting para usar readingSpeed
|
||||
useEffect(() => {
|
||||
if (isHighlighting) {
|
||||
const words = getWords(story?.content?.pages?.[currentPage]?.text || '');
|
||||
const intervalTime = (60 / readingSpeed) * 1000; // Converter palavras por minuto para milissegundos
|
||||
|
||||
highlightInterval.current = window.setInterval(() => {
|
||||
setCurrentWordIndex(prev => {
|
||||
if (prev >= words.length - 1) {
|
||||
setIsHighlighting(false);
|
||||
return -1;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
}, intervalTime);
|
||||
|
||||
return () => {
|
||||
if (highlightInterval.current) {
|
||||
window.clearInterval(highlightInterval.current);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
setCurrentWordIndex(-1);
|
||||
if (highlightInterval.current) {
|
||||
window.clearInterval(highlightInterval.current);
|
||||
}
|
||||
}
|
||||
}, [isHighlighting, currentPage, story?.content?.pages, readingSpeed]);
|
||||
|
||||
// Função para renderizar o texto com destaque
|
||||
const renderHighlightedText = (text: string) => {
|
||||
if (!text || currentWordIndex === -1) {
|
||||
return (
|
||||
<AdaptiveText
|
||||
text={text}
|
||||
isUpperCase={isUpperCase}
|
||||
highlightSyllables={isSyllablesEnabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const words = getWords(text);
|
||||
return (
|
||||
<div className="leading-relaxed whitespace-pre-wrap break-words">
|
||||
{words.map((word, index) => (
|
||||
<span
|
||||
key={index}
|
||||
className={cn(
|
||||
'inline-block',
|
||||
'transition-colors duration-200',
|
||||
index === currentWordIndex && 'bg-yellow-200 rounded px-0.5'
|
||||
)}
|
||||
>
|
||||
<AdaptiveText
|
||||
text={word}
|
||||
isUpperCase={isUpperCase}
|
||||
highlightSyllables={isSyllablesEnabled}
|
||||
/>
|
||||
{' '}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchStory = async () => {
|
||||
@ -447,8 +524,6 @@ export function StoryPage() {
|
||||
fetchStory();
|
||||
}, [id]);
|
||||
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const fetchRecordings = async () => {
|
||||
if (!story?.id) return;
|
||||
@ -628,12 +703,12 @@ export function StoryPage() {
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={toggleHighlight}
|
||||
onClick={toggleSyllables}
|
||||
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'}
|
||||
{isSyllablesEnabled ? 'Desativar Sílabas' : 'Ativar Sílabas'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
@ -643,78 +718,108 @@ export function StoryPage() {
|
||||
<Share2 className="h-5 w-5" />
|
||||
Compartilhar
|
||||
</button>
|
||||
{
|
||||
/* <button
|
||||
onClick={() => setIsPlaying(!isPlaying)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
|
||||
>
|
||||
<Volume2 className="h-5 w-5" />
|
||||
{isPlaying ? 'Pausar' : 'Ouvir'}
|
||||
</button>
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* História Principal */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-8">
|
||||
{/* Imagem da página atual */}
|
||||
{story?.content?.pages?.[currentPage]?.image && (
|
||||
<ImageWithLoading
|
||||
src={story.content.pages[currentPage].image}
|
||||
alt={`Página ${currentPage + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="p-8">
|
||||
<AdaptiveTitle
|
||||
text={story?.title || ''}
|
||||
{/* Conteúdo Principal */}
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-6">
|
||||
{/* 1. Controles de Texto */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
<TextControls
|
||||
isUpperCase={isUpperCase}
|
||||
className="text-3xl font-bold text-gray-900 mb-6"
|
||||
/>
|
||||
|
||||
{/* Texto da página atual */}
|
||||
<AdaptiveParagraph
|
||||
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||
isUpperCase={isUpperCase}
|
||||
highlightSyllables={isHighlighted}
|
||||
className="text-xl leading-relaxed text-gray-700 mb-8"
|
||||
onToggleUpperCase={toggleUppercase}
|
||||
isSyllablesEnabled={isSyllablesEnabled}
|
||||
onToggleSyllables={toggleSyllables}
|
||||
fontSize={fontSize}
|
||||
onFontSizeChange={setFontSize}
|
||||
readingSpeed={readingSpeed}
|
||||
onReadingSpeedChange={setReadingSpeed}
|
||||
letterSpacing={letterSpacing}
|
||||
onLetterSpacingChange={setLetterSpacing}
|
||||
wordSpacing={wordSpacing}
|
||||
onWordSpacingChange={setWordSpacing}
|
||||
lineHeight={lineHeight}
|
||||
onLineHeightChange={setLineHeight}
|
||||
isLoading={isLoading}
|
||||
isHighlighting={isHighlighting}
|
||||
onToggleHighlight={() => setIsHighlighting(prev => !prev)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Gravador de áudio */}
|
||||
<AudioRecorder
|
||||
storyId={story.id}
|
||||
studentId={story.student_id}
|
||||
onAudioUploaded={(audioUrl) => {
|
||||
console.log('Áudio gravado:', audioUrl);
|
||||
{/* 2. Título da História */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">{story?.title}</h1>
|
||||
</div>
|
||||
|
||||
{/* 3. Texto da História */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
<div
|
||||
className="prose max-w-none overflow-hidden"
|
||||
style={{
|
||||
fontSize: `${fontSize}px`,
|
||||
letterSpacing: `${letterSpacing}px`,
|
||||
wordSpacing: `${wordSpacing}px`,
|
||||
lineHeight: lineHeight
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{renderHighlightedText(story?.content?.pages?.[currentPage]?.text || '')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navegação entre páginas */}
|
||||
<div className="flex justify-between items-center mt-8 pt-6 border-t border-gray-200">
|
||||
{/* 4. Controles de Navegação */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(0, prev - 1))}
|
||||
onClick={() => setCurrentPage(prev => prev - 1)}
|
||||
disabled={currentPage === 0}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900 disabled:opacity-50"
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
Anterior
|
||||
Página Anterior
|
||||
</button>
|
||||
|
||||
<span className="text-sm text-gray-500">
|
||||
Página {currentPage + 1} de {story.content.pages.length}
|
||||
Página {currentPage + 1} de {story?.content?.pages?.length}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(story.content.pages.length - 1, prev + 1))}
|
||||
disabled={currentPage === story.content.pages.length - 1}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900 disabled:opacity-50"
|
||||
onClick={() => setCurrentPage(prev => prev + 1)}
|
||||
disabled={currentPage === (story?.content?.pages?.length ?? 0) - 1}
|
||||
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Próxima
|
||||
Próxima Página
|
||||
<ArrowRight className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 5. Imagem da História */}
|
||||
<div className="border-b border-gray-200 pb-6">
|
||||
{story?.content?.pages?.[currentPage]?.image ? (
|
||||
<ImageWithLoading
|
||||
src={story.content.pages[currentPage].image}
|
||||
alt={`Ilustração da página ${currentPage + 1}`}
|
||||
className="w-full rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<div className="aspect-video bg-gray-100 rounded-lg flex items-center justify-center">
|
||||
<p className="text-gray-500">Sem imagem para esta página</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 6. Controle de Gravação */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Gravação de Áudio</h2>
|
||||
<AudioRecorder
|
||||
storyId={story.id}
|
||||
onRecordingComplete={(recording) => {
|
||||
setRecordings(prev => [recording, ...prev]);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dashboard de métricas */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user