'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 { useRouter } from 'next/navigation'; 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: (id: string, formData: FormData) => Promise<{ success: boolean; error?: string; data?: unknown } | void>; appwriteConfig: { endpoint: string; projectId: string; databaseId: string; }; globalSettings?: Record; entity: string; } type SortKey = 'fecha_publicacion' | '$createdAt' | 'titulo' | 'estado'; // ───────────────────────────────────────────── // COMPONENT // ───────────────────────────────────────────── export const PublicationsList: React.FC = ({ initialPosts, user, relationData, submitEditAction, appwriteConfig, globalSettings, entity, }) => { const router = useRouter(); const toast = useToast(); const [isPending, startTransition] = useTransition(); // Local state const [posts, setPosts] = useState(initialPosts); const [statusFilter, setStatusFilter] = useState('all'); const [sortBy, setSortBy] = useState('fecha_publicacion'); const canEdit = user?.$id === 'admin-emergency' || user?.labels?.includes('admin') || user?.labels?.includes('editor'); const userRole = canEdit ? 'admin' : 'viewer'; // ── DERIVED STATE ──────────────────────────── const filteredAndSortedPosts = useMemo(() => { let result = [...posts]; // 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); if (sortBy === 'estado') return (a.estado || '').localeCompare(b.estado || ''); const aDate = new Date(a[sortBy] || a.fecha_publicacion || a.$createdAt || 0).getTime(); const bDate = new Date(b[sortBy] || b.fecha_publicacion || b.$createdAt || 0).getTime(); return bDate - aDate; // Strictly Newest first }); return result; }, [posts, 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((post: Post) => { router.push(`/dashboard/${entity}/${post.$id}`); }, [entity, router]); 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]); // ── 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 */}
{/* Status Filter */}
{/* Sort */} {/* New post button */} Nueva publicación
{/* Grid de publicaciones */} {isPending && (
Procesando…
)} {filteredAndSortedPosts.length > 0 ? (
{filteredAndSortedPosts.map((post) => { const enrichedPost = { ...post }; if (typeof enrichedPost.imagen_destacada_id === 'string' && relationData['imagen_destacada_id']) { const mediaDoc = relationData['imagen_destacada_id'].find(m => m.$id === enrichedPost.imagen_destacada_id); if (mediaDoc) enrichedPost.imagen_destacada_id = mediaDoc as any; } if (typeof enrichedPost.tipo_publicacion_id === 'string' && relationData['tipo_publicacion_id']) { const typeDoc = relationData['tipo_publicacion_id'].find(t => t.$id === enrichedPost.tipo_publicacion_id); if (typeDoc) enrichedPost.tipo_publicacion_id = typeDoc as any; } return ( ); })}
) : (

Sin resultados

{'No se encontraron publicaciones. ¡Intenta con otra búsqueda o crea una nueva!'}

Crear publicación
)}
); }; export default PublicationsList;