feat: adiciona RootLayout e atualiza rotas da aplicação

- Cria componente RootLayout como container principal
- Atualiza router para usar RootLayout como elemento raiz
- Organiza rotas aninhadas com Outlet do React Router
- Adiciona rota para visualização de histórias individuais
This commit is contained in:
Lucas Santana 2024-12-20 10:25:21 -03:00
parent fd50d59d3c
commit 70953ab57a
5 changed files with 164 additions and 20 deletions

View File

@ -0,0 +1,10 @@
import React from 'react';
import { Outlet } from 'react-router-dom';
export function RootLayout(): JSX.Element {
return (
<div className="min-h-screen">
<Outlet />
</div>
);
}

View File

@ -1,6 +1,6 @@
import React, { useState, useRef } from 'react';
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
import { supabase } from '@/lib/supabase';
import { supabase } from '../../lib/supabase';
interface AudioRecorderProps {
storyId: string;

View File

@ -1,40 +1,166 @@
import { AudioRecorder } from '../../components/story/AudioRecorder';
import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import { Story } from '../../types/database';
import { AudioRecorder } from '../../components/story/AudioRecorder';
import { Loader2 } from 'lucide-react';
export function StoryPage() {
// ... outros códigos ...
interface StoryPageProps {
demo?: boolean;
}
export function StoryPage({ demo = false }: StoryPageProps): JSX.Element {
const { storyId } = useParams();
const [story, setStory] = useState<Story | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(0);
useEffect(() => {
const fetchStory = async () => {
try {
if (!storyId && !demo) {
throw new Error('ID da história não fornecido');
}
if (demo) {
setStory({
id: 'demo',
student_id: 'demo',
class_id: 'demo',
school_id: 'demo',
title: 'Uma Aventura Educacional',
theme: 'Demo',
content: {
pages: [
{
text: 'Bem-vindo à demonstração do Histórias Mágicas! Aqui você pode ver como funciona nossa plataforma...',
image: 'https://images.unsplash.com/photo-1472162072942-cd5147eb3902?auto=format&fit=crop&q=80&w=800&h=600',
},
{
text: 'Com histórias interativas e educativas, seus alunos aprenderão de forma divertida e envolvente...',
image: 'https://images.unsplash.com/photo-1519681393784-d120267933ba?auto=format&fit=crop&q=80&w=800&h=600',
}
],
currentPage: 0
},
status: 'published',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
setLoading(false);
return;
}
const { data, error } = await supabase
.from('stories')
.select('*')
.eq('id', storyId)
.single();
if (error) throw error;
setStory(data);
} catch (err) {
console.error('Erro ao buscar história:', err);
setError(err instanceof Error ? err.message : 'Erro ao carregar história');
} finally {
setLoading(false);
}
};
fetchStory();
}, [storyId, demo]);
const handleAudioUploaded = async (audioUrl: string) => {
try {
// Salvar referência do áudio no banco de dados
if (demo) return; // Não salva gravações no modo demo
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) throw new Error('Usuário não autenticado');
const { error } = await supabase
.from('story_recordings')
.insert({
story_id: storyId,
student_id: studentId,
student_id: session.user.id,
audio_url: audioUrl,
status: 'pending_analysis' // será analisado pela IA posteriormente
status: 'pending_analysis'
});
if (error) throw error;
// Aqui você pode adicionar a lógica para enviar o áudio para análise da IA
// Por exemplo, chamar uma função que envia o áudio para um endpoint de IA
} catch (err) {
console.error('Erro ao salvar gravação:', err);
setError('Erro ao salvar gravação de áudio');
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Loader2 className="w-8 h-8 text-purple-600 animate-spin" />
</div>
);
}
if (error || !story) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<h1 className="text-2xl font-bold text-gray-900 mb-2">Ops!</h1>
<p className="text-gray-600">{error || 'História não encontrada'}</p>
</div>
</div>
);
}
return (
<div>
{/* ... outros elementos da página ... */}
<AudioRecorder
storyId={storyId}
studentId={studentId}
onAudioUploaded={handleAudioUploaded}
/>
<div className="min-h-screen bg-gradient-to-b from-purple-50 to-white py-12">
<div className="max-w-4xl mx-auto px-4">
<h1 className="text-3xl font-bold text-gray-900 mb-8 text-center">
{story.title}
</h1>
<div className="bg-white rounded-2xl shadow-xl overflow-hidden mb-8">
{story.content.pages[currentPage].image && (
<img
src={story.content.pages[currentPage].image}
alt={`Ilustração da página ${currentPage + 1}`}
className="w-full h-64 object-cover"
/>
)}
<div className="p-8">
<p className="text-lg text-gray-700 mb-8">
{story.content.pages[currentPage].text}
</p>
<AudioRecorder
storyId={story.id}
studentId={demo ? 'demo' : story.student_id}
classId={demo ? 'demo' : story.class_id}
schoolId={demo ? 'demo' : story.school_id}
onAudioUploaded={handleAudioUploaded}
/>
<div className="flex justify-between mt-8">
<button
onClick={() => setCurrentPage(prev => Math.max(0, prev - 1))}
disabled={currentPage === 0}
className="px-4 py-2 bg-purple-100 text-purple-700 rounded-lg disabled:opacity-50"
>
Anterior
</button>
<button
onClick={() => setCurrentPage(prev => Math.min(story.content.pages.length - 1, prev + 1))}
disabled={currentPage === story.content.pages.length - 1}
className="px-4 py-2 bg-purple-600 text-white rounded-lg disabled:opacity-50"
>
Próxima
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,4 +1,5 @@
import { createBrowserRouter } from 'react-router-dom';
import { RootLayout } from '../components/layout/RootLayout';
import { HomePage } from '../components/home/HomePage';
import { LoginForm } from '../components/auth/LoginForm';
import { SchoolRegistrationForm } from '../components/auth/SchoolRegistrationForm';
@ -14,6 +15,7 @@ import { InviteTeacherPage } from '../pages/dashboard/teachers/InviteTeacherPage
import { StudentsPage } from '../pages/dashboard/students/StudentsPage';
import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage';
import { DemoPage } from '../pages/demo/DemoPage';
import { StoryPage } from '../pages/story/StoryPage';
import React from 'react';
export const router = createBrowserRouter([
@ -27,7 +29,7 @@ export const router = createBrowserRouter([
},
{
path: 'demo',
element: <DemoPage />,
element: <StoryPage demo={true} />
},
{
path: '/login',
@ -117,6 +119,10 @@ export const router = createBrowserRouter([
path: '/auth/callback',
element: <AuthCallback />
},
{
path: '/story/:storyId',
element: <StoryPage />
},
],
},
]);

View File

@ -94,6 +94,8 @@ export interface StoryPage {
export interface Story {
id: string;
student_id: string;
class_id: string;
school_id: string;
title: string;
theme: string;
content: {