mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-18 06:17:56 +00:00
feat: Adiciona toggle de texto maiúsculo para apoio à alfabetização
- Implementa componente TextCaseToggle para alternância de caixa - Cria sistema de texto adaptativo com componentes AdaptiveText - Adiciona hook useUppercasePreference para gerenciar estado - Integra funcionalidade em todas as páginas principais - Persiste preferência do usuário no banco de dados
This commit is contained in:
parent
7880ce8dda
commit
e4c225ebd7
18
CHANGELOG.md
18
CHANGELOG.md
@ -54,3 +54,21 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/).
|
|||||||
|
|
||||||
### Removido
|
### Removido
|
||||||
- N/A (primeira versão)
|
- N/A (primeira versão)
|
||||||
|
|
||||||
|
## [1.1.0] - 2024-05-20
|
||||||
|
|
||||||
|
### Adicionado
|
||||||
|
- Novo componente `TextCaseToggle` para alternar entre maiúsculas/minúsculas
|
||||||
|
- Componentes de texto adaptativo (`AdaptiveText`, `AdaptiveTitle`, `AdaptiveParagraph`)
|
||||||
|
- Hook `useUppercasePreference` para gerenciar preferência do usuário
|
||||||
|
- Suporte a texto em maiúsculas para crianças em fase de alfabetização
|
||||||
|
|
||||||
|
### Modificado
|
||||||
|
- Páginas de histórias e exercícios para usar o novo sistema de texto
|
||||||
|
- Cabeçalho das páginas principal com controle de caixa de texto
|
||||||
|
- Componentes de exercícios para suportar transformação de texto
|
||||||
|
|
||||||
|
### Técnico
|
||||||
|
- Adicionada coluna `uppercase_text_preferences` na tabela students
|
||||||
|
- Sistema de persistência de preferências via Supabase
|
||||||
|
- Otimizações de performance com memoização de componentes
|
||||||
|
|||||||
@ -3,12 +3,17 @@ 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">
|
||||||
@ -39,11 +44,23 @@ 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>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Criar Nova História</h1>
|
<AdaptiveTitle
|
||||||
<p className="text-gray-600">
|
text="Criar Nova História"
|
||||||
Vamos criar uma história personalizada baseada nos seus interesses
|
isUpperCase={isUpperCase}
|
||||||
</p>
|
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>
|
</div>
|
||||||
|
<TextCaseToggle
|
||||||
|
isUpperCase={isUpperCase}
|
||||||
|
onToggle={toggleUppercase}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="ml-auto"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@ -59,10 +76,19 @@ 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>
|
{[
|
||||||
<li>2. Escolha personagens e cenários para sua história</li>
|
'Conte-nos sobre seus interesses e preferências',
|
||||||
<li>3. Nossa IA criará uma história única e personalizada</li>
|
'Escolha personagens e cenários para sua história',
|
||||||
<li>4. Você poderá ler e praticar com sua nova história</li>
|
'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>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,6 +5,10 @@ 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;
|
||||||
@ -31,6 +35,8 @@ 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 () => {
|
||||||
@ -136,7 +142,9 @@ export function ExercisePage() {
|
|||||||
case 'word-formation':
|
case 'word-formation':
|
||||||
return (
|
return (
|
||||||
<WordFormation
|
<WordFormation
|
||||||
words={exerciseWords.map(w => w.word)}
|
words={exerciseWords.map(w =>
|
||||||
|
isUpperCase ? w.word.toUpperCase() : w.word
|
||||||
|
)}
|
||||||
storyId={storyId as string}
|
storyId={storyId as string}
|
||||||
studentId={exerciseData.story.student_id}
|
studentId={exerciseData.story.student_id}
|
||||||
/>
|
/>
|
||||||
@ -170,8 +178,24 @@ 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" />
|
||||||
Voltar para história
|
<AdaptiveText
|
||||||
|
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 */}
|
||||||
|
|||||||
@ -8,6 +8,10 @@ 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 { useUppercasePreference } from '../../hooks/useUppercasePreference';
|
||||||
|
import { useSession } from '../../hooks/useSession';
|
||||||
|
|
||||||
interface StoryRecording {
|
interface StoryRecording {
|
||||||
id: string;
|
id: string;
|
||||||
@ -385,6 +389,8 @@ 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);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const fetchStory = async () => {
|
const fetchStory = async () => {
|
||||||
@ -602,10 +608,15 @@ 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" />
|
||||||
Voltar para histórias
|
<AdaptiveText text="Voltar para histórias" isUpperCase={isUpperCase} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
<TextCaseToggle
|
||||||
|
isUpperCase={isUpperCase}
|
||||||
|
onToggle={toggleUppercase}
|
||||||
|
isLoading={isLoading}
|
||||||
|
/>
|
||||||
<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"
|
||||||
@ -635,12 +646,18 @@ export function StoryPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="p-8">
|
<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 */}
|
{/* Texto da página atual */}
|
||||||
<p className="text-xl leading-relaxed text-gray-700 mb-8">
|
<AdaptiveParagraph
|
||||||
{story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
text={story?.content?.pages?.[currentPage]?.text || 'Carregando...'}
|
||||||
</p>
|
isUpperCase={isUpperCase}
|
||||||
|
className="text-xl leading-relaxed text-gray-700 mb-8"
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Gravador de áudio */}
|
{/* Gravador de áudio */}
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
|
|||||||
@ -64,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 }) =>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user