mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
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:
parent
fd50d59d3c
commit
70953ab57a
10
src/components/layout/RootLayout.tsx
Normal file
10
src/components/layout/RootLayout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useRef } from 'react';
|
import React, { useState, useRef } from 'react';
|
||||||
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
import { Mic, Square, Loader, Play, Upload } from 'lucide-react';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
|
|
||||||
interface AudioRecorderProps {
|
interface AudioRecorderProps {
|
||||||
storyId: string;
|
storyId: string;
|
||||||
|
|||||||
@ -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 { supabase } from '../../lib/supabase';
|
||||||
|
import { Story } from '../../types/database';
|
||||||
|
import { AudioRecorder } from '../../components/story/AudioRecorder';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
export function StoryPage() {
|
interface StoryPageProps {
|
||||||
// ... outros códigos ...
|
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) => {
|
const handleAudioUploaded = async (audioUrl: string) => {
|
||||||
try {
|
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
|
const { error } = await supabase
|
||||||
.from('story_recordings')
|
.from('story_recordings')
|
||||||
.insert({
|
.insert({
|
||||||
story_id: storyId,
|
story_id: storyId,
|
||||||
student_id: studentId,
|
student_id: session.user.id,
|
||||||
audio_url: audioUrl,
|
audio_url: audioUrl,
|
||||||
status: 'pending_analysis' // será analisado pela IA posteriormente
|
status: 'pending_analysis'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) throw error;
|
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) {
|
} catch (err) {
|
||||||
console.error('Erro ao salvar gravação:', err);
|
console.error('Erro ao salvar gravação:', err);
|
||||||
|
setError('Erro ao salvar gravação de áudio');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (loading) {
|
||||||
<div>
|
return (
|
||||||
{/* ... outros elementos da página ... */}
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<Loader2 className="w-8 h-8 text-purple-600 animate-spin" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
<AudioRecorder
|
if (error || !story) {
|
||||||
storyId={storyId}
|
return (
|
||||||
studentId={studentId}
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
onAudioUploaded={handleAudioUploaded}
|
<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 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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { createBrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter } from 'react-router-dom';
|
||||||
|
import { RootLayout } from '../components/layout/RootLayout';
|
||||||
import { HomePage } from '../components/home/HomePage';
|
import { HomePage } from '../components/home/HomePage';
|
||||||
import { LoginForm } from '../components/auth/LoginForm';
|
import { LoginForm } from '../components/auth/LoginForm';
|
||||||
import { SchoolRegistrationForm } from '../components/auth/SchoolRegistrationForm';
|
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 { StudentsPage } from '../pages/dashboard/students/StudentsPage';
|
||||||
import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage';
|
import { AddStudentPage } from '../pages/dashboard/students/AddStudentPage';
|
||||||
import { DemoPage } from '../pages/demo/DemoPage';
|
import { DemoPage } from '../pages/demo/DemoPage';
|
||||||
|
import { StoryPage } from '../pages/story/StoryPage';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const router = createBrowserRouter([
|
export const router = createBrowserRouter([
|
||||||
@ -27,7 +29,7 @@ export const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'demo',
|
path: 'demo',
|
||||||
element: <DemoPage />,
|
element: <StoryPage demo={true} />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
@ -117,6 +119,10 @@ export const router = createBrowserRouter([
|
|||||||
path: '/auth/callback',
|
path: '/auth/callback',
|
||||||
element: <AuthCallback />
|
element: <AuthCallback />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/story/:storyId',
|
||||||
|
element: <StoryPage />
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -94,6 +94,8 @@ export interface StoryPage {
|
|||||||
export interface Story {
|
export interface Story {
|
||||||
id: string;
|
id: string;
|
||||||
student_id: string;
|
student_id: string;
|
||||||
|
class_id: string;
|
||||||
|
school_id: string;
|
||||||
title: string;
|
title: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
content: {
|
content: {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user