mirror of
https://github.com/lucasrcsantana/story-generator.git
synced 2025-12-17 05:47:52 +00:00
- Adiciona tipagem para cover na interface Story - Atualiza queries para usar story_pages como capa - Usa página 1 como capa padrão das histórias - Otimiza carregamento de imagens com parâmetros
230 lines
8.6 KiB
TypeScript
230 lines
8.6 KiB
TypeScript
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 && (
|
|
<div className="relative aspect-video">
|
|
<img
|
|
src={story.content.pages[0].image}
|
|
alt={story.title}
|
|
className="w-full h-48 object-cover"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
)}
|
|
<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>
|
|
);
|
|
}
|