mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 14:27:51 +00:00
Compare commits
No commits in common. "62594f5e62c8d17d07c58d73de06a50e0ccd3866" and "a0cfccc14d869dd69923a4d190b414334f30bfc8" have entirely different histories.
62594f5e62
...
a0cfccc14d
20
CHANGELOG.md
20
CHANGELOG.md
@ -51,26 +51,6 @@ 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)
|
||||||
|
|
||||||
## [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
|
|
||||||
|
|||||||
@ -1,15 +0,0 @@
|
|||||||
## 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,8 +7,6 @@ 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;
|
||||||
@ -35,7 +33,6 @@ 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(() => {
|
||||||
@ -153,12 +150,6 @@ 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,94 +0,0 @@
|
|||||||
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]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export function useSyllables() {
|
|
||||||
const [isHighlighted, setIsHighlighted] = useState(false);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isHighlighted,
|
|
||||||
toggleHighlight: () => setIsHighlighted(!isHighlighted)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
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']);
|
|
||||||
});
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
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];
|
|
||||||
}
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
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,17 +3,12 @@ import { ArrowLeft, Sparkles } from 'lucide-react';
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { StoryGenerator } from '../../components/story/StoryGenerator';
|
import { StoryGenerator } from '../../components/story/StoryGenerator';
|
||||||
import { useSession } from '../../hooks/useSession';
|
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() {
|
export function CreateStoryPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
@ -44,31 +39,16 @@ export function CreateStoryPage() {
|
|||||||
<Sparkles className="h-6 w-6 text-purple-600" />
|
<Sparkles className="h-6 w-6 text-purple-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<AdaptiveTitle
|
<h1 className="text-2xl font-bold text-gray-900">Criar Nova História</h1>
|
||||||
text="Criar Nova História"
|
<p className="text-gray-600">
|
||||||
isUpperCase={isUpperCase}
|
Vamos criar uma história personalizada baseada nos seus interesses
|
||||||
className="text-2xl font-bold text-gray-900"
|
</p>
|
||||||
/>
|
|
||||||
<AdaptiveParagraph
|
|
||||||
text="Vamos criar uma história personalizada baseada nos seus interesses"
|
|
||||||
isUpperCase={isUpperCase}
|
|
||||||
className="text-gray-600"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TextCaseToggle
|
|
||||||
isUpperCase={isUpperCase}
|
|
||||||
onToggle={toggleUppercase}
|
|
||||||
isLoading={isLoading}
|
|
||||||
className="ml-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{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">
|
||||||
<AdaptiveText
|
{error}
|
||||||
text={error}
|
|
||||||
isUpperCase={isUpperCase}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -79,19 +59,10 @@ export function CreateStoryPage() {
|
|||||||
Como funciona?
|
Como funciona?
|
||||||
</h3>
|
</h3>
|
||||||
<ol className="text-sm text-purple-700 space-y-2">
|
<ol className="text-sm text-purple-700 space-y-2">
|
||||||
{[
|
<li>1. Conte-nos sobre seus interesses e preferências</li>
|
||||||
'Conte-nos sobre seus interesses e preferências',
|
<li>2. Escolha personagens e cenários para sua história</li>
|
||||||
'Escolha personagens e cenários para sua história',
|
<li>3. Nossa IA criará uma história única e personalizada</li>
|
||||||
'Nossa IA criará uma história única e personalizada',
|
<li>4. Você poderá ler e praticar com sua nova história</li>
|
||||||
'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>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,10 +5,6 @@ import { WordFormation } from '../../components/exercises/WordFormation';
|
|||||||
import { SentenceCompletion } from '../../components/exercises/SentenceCompletion';
|
import { SentenceCompletion } from '../../components/exercises/SentenceCompletion';
|
||||||
import { PronunciationPractice } from '../../components/exercises/PronunciationPractice';
|
import { PronunciationPractice } from '../../components/exercises/PronunciationPractice';
|
||||||
import { ArrowLeft } from 'lucide-react';
|
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 {
|
interface ExerciseWord {
|
||||||
word: string;
|
word: string;
|
||||||
@ -35,8 +31,6 @@ export function ExercisePage() {
|
|||||||
const [exerciseData, setExerciseData] = React.useState<any>(null);
|
const [exerciseData, setExerciseData] = React.useState<any>(null);
|
||||||
const [exerciseWords, setExerciseWords] = React.useState<ExerciseWord[]>([]);
|
const [exerciseWords, setExerciseWords] = React.useState<ExerciseWord[]>([]);
|
||||||
const [error, setError] = React.useState<string | null>(null);
|
const [error, setError] = React.useState<string | null>(null);
|
||||||
const { session } = useSession();
|
|
||||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const loadExerciseData = async () => {
|
const loadExerciseData = async () => {
|
||||||
@ -110,11 +104,7 @@ 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">
|
||||||
<AdaptiveText
|
<p className="text-red-600 mb-4">{error}</p>
|
||||||
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"
|
||||||
@ -146,9 +136,7 @@ export function ExercisePage() {
|
|||||||
case 'word-formation':
|
case 'word-formation':
|
||||||
return (
|
return (
|
||||||
<WordFormation
|
<WordFormation
|
||||||
words={exerciseWords.map(w =>
|
words={exerciseWords.map(w => w.word)}
|
||||||
isUpperCase ? w.word.toUpperCase() : w.word
|
|
||||||
)}
|
|
||||||
storyId={storyId as string}
|
storyId={storyId as string}
|
||||||
studentId={exerciseData.story.student_id}
|
studentId={exerciseData.story.student_id}
|
||||||
/>
|
/>
|
||||||
@ -182,24 +170,8 @@ export function ExercisePage() {
|
|||||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-4"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
<AdaptiveText
|
Voltar para história
|
||||||
text="Voltar para história"
|
|
||||||
isUpperCase={isUpperCase}
|
|
||||||
/>
|
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Exercício */}
|
{/* Exercício */}
|
||||||
|
|||||||
@ -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, TextSelect } from 'lucide-react';
|
import { ArrowLeft, ArrowRight, Volume2, Share2, ChevronDown, ChevronUp, Loader2, Pause, Play, Download, RefreshCw, Trash2 } 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';
|
||||||
@ -8,12 +8,6 @@ import { StoryMetrics } from '../../components/story/StoryMetrics';
|
|||||||
import { convertWebmToMp3 } from '../../utils/audioConverter';
|
import { convertWebmToMp3 } from '../../utils/audioConverter';
|
||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { ExerciseSuggestions } from '../../components/learning/ExerciseSuggestions';
|
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 {
|
interface StoryRecording {
|
||||||
id: string;
|
id: string;
|
||||||
@ -42,9 +36,6 @@ function RecordingHistoryCard({ recording }: { recording: StoryRecording }) {
|
|||||||
const [isConverting, setIsConverting] = React.useState(false);
|
const [isConverting, setIsConverting] = React.useState(false);
|
||||||
const [mp3Url, setMp3Url] = React.useState<string | null>(null);
|
const [mp3Url, setMp3Url] = React.useState<string | null>(null);
|
||||||
const [conversionError, setConversionError] = 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
|
// Verificar suporte ao formato WebM
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@ -293,12 +284,9 @@ 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">
|
||||||
<AdaptiveText
|
<p className="text-sm text-gray-600 whitespace-pre-wrap">
|
||||||
text={recording.transcription || 'Transcrição não disponível'}
|
{recording.transcription || 'Transcrição não disponível'}
|
||||||
isUpperCase={isUpperCase}
|
</p>
|
||||||
className="text-sm text-gray-600 whitespace-pre-wrap"
|
|
||||||
highlightSyllables={isHighlighted}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -397,10 +385,6 @@ export function StoryPage() {
|
|||||||
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
|
const [loadingRecordings, setLoadingRecordings] = React.useState(true);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
const { session } = useSession();
|
|
||||||
const { isUpperCase, toggleUppercase, isLoading } = useUppercasePreference(session?.user?.id);
|
|
||||||
const { isHighlighted, toggleHighlight } = useSyllables();
|
|
||||||
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchStory = async () => {
|
const fetchStory = async () => {
|
||||||
@ -618,24 +602,10 @@ export function StoryPage() {
|
|||||||
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
|
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
|
||||||
>
|
>
|
||||||
<ArrowLeft className="h-5 w-5" />
|
<ArrowLeft className="h-5 w-5" />
|
||||||
<AdaptiveText text="Voltar para histórias" isUpperCase={isUpperCase} />
|
Voltar para histórias
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<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
|
<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"
|
||||||
@ -665,19 +635,12 @@ export function StoryPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-8">
|
<div className="p-8">
|
||||||
<AdaptiveTitle
|
<h1 className="text-3xl font-bold text-gray-900 mb-6">{story?.title}</h1>
|
||||||
text={story?.title || ''}
|
|
||||||
isUpperCase={isUpperCase}
|
|
||||||
className="text-3xl font-bold text-gray-900 mb-6"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Texto da página atual */}
|
{/* Texto da página atual */}
|
||||||
<AdaptiveParagraph
|
<p className="text-xl leading-relaxed text-gray-700 mb-8">
|
||||||
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
{story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||||
isUpperCase={isUpperCase}
|
</p>
|
||||||
highlightSyllables={isHighlighted}
|
|
||||||
className="text-xl leading-relaxed text-gray-700 mb-8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Gravador de áudio */}
|
{/* Gravador de áudio */}
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
|
|||||||
@ -11,21 +11,16 @@ 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();
|
||||||
@ -69,7 +64,7 @@ export function StudentDashboardLayout() {
|
|||||||
{!isCollapsed && <span>Minhas Histórias</span>}
|
{!isCollapsed && <span>Minhas Histórias</span>}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
|
||||||
{/* <NavLink
|
<NavLink
|
||||||
to="/aluno/conquistas"
|
to="/aluno/conquistas"
|
||||||
onClick={handleNavigation}
|
onClick={handleNavigation}
|
||||||
className={({ isActive }) =>
|
className={({ isActive }) =>
|
||||||
@ -128,7 +123,7 @@ export function StudentDashboardLayout() {
|
|||||||
<Trophy className="h-5 w-5" />
|
<Trophy className="h-5 w-5" />
|
||||||
{!isCollapsed && <span>Progresso Fônico</span>}
|
{!isCollapsed && <span>Progresso Fônico</span>}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
*/}
|
|
||||||
<NavLink
|
<NavLink
|
||||||
to="/aluno/configuracoes"
|
to="/aluno/configuracoes"
|
||||||
onClick={handleNavigation}
|
onClick={handleNavigation}
|
||||||
|
|||||||
@ -77,7 +77,7 @@ export function StudentDashboardPage() {
|
|||||||
.eq('student_id', session.user.id)
|
.eq('student_id', session.user.id)
|
||||||
.eq('story_pages.page_number', 1) // Garante que pegamos a primeira página
|
.eq('story_pages.page_number', 1) // Garante que pegamos a primeira página
|
||||||
.order('created_at', { ascending: false })
|
.order('created_at', { ascending: false })
|
||||||
.limit(6);
|
.limit(3);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setRecentStories(data || []);
|
setRecentStories(data || []);
|
||||||
@ -252,7 +252,10 @@ export function StudentDashboardPage() {
|
|||||||
{story.cover && (
|
{story.cover && (
|
||||||
<div className="relative aspect-video">
|
<div className="relative aspect-video">
|
||||||
<img
|
<img
|
||||||
src={`${story.cover.image_url}?width=400&height=300&quality=80&format=webp`}
|
src={supabase.storage
|
||||||
|
.from('story-images')
|
||||||
|
.getPublicUrl(story.cover.image_url).data.publicUrl +
|
||||||
|
`?width=400&height=300&quality=80&format=webp`}
|
||||||
alt={story.title}
|
alt={story.title}
|
||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@ -25,14 +25,8 @@ export function StudentStoriesPage() {
|
|||||||
|
|
||||||
const query = supabase
|
const query = supabase
|
||||||
.from('stories')
|
.from('stories')
|
||||||
.select(`
|
.select('*')
|
||||||
*,
|
.eq('student_id', session.user.id);
|
||||||
pages:story_pages (
|
|
||||||
image_url
|
|
||||||
)
|
|
||||||
`)
|
|
||||||
.eq('student_id', session.user.id)
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (statusFilter !== 'all') {
|
if (statusFilter !== 'all') {
|
||||||
query.eq('status', statusFilter);
|
query.eq('status', statusFilter);
|
||||||
@ -203,21 +197,16 @@ export function StudentStoriesPage() {
|
|||||||
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden cursor-pointer hover:shadow-md transition"
|
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}`)}
|
onClick={() => navigate(`/aluno/historias/${story.id}`)}
|
||||||
>
|
>
|
||||||
{!story.cover?.image_url && (
|
{story.cover && (
|
||||||
<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">
|
<div className="relative aspect-video">
|
||||||
<img
|
<img
|
||||||
src={story.cover.image_url}
|
src={supabase.storage
|
||||||
alt={`Capa da história: ${story.title}`}
|
.from('story-images')
|
||||||
|
.getPublicUrl(story.cover.image_url).data.publicUrl +
|
||||||
|
`?width=400&height=300&quality=80&format=webp`}
|
||||||
|
alt={story.title}
|
||||||
className="w-full h-48 object-cover"
|
className="w-full h-48 object-cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={(e) => {
|
|
||||||
e.currentTarget.style.display = 'none';
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user