'use client'; /** * @file PublicationsList.tsx * @description Optimized, RBAC-aware publications list for the dashboard. * * Architecture: * - Uses React.memo on PostCard to avoid re-renders on unrelated state changes * - useCallback for all event handlers * - useMemo for expensive derived state (filtered/sorted posts) * - Integrates PublicationDrawer for inline editing * - Integrates useToast for user feedback * - Wrapped by ErrorBoundary at usage site * * RBAC: * - Editor/Admin: can open drawer to edit posts on click * - Admin only: can delete posts * - Viewer: read-only list */ import React, { useState, useCallback, useMemo, useTransition } from 'react'; import { Search, Plus, Filter, Loader2, FileText } from 'lucide-react'; import Link from 'next/link'; import type { Post, AuthUser, UserRole } from '@/types/publications'; import PostCard from './PostCard'; import PermissionGuard from './PermissionGuard'; import { useToast } from './ToastProvider'; // ───────────────────────────────────────────── // TYPES // ───────────────────────────────────────────── interface PublicationsListProps { initialPosts: Post[]; user: AuthUser; relationData: Record>; submitEditAction: (formData: FormData) => Promise<{ success: boolean; error?: string; data?: unknown } | void>; appwriteConfig: { endpoint: string; projectId: string; databaseId: string; }; entity: string; } type SortKey = 'fecha_publicacion' | '$createdAt' | 'titulo' | 'estado'; // ───────────────────────────────────────────── // COMPONENT // ───────────────────────────────────────────── export const PublicationsList: React.FC = ({ initialPosts, user, relationData, submitEditAction, appwriteConfig, entity, }) => { const toast = useToast(); const [isPending, startTransition] = useTransition(); // Local state const [posts, setPosts] = useState(initialPosts); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [sortBy, setSortBy] = useState('fecha_publicacion'); const [drawerPost, setDrawerPost] = useState(null); // Lazy-load Drawer to avoid shipping it unless needed const [DrawerComponent, setDrawerComponent] = useState | null>(null); const userRole = (user?.prefs?.role as UserRole) ?? 'viewer'; // ── DERIVED STATE ──────────────────────────── const filteredAndSortedPosts = useMemo(() => { let result = [...posts]; // Filter by search if (searchQuery.trim()) { const q = searchQuery.toLowerCase(); result = result.filter( (p) => p.titulo.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q) || (p.resumen_redes || '').toLowerCase().includes(q) ); } // Filter by status if (statusFilter !== 'all') { result = result.filter((p) => p.estado === statusFilter); } // Sort result.sort((a, b) => { if (sortBy === 'titulo') return a.titulo.localeCompare(b.titulo); const aDate = new Date(a[sortBy] || a.$createdAt).getTime(); const bDate = new Date(b[sortBy] || b.$createdAt).getTime(); return bDate - aDate; // Newest first }); return result; }, [posts, searchQuery, statusFilter, sortBy]); const stats = useMemo(() => ({ total: posts.length, published: posts.filter((p) => p.estado === 'Publicado').length, draft: posts.filter((p) => p.estado === 'Borrador').length, pending: posts.filter((p) => p.estado === 'Pendiente').length, }), [posts]); // ── HANDLERS ──────────────────────────────── const handleEdit = useCallback(async (post: Post) => { // Lazy-load the drawer component if (!DrawerComponent) { const mod = await import('./PublicationDrawer'); setDrawerComponent(() => mod.PublicationDrawer); } setDrawerPost(post); }, [DrawerComponent]); const handleCloseDrawer = useCallback(() => { setDrawerPost(null); }, []); const handleDelete = useCallback((postId: string) => { if (!confirm('¿Eliminar esta publicación permanentemente?')) return; startTransition(async () => { try { const res = await fetch(`/api/publications?id=${postId}`, { method: 'DELETE' }); const json = await res.json() as { success: boolean; error?: string }; if (json.success) { // Optimistic UI: remove from local state immediately setPosts((prev) => prev.filter((p) => p.$id !== postId)); toast.success('Publicación eliminada correctamente.'); } else { toast.error(json.error || 'Error al eliminar la publicación.'); } } catch { toast.error('Error de red. Intenta de nuevo.'); } }); }, [toast]); const handleEditSuccess = useCallback(() => { setDrawerPost(null); toast.success('Publicación actualizada correctamente.'); // Note: For a full refresh of the list, the parent Server Component should // call revalidatePath. Here we rely on Next.js router refresh. window.location.reload(); }, [toast]); // ── RENDER ────────────────────────────────── return (
{/* Stats Row */}
{[ { label: 'Total', value: stats.total, color: 'text-white' }, { label: 'Publicados', value: stats.published, color: 'text-emerald-400' }, { label: 'Borradores', value: stats.draft, color: 'text-slate-400' }, { label: 'Pendientes', value: stats.pending, color: 'text-amber-400' }, ].map((s) => (

{s.label}

{s.value}

))}
{/* Toolbar */}
{/* Search */}
setSearchQuery(e.target.value)} className="w-full bg-slate-900/60 border border-white/8 rounded-xl pl-10 pr-4 py-2.5 text-white text-sm focus:border-cyan-500 focus:outline-none focus:ring-1 focus:ring-cyan-500/30 transition-all" />
{/* Status Filter */}
{/* Sort */} {/* New post button */} Nueva publicación
{/* Grid de publicaciones */} {isPending && (
Procesando…
)} {filteredAndSortedPosts.length > 0 ? (
{filteredAndSortedPosts.map((post) => ( ))}
) : (

Sin resultados

{searchQuery ? 'No hay publicaciones que coincidan con tu búsqueda.' : 'Aún no hay publicaciones. ¡Crea la primera!'}

Crear publicación
)} {/* SideDrawer — lazy loaded */} {DrawerComponent && ( )}
); }; export default PublicationsList;