feat: implementa páginas do dashboard do aluno

- Adiciona página de listagem de histórias com filtros e ordenação
- Cria formulário de criação de novas histórias com temas
- Implementa visualizador de história com navegação entre páginas
- Integra gravador de áudio para leitura
- Adiciona funcionalidade de compartilhamento
- Implementa estados de loading e tratamento de erros
This commit is contained in:
Lucas Santana 2024-12-20 11:11:28 -03:00
parent f1a7cd8730
commit 5952d83ec8
6 changed files with 1012 additions and 0 deletions

View File

@ -0,0 +1,185 @@
import React from 'react';
import { ArrowLeft, Sparkles, Send } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
interface StoryForm {
title: string;
theme: string;
prompt: string;
}
export function CreateStoryPage() {
const navigate = useNavigate();
const [formData, setFormData] = React.useState<StoryForm>({
title: '',
theme: '',
prompt: ''
});
const [generating, setGenerating] = React.useState(false);
const [error, setError] = React.useState<string | null>(null);
const themes = [
{ id: 'nature', name: 'Natureza e Meio Ambiente' },
{ id: 'culture', name: 'Cultura Brasileira' },
{ id: 'science', name: 'Ciência e Descobertas' },
{ id: 'adventure', name: 'Aventura e Exploração' },
{ id: 'friendship', name: 'Amizade e Cooperação' }
];
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setGenerating(true);
setError(null);
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) throw new Error('Usuário não autenticado');
// Em produção: Integrar com API de IA para gerar história
const generatedStory = {
title: formData.title,
content: {
pages: [
{
text: "Era uma vez...",
image: "https://images.unsplash.com/photo-1472162072942-cd5147eb3902"
}
]
},
theme: formData.theme,
status: 'draft'
};
const { error: saveError } = await supabase
.from('stories')
.insert({
student_id: session.user.id,
title: generatedStory.title,
theme: generatedStory.theme,
content: generatedStory.content,
status: generatedStory.status,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
if (saveError) throw saveError;
navigate('/aluno/historias');
} catch (err) {
console.error('Erro ao criar história:', err);
setError('Não foi possível criar sua história. Tente novamente.');
} finally {
setGenerating(false);
}
};
return (
<div>
<button
onClick={() => navigate('/aluno/historias')}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
>
<ArrowLeft className="h-5 w-5" />
Voltar para histórias
</button>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-6">Criar Nova História</h1>
{error && (
<div className="mb-6 p-4 bg-red-50 text-red-600 rounded-lg">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-6 max-w-2xl">
<div>
<label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
Título da História
</label>
<input
type="text"
id="title"
name="title"
value={formData.title}
onChange={handleChange}
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
required
placeholder="Ex: A Aventura na Floresta Encantada"
/>
</div>
<div>
<label htmlFor="theme" className="block text-sm font-medium text-gray-700 mb-1">
Tema
</label>
<select
id="theme"
name="theme"
value={formData.theme}
onChange={handleChange}
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
required
>
<option value="">Selecione um tema</option>
{themes.map(theme => (
<option key={theme.id} value={theme.id}>
{theme.name}
</option>
))}
</select>
</div>
<div>
<label htmlFor="prompt" className="block text-sm font-medium text-gray-700 mb-1">
Sobre o que você quer escrever?
</label>
<textarea
id="prompt"
name="prompt"
value={formData.prompt}
onChange={handleChange}
rows={4}
className="w-full rounded-lg border-gray-300 focus:ring-purple-500 focus:border-purple-500"
required
placeholder="Descreva sua ideia para a história..."
/>
</div>
<div className="bg-purple-50 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<Sparkles className="h-5 w-5 text-purple-600" />
</div>
<div>
<h3 className="text-sm font-medium text-purple-900">
Assistente de Criação
</h3>
<p className="text-sm text-purple-700">
Nossa IA vai ajudar você a criar uma história incrível baseada nas suas ideias!
</p>
</div>
</div>
</div>
<div className="flex justify-end pt-6">
<button
type="submit"
disabled={generating}
className="flex items-center gap-2 px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition disabled:opacity-50"
>
<Send className="h-5 w-5" />
{generating ? 'Criando História...' : 'Criar História'}
</button>
</div>
</form>
</div>
</div>
);
}

View File

@ -0,0 +1,165 @@
import React from 'react';
import { ArrowLeft, ArrowRight, Mic, Volume2, Share2, Save } from 'lucide-react';
import { useParams, useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import { AudioRecorder } from '../../components/story/AudioRecorder';
import type { Story } from '../../types/database';
export function StoryPage() {
const { id } = useParams();
const navigate = useNavigate();
const [story, setStory] = React.useState<Story | null>(null);
const [currentPage, setCurrentPage] = React.useState(0);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [isPlaying, setIsPlaying] = React.useState(false);
React.useEffect(() => {
const fetchStory = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) return;
const { data, error } = await supabase
.from('stories')
.select('*')
.eq('id', id)
.single();
if (error) throw error;
setStory(data);
} catch (err) {
console.error('Erro ao carregar história:', err);
setError('Não foi possível carregar a história');
} finally {
setLoading(false);
}
};
fetchStory();
}, [id]);
const handleShare = async () => {
if (navigator.share) {
try {
await navigator.share({
title: story?.title,
text: 'Confira minha história!',
url: window.location.href
});
} catch (err) {
console.error('Erro ao compartilhar:', err);
}
}
};
if (loading) {
return (
<div className="animate-pulse">
<div className="h-96 bg-gray-200 rounded-xl mb-8" />
<div className="h-20 bg-gray-200 rounded-xl" />
</div>
);
}
if (error || !story) {
return (
<div className="text-center py-12">
<div className="text-red-500 mb-4">{error}</div>
<button
onClick={() => navigate('/aluno/historias')}
className="text-purple-600 hover:text-purple-700"
>
Voltar para histórias
</button>
</div>
);
}
return (
<div>
<div className="flex justify-between items-center mb-6">
<button
onClick={() => navigate('/aluno/historias')}
className="flex items-center gap-2 text-gray-600 hover:text-gray-900"
>
<ArrowLeft className="h-5 w-5" />
Voltar para histórias
</button>
<div className="flex items-center gap-4">
<button
onClick={handleShare}
className="flex items-center gap-2 px-4 py-2 text-gray-600 hover:text-gray-900"
>
<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>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{/* Imagem da página atual */}
{story.content.pages[currentPage].image && (
<div className="relative aspect-video">
<img
src={story.content.pages[currentPage].image}
alt={`Página ${currentPage + 1}`}
className="w-full h-full object-cover"
/>
</div>
)}
<div className="p-6">
<h1 className="text-2xl font-bold text-gray-900 mb-4">{story.title}</h1>
{/* Texto da página atual */}
<p className="text-lg text-gray-700 mb-8">
{story.content.pages[currentPage].text}
</p>
{/* Gravador de áudio */}
<AudioRecorder
storyId={story.id}
studentId={story.student_id}
onAudioUploaded={(audioUrl) => {
console.log('Áudio gravado:', audioUrl);
}}
/>
{/* Navegação entre páginas */}
<div className="flex justify-between items-center mt-8 pt-6 border-t border-gray-200">
<button
onClick={() => setCurrentPage(prev => Math.max(0, 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"
>
<ArrowLeft className="h-5 w-5" />
Anterior
</button>
<span className="text-sm text-gray-500">
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"
>
Próxima
<ArrowRight className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,142 @@
import React from 'react';
import { Outlet, NavLink, useNavigate } from 'react-router-dom';
import {
LayoutDashboard,
BookOpen,
Settings,
LogOut,
School,
Trophy,
History
} from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
export function StudentDashboardLayout() {
const navigate = useNavigate();
const { signOut } = useAuth();
const handleLogout = async () => {
await signOut();
navigate('/');
};
return (
<div className="min-h-screen bg-gray-50">
{/* Sidebar */}
<aside className="fixed left-0 top-0 h-full w-64 bg-white border-r border-gray-200">
<div className="flex items-center gap-2 p-6 border-b border-gray-200">
<School className="h-8 w-8 text-purple-600" />
<span className="font-semibold text-gray-900">Histórias Mágicas</span>
</div>
<nav className="p-4 space-y-1">
<NavLink
to="/aluno"
end
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<LayoutDashboard className="h-5 w-5" />
Início
</NavLink>
<NavLink
to="/aluno/historias"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<BookOpen className="h-5 w-5" />
Minhas Histórias
</NavLink>
<NavLink
to="/aluno/conquistas"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<Trophy className="h-5 w-5" />
Conquistas
</NavLink>
<NavLink
to="/aluno/historico"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<History className="h-5 w-5" />
Histórico
</NavLink>
<NavLink
to="/aluno/configuracoes"
className={({ isActive }) =>
`flex items-center gap-2 px-4 py-2 rounded-lg text-sm ${
isActive
? 'bg-purple-50 text-purple-700'
: 'text-gray-600 hover:bg-gray-50'
}`
}
>
<Settings className="h-5 w-5" />
Configurações
</NavLink>
<button
onClick={handleLogout}
className="flex items-center gap-2 px-4 py-2 rounded-lg text-sm text-gray-600 hover:bg-gray-50 w-full mt-4"
>
<LogOut className="h-5 w-5" />
Sair
</button>
</nav>
{/* Footer com informações do aluno */}
<div className="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<span className="text-sm font-medium text-purple-600">
{/* Primeira letra do nome do aluno */}
A
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{/* Nome do aluno */}
Aluno da Silva
</p>
<p className="text-xs text-gray-500 truncate">
{/* Turma do aluno */}
5º Ano A
</p>
</div>
</div>
</div>
</aside>
{/* Main Content */}
<main className="ml-64 p-8">
<Outlet />
</main>
</div>
);
}

View File

@ -0,0 +1,261 @@
import React from 'react';
import { Plus, BookOpen, Clock, TrendingUp, Award } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import type { Story, Student } from '../../types/database';
interface DashboardMetrics {
totalStories: number;
averageReadingFluency: number;
totalReadingTime: number;
currentLevel: number;
}
export function StudentDashboardPage() {
const navigate = useNavigate();
const [student, setStudent] = React.useState<Student | null>(null);
const [stories, setStories] = React.useState<Story[]>([]);
const [metrics, setMetrics] = React.useState<DashboardMetrics>({
totalStories: 0,
averageReadingFluency: 0,
totalReadingTime: 0,
currentLevel: 1
});
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
React.useEffect(() => {
const fetchDashboardData = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) return;
// Buscar dados do aluno
const { data: studentData, error: studentError } = await supabase
.from('students')
.select(`
*,
class:classes(name, grade),
school:schools(name)
`)
.eq('id', session.user.id)
.single();
if (studentError) throw studentError;
setStudent(studentData);
// Buscar histórias do aluno
const { data: storiesData, error: storiesError } = await supabase
.from('stories')
.select('*')
.eq('student_id', session.user.id)
.order('created_at', { ascending: false })
.limit(6);
if (storiesError) throw storiesError;
setStories(storiesData || []);
// Calcular métricas
// Em produção: Implementar cálculos reais baseados nos dados
setMetrics({
totalStories: storiesData?.length || 0,
averageReadingFluency: 85, // Exemplo
totalReadingTime: 120, // Exemplo: 120 minutos
currentLevel: 3 // Exemplo
});
} catch (err) {
console.error('Erro ao carregar dashboard:', err);
setError('Não foi possível carregar seus dados');
} finally {
setLoading(false);
}
};
fetchDashboardData();
}, []);
if (loading) {
return (
<div className="animate-pulse">
<div className="h-32 bg-gray-200 rounded-xl mb-8" />
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-gray-200 rounded-xl" />
))}
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-64 bg-gray-200 rounded-xl" />
))}
</div>
</div>
);
}
if (error) {
return (
<div className="text-center py-12">
<div className="text-red-500 mb-4">{error}</div>
<button
onClick={() => window.location.reload()}
className="text-purple-600 hover:text-purple-700"
>
Tentar novamente
</button>
</div>
);
}
return (
<div>
{/* Cabeçalho */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-8">
<div className="flex justify-between items-start">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-purple-100 rounded-full flex items-center justify-center">
{student?.avatar_url ? (
<img
src={student.avatar_url}
alt={student?.name}
className="w-full h-full rounded-full object-cover"
/>
) : (
<span className="text-2xl font-bold text-purple-600">
{student?.name?.charAt(0)}
</span>
)}
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">{student?.name}</h1>
<p className="text-gray-500">
{(student?.class as any)?.grade} {(student?.class as any)?.name} {(student?.school as any)?.name}
</p>
</div>
</div>
<button
onClick={() => navigate('/aluno/historias/nova')}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Nova História
</button>
</div>
</div>
{/* Métricas */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-purple-100 rounded-lg">
<BookOpen className="h-6 w-6 text-purple-600" />
</div>
<div>
<p className="text-sm text-gray-500">Total de Histórias</p>
<p className="text-2xl font-bold text-gray-900">{metrics.totalStories}</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-green-100 rounded-lg">
<TrendingUp className="h-6 w-6 text-green-600" />
</div>
<div>
<p className="text-sm text-gray-500">Fluência Média</p>
<p className="text-2xl font-bold text-gray-900">{metrics.averageReadingFluency}%</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<Clock className="h-6 w-6 text-blue-600" />
</div>
<div>
<p className="text-sm text-gray-500">Tempo de Leitura</p>
<p className="text-2xl font-bold text-gray-900">{metrics.totalReadingTime}min</p>
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div className="flex items-center gap-4">
<div className="p-3 bg-yellow-100 rounded-lg">
<Award className="h-6 w-6 text-yellow-600" />
</div>
<div>
<p className="text-sm text-gray-500">Nível Atual</p>
<p className="text-2xl font-bold text-gray-900">{metrics.currentLevel}</p>
</div>
</div>
</div>
</div>
{/* Histórias Recentes */}
<div>
<div className="flex justify-between items-center mb-6">
<h2 className="text-xl font-bold text-gray-900">Histórias Recentes</h2>
<button
onClick={() => navigate('/aluno/historias')}
className="text-purple-600 hover:text-purple-700"
>
Ver todas
</button>
</div>
{stories.length === 0 ? (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
<BookOpen className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Nenhuma história ainda
</h3>
<p className="text-gray-500 mb-6">
Comece sua jornada criando sua primeira história!
</p>
<button
onClick={() => navigate('/aluno/historias/nova')}
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Criar História
</button>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{stories.map((story) => (
<div
key={story.id}
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}`)}
>
{story.content.pages[0].image && (
<img
src={story.content.pages[0].image}
alt={story.title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h3 className="font-medium text-gray-900 mb-2">{story.title}</h3>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{new Date(story.created_at).toLocaleDateString()}</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
story.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{story.status === 'published' ? 'Publicada' : 'Rascunho'}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,227 @@
import React from 'react';
import { Plus, Search, Filter, BookOpen, ArrowUpDown } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { supabase } from '../../lib/supabase';
import type { Story } from '../../types/database';
type StoryStatus = 'all' | 'draft' | 'published';
type SortOption = 'recent' | 'oldest' | 'title' | 'performance';
export function StudentStoriesPage() {
const navigate = useNavigate();
const [stories, setStories] = React.useState<Story[]>([]);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState<string | null>(null);
const [searchTerm, setSearchTerm] = React.useState('');
const [statusFilter, setStatusFilter] = React.useState<StoryStatus>('all');
const [sortBy, setSortBy] = React.useState<SortOption>('recent');
const [showFilters, setShowFilters] = React.useState(false);
React.useEffect(() => {
const fetchStories = async () => {
try {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.user?.id) return;
const query = supabase
.from('stories')
.select('*')
.eq('student_id', session.user.id);
if (statusFilter !== 'all') {
query.eq('status', statusFilter);
}
let { data, error } = await query;
if (error) throw error;
// Aplicar ordenação
const sortedData = (data || []).sort((a, b) => {
switch (sortBy) {
case 'oldest':
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
case 'title':
return a.title.localeCompare(b.title);
case 'performance':
return (b.performance_score || 0) - (a.performance_score || 0);
default: // recent
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
}
});
setStories(sortedData);
} catch (err) {
console.error('Erro ao buscar histórias:', err);
setError('Não foi possível carregar suas histórias');
} finally {
setLoading(false);
}
};
fetchStories();
}, [statusFilter, sortBy]);
const filteredStories = stories.filter(story =>
story.title.toLowerCase().includes(searchTerm.toLowerCase())
);
if (loading) {
return (
<div className="animate-pulse">
<div className="h-20 bg-gray-200 rounded-xl mb-6" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="h-64 bg-gray-200 rounded-xl" />
))}
</div>
</div>
);
}
return (
<div>
<div className="flex justify-between items-center mb-6">
<h1 className="text-2xl font-bold text-gray-900">Minhas Histórias</h1>
<button
onClick={() => navigate('/aluno/historias/nova')}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Nova História
</button>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden mb-6">
<div className="p-4 border-b border-gray-200">
<div className="flex flex-col md:flex-row gap-4">
{/* Busca */}
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar histórias..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
/>
</div>
{/* Filtros e Ordenação */}
<div className="flex gap-2">
<button
onClick={() => setShowFilters(!showFilters)}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50"
>
<Filter className="h-5 w-5" />
Filtros
</button>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as SortOption)}
className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-purple-500 focus:border-purple-500"
>
<option value="recent">Mais recentes</option>
<option value="oldest">Mais antigas</option>
<option value="title">Por título</option>
<option value="performance">Melhor desempenho</option>
</select>
</div>
</div>
{/* Painel de Filtros */}
{showFilters && (
<div className="mt-4 pt-4 border-t border-gray-200">
<div className="flex gap-2">
<button
onClick={() => setStatusFilter('all')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'all'
? 'bg-purple-100 text-purple-700'
: 'hover:bg-gray-100'
}`}
>
Todas
</button>
<button
onClick={() => setStatusFilter('published')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'published'
? 'bg-purple-100 text-purple-700'
: 'hover:bg-gray-100'
}`}
>
Publicadas
</button>
<button
onClick={() => setStatusFilter('draft')}
className={`px-4 py-2 rounded-lg ${
statusFilter === 'draft'
? 'bg-purple-100 text-purple-700'
: 'hover:bg-gray-100'
}`}
>
Rascunhos
</button>
</div>
</div>
)}
</div>
{/* Lista de Histórias */}
{filteredStories.length === 0 ? (
<div className="p-12 text-center">
<BookOpen className="h-12 w-12 text-gray-400 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">
Nenhuma história encontrada
</h3>
<p className="text-gray-500 mb-6">
{searchTerm
? 'Tente usar outros termos na busca'
: 'Comece criando sua primeira história!'}
</p>
{!searchTerm && (
<button
onClick={() => navigate('/aluno/historias/nova')}
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition"
>
<Plus className="h-5 w-5" />
Criar História
</button>
)}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 p-6">
{filteredStories.map((story) => (
<div
key={story.id}
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}`)}
>
{story.content.pages[0].image && (
<img
src={story.content.pages[0].image}
alt={story.title}
className="w-full h-48 object-cover"
/>
)}
<div className="p-6">
<h3 className="font-medium text-gray-900 mb-2">{story.title}</h3>
<div className="flex items-center justify-between text-sm text-gray-500">
<span>{new Date(story.created_at).toLocaleDateString()}</span>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
story.status === 'published'
? 'bg-green-100 text-green-800'
: 'bg-yellow-100 text-yellow-800'
}`}>
{story.status === 'published' ? 'Publicada' : 'Rascunho'}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@ -14,6 +14,11 @@ import { InviteTeacherPage } from './pages/dashboard/teachers/InviteTeacherPage'
import { StudentsPage } from './pages/dashboard/students/StudentsPage';
import { AddStudentPage } from './pages/dashboard/students/AddStudentPage';
import { SettingsPage } from './pages/dashboard/settings/SettingsPage';
import { StudentDashboardPage } from './pages/student-dashboard/StudentDashboardPage';
import { StudentDashboardLayout } from './pages/student-dashboard/StudentDashboardLayout';
import { StudentStoriesPage } from './pages/student-dashboard/StudentStoriesPage';
import { CreateStoryPage } from './pages/student-dashboard/CreateStoryPage';
import { StoryPage } from './pages/student-dashboard/StoryPage';
export const router = createBrowserRouter([
{
@ -116,4 +121,31 @@ export const router = createBrowserRouter([
path: '/auth/callback',
element: <AuthCallback />
},
{
path: '/aluno',
element: <StudentDashboardLayout />,
children: [
{
index: true,
element: <StudentDashboardPage />,
},
{
path: 'historias',
children: [
{
index: true,
element: <StudentStoriesPage />,
},
{
path: 'nova',
element: <CreateStoryPage />,
},
{
path: ':id',
element: <StoryPage />,
}
]
}
]
}
]);