feat: add publication filters and sorting options
This commit is contained in:
parent
e35b8f55cf
commit
3c169eab97
@ -15,13 +15,14 @@ export default async function EntityPage({
|
||||
searchParams,
|
||||
}: {
|
||||
params: Promise<{ entity: string }>;
|
||||
searchParams: Promise<{ q?: string; page?: string; sort?: string; dir?: string }>;
|
||||
searchParams: Promise<{ q?: string; page?: string; sort?: string; dir?: string; year?: string; month?: string; status?: string }>;
|
||||
}) {
|
||||
const { entity } = await params;
|
||||
const sParams = await searchParams;
|
||||
const q = sParams.q || '';
|
||||
const page = Number(sParams.page) || 1;
|
||||
const sort = sParams.sort || '$createdAt';
|
||||
const defaultSort = entity === 'posts' ? 'fecha_publicacion' : '$createdAt';
|
||||
const sort = sParams.sort || defaultSort;
|
||||
const dir: 'asc' | 'desc' = sParams.dir === 'asc' ? 'asc' : 'desc';
|
||||
const limit: number = entity === 'posts' ? 12 : 15;
|
||||
const offset = (page - 1) * limit;
|
||||
@ -73,46 +74,81 @@ export default async function EntityPage({
|
||||
}
|
||||
}
|
||||
|
||||
if (entity === 'posts') {
|
||||
const status = sParams.status || '';
|
||||
if (status && status !== 'all') {
|
||||
if (status === 'publicado') {
|
||||
queries.push(Query.equal('estado', ['publicado', 'Publicado']));
|
||||
} else if (status === 'borrador') {
|
||||
queries.push(Query.equal('estado', ['borrador', 'Borrador']));
|
||||
} else if (status === 'pendiente') {
|
||||
queries.push(Query.equal('estado', ['pendiente', 'Pendiente']));
|
||||
} else if (status === 'archivado') {
|
||||
queries.push(Query.equal('estado', ['archivado', 'Archivado']));
|
||||
} else {
|
||||
queries.push(Query.equal('estado', status));
|
||||
}
|
||||
}
|
||||
|
||||
const year = sParams.year ? Number(sParams.year) : null;
|
||||
const month = sParams.month ? Number(sParams.month) : null;
|
||||
|
||||
if (year || month) {
|
||||
const filterYear = year || new Date().getFullYear();
|
||||
if (month) {
|
||||
const startDate = new Date(Date.UTC(filterYear, month - 1, 1, 0, 0, 0)).toISOString();
|
||||
const endDate = new Date(Date.UTC(filterYear, month, 1, 0, 0, 0)).toISOString();
|
||||
queries.push(Query.greaterThanEqual('fecha_publicacion', startDate));
|
||||
queries.push(Query.lessThan('fecha_publicacion', endDate));
|
||||
} else {
|
||||
const startDate = new Date(Date.UTC(filterYear, 0, 1, 0, 0, 0)).toISOString();
|
||||
const endDate = new Date(Date.UTC(filterYear + 1, 0, 1, 0, 0, 0)).toISOString();
|
||||
queries.push(Query.greaterThanEqual('fecha_publicacion', startDate));
|
||||
queries.push(Query.lessThan('fecha_publicacion', endDate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await client.databases.listDocuments(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
collectionId,
|
||||
queries
|
||||
);
|
||||
|
||||
|
||||
documents = JSON.parse(JSON.stringify(response.documents));
|
||||
total = response.total;
|
||||
|
||||
const fields = SCHEMA_MAP[entity] || [];
|
||||
await Promise.all(
|
||||
fields
|
||||
.filter((f) => f.type === 'relation' && f.relationCollection)
|
||||
.map(async (field) => {
|
||||
const relKey = field.relationCollection as string;
|
||||
const relColId = collectionMap[relKey];
|
||||
if (!relColId) return;
|
||||
await Promise.all(
|
||||
fields
|
||||
.filter((f) => f.type === 'relation' && f.relationCollection)
|
||||
.map(async (field) => {
|
||||
const relKey = field.relationCollection as string;
|
||||
const relColId = collectionMap[relKey];
|
||||
if (!relColId) return;
|
||||
|
||||
const ids = Array.from(new Set(
|
||||
response.documents
|
||||
.map(d => typeof d[field.name] === 'object' && d[field.name] !== null ? d[field.name].$id : d[field.name])
|
||||
.filter(id => typeof id === 'string' && id.length > 0) as string[]
|
||||
));
|
||||
const ids = Array.from(new Set(
|
||||
response.documents
|
||||
.map(d => typeof d[field.name] === 'object' && d[field.name] !== null ? d[field.name].$id : d[field.name])
|
||||
.filter(id => typeof id === 'string' && id.length > 0) as string[]
|
||||
));
|
||||
|
||||
if (ids.length === 0) {
|
||||
relationData[field.name] = [];
|
||||
return;
|
||||
}
|
||||
if (ids.length === 0) {
|
||||
relationData[field.name] = [];
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const relRes = await client.databases.listDocuments(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
relColId,
|
||||
[Query.equal('$id', ids), Query.limit(100)]
|
||||
);
|
||||
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
|
||||
} catch { }
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const relRes = await client.databases.listDocuments(
|
||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
relColId,
|
||||
[Query.equal('$id', ids), Query.limit(100)]
|
||||
);
|
||||
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
|
||||
} catch { }
|
||||
})
|
||||
);
|
||||
|
||||
// Enrich documents with resolved relation data for DataTable
|
||||
documents = documents.map(doc => {
|
||||
const enriched = { ...doc };
|
||||
@ -137,6 +173,18 @@ export default async function EntityPage({
|
||||
return updateEntityAction(entity, id, formData);
|
||||
};
|
||||
|
||||
const buildQueryString = (p: number) => {
|
||||
const paramsObj = new URLSearchParams();
|
||||
paramsObj.set('page', p.toString());
|
||||
if (q) paramsObj.set('q', q);
|
||||
if (sort) paramsObj.set('sort', sort);
|
||||
if (dir) paramsObj.set('dir', dir);
|
||||
if (sParams.year) paramsObj.set('year', sParams.year);
|
||||
if (sParams.month) paramsObj.set('month', sParams.month);
|
||||
if (sParams.status) paramsObj.set('status', sParams.status);
|
||||
return paramsObj.toString();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in pb-10">
|
||||
<div className="flex flex-col md:flex-row justify-between items-center bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5 gap-6">
|
||||
@ -149,7 +197,7 @@ export default async function EntityPage({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form className="flex-1 max-w-md w-full relative group">
|
||||
<form className="flex-1 max-w-md w-full relative group" method="GET" action={`/dashboard/${entity}`}>
|
||||
<input
|
||||
type="text"
|
||||
name="q"
|
||||
@ -157,8 +205,13 @@ export default async function EntityPage({
|
||||
placeholder={`Buscar en ${entityName}...`}
|
||||
className="w-full bg-slate-950 border border-slate-800 rounded-full px-6 py-3 text-slate-200 focus:outline-none focus:border-cyan-500 transition-all font-medium placeholder:text-slate-600"
|
||||
/>
|
||||
{sort && <input type="hidden" name="sort" value={sort} />}
|
||||
{dir && <input type="hidden" name="dir" value={dir} />}
|
||||
{sParams.year && <input type="hidden" name="year" value={sParams.year} />}
|
||||
{sParams.month && <input type="hidden" name="month" value={sParams.month} />}
|
||||
{sParams.status && <input type="hidden" name="status" value={sParams.status} />}
|
||||
<button type="submit" className="absolute right-2 top-2 bg-slate-800 p-1.5 rounded-full hover:bg-slate-700 transition-colors text-slate-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@ -173,7 +226,7 @@ export default async function EntityPage({
|
||||
{entity === 'posts' ? (
|
||||
<ErrorBoundary>
|
||||
<PublicationsList
|
||||
key={`list-${entity}-${page}-${q}-${sort}-${dir}`}
|
||||
key={`list-${entity}-${page}-${q}-${sort}-${dir}-${sParams.year || ''}-${sParams.month || ''}-${sParams.status || ''}`}
|
||||
initialPosts={documents as unknown as Post[]}
|
||||
user={user as AuthUser}
|
||||
relationData={relationData}
|
||||
@ -184,6 +237,11 @@ export default async function EntityPage({
|
||||
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||
}}
|
||||
entity={entity}
|
||||
currentSort={sort}
|
||||
currentDir={dir}
|
||||
currentYear={sParams.year || ''}
|
||||
currentMonth={sParams.month || ''}
|
||||
currentStatus={sParams.status || 'all'}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
) : (
|
||||
@ -204,31 +262,31 @@ export default async function EntityPage({
|
||||
return (
|
||||
<div className="flex justify-center items-center gap-2 mt-8 flex-wrap">
|
||||
<Link
|
||||
href={`/dashboard/${entity}?page=1${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
||||
href={`/dashboard/${entity}?${buildQueryString(1)}`}
|
||||
className={`px-4 py-2 rounded-xl font-bold text-xs transition-all border border-slate-800 ${page <= 1 ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
||||
title="Ir a la primera página"
|
||||
>
|
||||
« INICIO
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/${entity}?page=${page - 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
||||
href={`/dashboard/${entity}?${buildQueryString(page - 1)}`}
|
||||
className={`px-5 py-2 rounded-xl font-bold text-sm transition-all border border-slate-800 ${page <= 1 ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
||||
>
|
||||
ANTERIOR
|
||||
</Link>
|
||||
|
||||
|
||||
<span className="text-slate-400 font-bold tracking-widest text-[10px] md:text-xs uppercase text-center px-2 md:px-4 bg-slate-900 py-2 rounded-xl border border-slate-800">
|
||||
Página {page} de {totalPages}
|
||||
</span>
|
||||
|
||||
|
||||
<Link
|
||||
href={`/dashboard/${entity}?page=${page + 1}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
||||
href={`/dashboard/${entity}?${buildQueryString(page + 1)}`}
|
||||
className={`px-5 py-2 rounded-xl font-bold text-sm transition-all border border-slate-800 ${page >= totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
||||
>
|
||||
SIGUIENTE
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/${entity}?page=${totalPages}${q ? `&q=${q}` : ''}${sort ? `&sort=${sort}` : ''}${dir ? `&dir=${dir}` : ''}`}
|
||||
href={`/dashboard/${entity}?${buildQueryString(totalPages)}`}
|
||||
className={`px-4 py-2 rounded-xl font-bold text-xs transition-all border border-slate-800 ${page >= totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
||||
title="Ir a la última página"
|
||||
>
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
import React, { useState, useCallback, useMemo, useTransition } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Search, Plus, Filter, Loader2, FileText } from 'lucide-react';
|
||||
import { Search, Plus, Filter, Loader2, FileText, Calendar, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import type { Post, AuthUser, UserRole } from '@/types/publications';
|
||||
import PostCard from './PostCard';
|
||||
@ -43,10 +43,13 @@ interface PublicationsListProps {
|
||||
};
|
||||
globalSettings?: Record<string, any>;
|
||||
entity: string;
|
||||
currentSort?: string;
|
||||
currentDir?: 'asc' | 'desc';
|
||||
currentYear?: string;
|
||||
currentMonth?: string;
|
||||
currentStatus?: string;
|
||||
}
|
||||
|
||||
type SortKey = 'fecha_publicacion' | '$createdAt' | 'titulo' | 'estado';
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// COMPONENT
|
||||
// ─────────────────────────────────────────────
|
||||
@ -59,6 +62,11 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
||||
appwriteConfig,
|
||||
globalSettings,
|
||||
entity,
|
||||
currentSort = 'fecha_publicacion',
|
||||
currentDir = 'desc',
|
||||
currentYear = '',
|
||||
currentMonth = '',
|
||||
currentStatus = 'all',
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const toast = useToast();
|
||||
@ -67,40 +75,79 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
||||
// Local state
|
||||
const [posts, setPosts] = useState<Post[]>(initialPosts);
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all');
|
||||
const [sortBy, setSortBy] = useState<SortKey>('fecha_publicacion');
|
||||
|
||||
const canEdit = user?.$id === 'admin-emergency' || user?.labels?.includes('admin') || user?.labels?.includes('editor');
|
||||
const userRole = canEdit ? 'admin' : 'viewer';
|
||||
|
||||
// ── DERIVED STATE ────────────────────────────
|
||||
// ── FILTER & SORT ACTIONS ─────────────────────
|
||||
const updateFilters = useCallback((updates: {
|
||||
sort?: string;
|
||||
dir?: 'asc' | 'desc';
|
||||
year?: string;
|
||||
month?: string;
|
||||
status?: string;
|
||||
page?: number;
|
||||
}) => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
// Default page to 1 on filter changes
|
||||
params.set('page', (updates.page ?? 1).toString());
|
||||
|
||||
const filteredAndSortedPosts = useMemo(() => {
|
||||
let result = [...posts];
|
||||
|
||||
|
||||
// Filter by status
|
||||
if (statusFilter !== 'all') {
|
||||
result = result.filter((p) => p.estado === statusFilter);
|
||||
if (updates.sort !== undefined) {
|
||||
if (updates.sort) params.set('sort', updates.sort);
|
||||
else params.delete('sort');
|
||||
}
|
||||
if (updates.dir !== undefined) {
|
||||
if (updates.dir) params.set('dir', updates.dir);
|
||||
else params.delete('dir');
|
||||
}
|
||||
if (updates.year !== undefined) {
|
||||
if (updates.year) params.set('year', updates.year);
|
||||
else params.delete('year');
|
||||
}
|
||||
if (updates.month !== undefined) {
|
||||
if (updates.month) params.set('month', updates.month);
|
||||
else params.delete('month');
|
||||
}
|
||||
if (updates.status !== undefined) {
|
||||
if (updates.status && updates.status !== 'all') params.set('status', updates.status);
|
||||
else params.delete('status');
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
router.push(`/dashboard/${entity}?${params.toString()}`);
|
||||
}, [entity, router]);
|
||||
|
||||
return result;
|
||||
}, [posts, statusFilter, sortBy]);
|
||||
const years = useMemo(() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const startYear = 2024;
|
||||
const list = [];
|
||||
for (let y = currentYear; y >= startYear; y--) {
|
||||
list.push(y);
|
||||
}
|
||||
return list;
|
||||
}, []);
|
||||
|
||||
const months = useMemo(() => [
|
||||
{ value: '1', label: 'Enero' },
|
||||
{ value: '2', label: 'Febrero' },
|
||||
{ value: '3', label: 'Marzo' },
|
||||
{ value: '4', label: 'Abril' },
|
||||
{ value: '5', label: 'Mayo' },
|
||||
{ value: '6', label: 'Junio' },
|
||||
{ value: '7', label: 'Julio' },
|
||||
{ value: '8', label: 'Agosto' },
|
||||
{ value: '9', label: 'Septiembre' },
|
||||
{ value: '10', label: 'Octubre' },
|
||||
{ value: '11', label: 'Noviembre' },
|
||||
{ value: '12', label: 'Diciembre' },
|
||||
], []);
|
||||
|
||||
const filteredAndSortedPosts = posts;
|
||||
|
||||
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,
|
||||
published: posts.filter((p) => (p.estado || '').toLowerCase() === 'publicado').length,
|
||||
draft: posts.filter((p) => (p.estado || '').toLowerCase() === 'borrador').length,
|
||||
pending: posts.filter((p) => (p.estado || '').toLowerCase() === 'pendiente').length,
|
||||
}), [posts]);
|
||||
|
||||
// ── HANDLERS ────────────────────────────────
|
||||
@ -153,50 +200,113 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Toolbar */}
|
||||
<div className="flex flex-col md:flex-row gap-3 justify-end">
|
||||
<div className="glass-panel rounded-2xl p-4 flex flex-col xl:flex-row gap-4 justify-between items-center backdrop-blur-md">
|
||||
<div className="flex flex-wrap items-center gap-3 w-full xl:w-auto">
|
||||
{/* Status Filter */}
|
||||
<div className="flex items-center gap-2 bg-slate-950/50 border border-white/5 rounded-xl px-3 py-1.5 focus-within:border-cyan-500/50 transition-all">
|
||||
<Filter size={14} className="text-slate-400" />
|
||||
<select
|
||||
value={currentStatus}
|
||||
onChange={(e) => updateFilters({ status: e.target.value })}
|
||||
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||
>
|
||||
<option value="all" className="bg-slate-900 text-white">Todos los estados</option>
|
||||
<option value="publicado" className="bg-slate-900 text-white">Publicados</option>
|
||||
<option value="borrador" className="bg-slate-900 text-white">Borradores</option>
|
||||
<option value="pendiente" className="bg-slate-900 text-white">Pendientes</option>
|
||||
<option value="archivado" className="bg-slate-900 text-white">Archivados</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Year Filter */}
|
||||
<div className="flex items-center gap-2 bg-slate-950/50 border border-white/5 rounded-xl px-3 py-1.5 focus-within:border-cyan-500/50 transition-all">
|
||||
<Calendar size={14} className="text-slate-400" />
|
||||
<select
|
||||
value={currentYear}
|
||||
onChange={(e) => updateFilters({ year: e.target.value, month: e.target.value ? currentMonth : '' })}
|
||||
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||
>
|
||||
<option value="" className="bg-slate-900 text-white">Todos los años</option>
|
||||
{years.map(y => (
|
||||
<option key={y} value={y.toString()} className="bg-slate-900 text-white">{y}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Filter size={14} className="text-slate-500" />
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="bg-slate-900/60 border border-white/8 rounded-xl px-3 py-2.5 text-white text-sm
|
||||
focus:border-cyan-500 focus:outline-none transition-all"
|
||||
>
|
||||
<option value="all">Todos los estados</option>
|
||||
<option value="publicado">Publicados</option>
|
||||
<option value="borrador">Borradores</option>
|
||||
<option value="pendiente">Pendientes</option>
|
||||
<option value="archivado">Archivados</option>
|
||||
</select>
|
||||
{/* Month Filter */}
|
||||
<div className="flex items-center gap-2 bg-slate-950/50 border border-white/5 rounded-xl px-3 py-1.5 focus-within:border-cyan-500/50 transition-all">
|
||||
<Calendar size={14} className="text-slate-400" />
|
||||
<select
|
||||
value={currentMonth}
|
||||
onChange={(e) => updateFilters({ month: e.target.value })}
|
||||
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||
>
|
||||
<option value="" className="bg-slate-900 text-white">Todos los meses</option>
|
||||
{months.map(m => (
|
||||
<option key={m.value} value={m.value} className="bg-slate-900 text-white">{m.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as SortKey)}
|
||||
className="bg-slate-900/60 border border-white/8 rounded-xl px-3 py-2.5 text-white text-sm
|
||||
focus:border-cyan-500 focus:outline-none transition-all"
|
||||
>
|
||||
<option value="fecha_publicacion">Más recientes</option>
|
||||
<option value="titulo">Título A-Z</option>
|
||||
<option value="estado">Estado</option>
|
||||
</select>
|
||||
<div className="flex flex-wrap items-center gap-3 w-full xl:w-auto justify-end">
|
||||
{/* Sort By Field */}
|
||||
<div className="flex items-center gap-2 bg-slate-950/50 border border-white/5 rounded-xl px-3 py-1.5 focus-within:border-cyan-500/50 transition-all">
|
||||
<ArrowUpDown size={14} className="text-slate-400" />
|
||||
<select
|
||||
value={currentSort}
|
||||
onChange={(e) => updateFilters({ sort: e.target.value })}
|
||||
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||
>
|
||||
<option value="fecha_publicacion" className="bg-slate-900 text-white">Fecha de Publicación</option>
|
||||
<option value="$createdAt" className="bg-slate-900 text-white">Fecha de Creación</option>
|
||||
<option value="titulo" className="bg-slate-900 text-white">Título</option>
|
||||
<option value="estado" className="bg-slate-900 text-white">Estado</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* New post button */}
|
||||
<PermissionGuard allowedRoles={['editor', 'admin']} userRole={userRole}>
|
||||
<Link
|
||||
href={`/dashboard/${entity}/new`}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-cyan-500 hover:bg-cyan-400
|
||||
text-slate-950 font-black text-sm rounded-xl transition-all
|
||||
shadow-[0_0_20px_rgba(6,182,212,0.25)] whitespace-nowrap"
|
||||
{/* Sort Direction Toggle Button */}
|
||||
<button
|
||||
onClick={() => updateFilters({ dir: currentDir === 'asc' ? 'desc' : 'asc' })}
|
||||
className="flex items-center gap-2 bg-slate-950/50 border border-white/5 hover:border-cyan-500/50 text-white text-sm px-4 py-2.5 rounded-xl transition-all active:scale-95 cursor-pointer"
|
||||
title={currentDir === 'asc' ? 'Orden Ascendente' : 'Orden Descendente'}
|
||||
>
|
||||
<Plus size={16} />
|
||||
Nueva publicación
|
||||
</Link>
|
||||
</PermissionGuard>
|
||||
{currentDir === 'asc' ? (
|
||||
<>
|
||||
<ArrowUp size={14} className="text-cyan-400" />
|
||||
<span>Ascendente</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ArrowDown size={14} className="text-cyan-400" />
|
||||
<span>Descendente</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* Clear Filters Button (only show if any filter is active) */}
|
||||
{(currentStatus !== 'all' || currentYear || currentMonth) && (
|
||||
<button
|
||||
onClick={() => updateFilters({ status: 'all', year: '', month: '', sort: 'fecha_publicacion', dir: 'desc' })}
|
||||
className="text-xs text-rose-400 hover:text-rose-300 font-bold px-3 py-2 border border-rose-500/20 hover:border-rose-500/40 rounded-xl transition-all cursor-pointer"
|
||||
>
|
||||
Limpiar filtros
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* New post button */}
|
||||
<PermissionGuard allowedRoles={['editor', 'admin']} userRole={userRole}>
|
||||
<Link
|
||||
href={`/dashboard/${entity}/new`}
|
||||
className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-cyan-500 to-blue-600 hover:from-cyan-400 hover:to-blue-500
|
||||
text-white font-bold text-sm rounded-xl transition-all
|
||||
shadow-[0_0_20px_rgba(6,182,212,0.25)] hover:shadow-[0_0_30px_rgba(6,182,212,0.4)] hover:scale-105 active:scale-95 whitespace-nowrap"
|
||||
>
|
||||
<Plus size={16} />
|
||||
Nueva publicación
|
||||
</Link>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid de publicaciones */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user