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,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
params: Promise<{ entity: string }>;
|
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 { entity } = await params;
|
||||||
const sParams = await searchParams;
|
const sParams = await searchParams;
|
||||||
const q = sParams.q || '';
|
const q = sParams.q || '';
|
||||||
const page = Number(sParams.page) || 1;
|
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 dir: 'asc' | 'desc' = sParams.dir === 'asc' ? 'asc' : 'desc';
|
||||||
const limit: number = entity === 'posts' ? 12 : 15;
|
const limit: number = entity === 'posts' ? 12 : 15;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
@ -73,6 +74,41 @@ 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(
|
const response = await client.databases.listDocuments(
|
||||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||||
collectionId,
|
collectionId,
|
||||||
@ -137,6 +173,18 @@ export default async function EntityPage({
|
|||||||
return updateEntityAction(entity, id, formData);
|
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 (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in pb-10">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
@ -157,8 +205,13 @@ export default async function EntityPage({
|
|||||||
placeholder={`Buscar en ${entityName}...`}
|
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"
|
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">
|
<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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@ -173,7 +226,7 @@ export default async function EntityPage({
|
|||||||
{entity === 'posts' ? (
|
{entity === 'posts' ? (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<PublicationsList
|
<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[]}
|
initialPosts={documents as unknown as Post[]}
|
||||||
user={user as AuthUser}
|
user={user as AuthUser}
|
||||||
relationData={relationData}
|
relationData={relationData}
|
||||||
@ -184,6 +237,11 @@ export default async function EntityPage({
|
|||||||
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
|
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||||
}}
|
}}
|
||||||
entity={entity}
|
entity={entity}
|
||||||
|
currentSort={sort}
|
||||||
|
currentDir={dir}
|
||||||
|
currentYear={sParams.year || ''}
|
||||||
|
currentMonth={sParams.month || ''}
|
||||||
|
currentStatus={sParams.status || 'all'}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
) : (
|
) : (
|
||||||
@ -204,14 +262,14 @@ export default async function EntityPage({
|
|||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center gap-2 mt-8 flex-wrap">
|
<div className="flex justify-center items-center gap-2 mt-8 flex-wrap">
|
||||||
<Link
|
<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'}`}
|
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"
|
title="Ir a la primera página"
|
||||||
>
|
>
|
||||||
« INICIO
|
« INICIO
|
||||||
</Link>
|
</Link>
|
||||||
<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'}`}
|
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
|
ANTERIOR
|
||||||
@ -222,13 +280,13 @@ export default async function EntityPage({
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<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 >= totalPages ? 'opacity-30 pointer-events-none bg-slate-900' : 'bg-slate-900 text-white hover:border-cyan-500'}`}
|
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
|
SIGUIENTE
|
||||||
</Link>
|
</Link>
|
||||||
<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'}`}
|
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"
|
title="Ir a la última página"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
import React, { useState, useCallback, useMemo, useTransition } from 'react';
|
import React, { useState, useCallback, useMemo, useTransition } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
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 Link from 'next/link';
|
||||||
import type { Post, AuthUser, UserRole } from '@/types/publications';
|
import type { Post, AuthUser, UserRole } from '@/types/publications';
|
||||||
import PostCard from './PostCard';
|
import PostCard from './PostCard';
|
||||||
@ -43,10 +43,13 @@ interface PublicationsListProps {
|
|||||||
};
|
};
|
||||||
globalSettings?: Record<string, any>;
|
globalSettings?: Record<string, any>;
|
||||||
entity: string;
|
entity: string;
|
||||||
|
currentSort?: string;
|
||||||
|
currentDir?: 'asc' | 'desc';
|
||||||
|
currentYear?: string;
|
||||||
|
currentMonth?: string;
|
||||||
|
currentStatus?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SortKey = 'fecha_publicacion' | '$createdAt' | 'titulo' | 'estado';
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
// COMPONENT
|
// COMPONENT
|
||||||
// ─────────────────────────────────────────────
|
// ─────────────────────────────────────────────
|
||||||
@ -59,6 +62,11 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
|||||||
appwriteConfig,
|
appwriteConfig,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
entity,
|
entity,
|
||||||
|
currentSort = 'fecha_publicacion',
|
||||||
|
currentDir = 'desc',
|
||||||
|
currentYear = '',
|
||||||
|
currentMonth = '',
|
||||||
|
currentStatus = 'all',
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@ -67,40 +75,79 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
|||||||
// Local state
|
// Local state
|
||||||
const [posts, setPosts] = useState<Post[]>(initialPosts);
|
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 canEdit = user?.$id === 'admin-emergency' || user?.labels?.includes('admin') || user?.labels?.includes('editor');
|
||||||
const userRole = canEdit ? 'admin' : 'viewer';
|
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);
|
||||||
|
|
||||||
const filteredAndSortedPosts = useMemo(() => {
|
// Default page to 1 on filter changes
|
||||||
let result = [...posts];
|
params.set('page', (updates.page ?? 1).toString());
|
||||||
|
|
||||||
|
if (updates.sort !== undefined) {
|
||||||
// Filter by status
|
if (updates.sort) params.set('sort', updates.sort);
|
||||||
if (statusFilter !== 'all') {
|
else params.delete('sort');
|
||||||
result = result.filter((p) => p.estado === statusFilter);
|
}
|
||||||
|
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
|
router.push(`/dashboard/${entity}?${params.toString()}`);
|
||||||
result.sort((a, b) => {
|
}, [entity, router]);
|
||||||
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;
|
const years = useMemo(() => {
|
||||||
}, [posts, statusFilter, sortBy]);
|
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(() => ({
|
const stats = useMemo(() => ({
|
||||||
total: posts.length,
|
total: posts.length,
|
||||||
published: posts.filter((p) => p.estado === 'publicado').length,
|
published: posts.filter((p) => (p.estado || '').toLowerCase() === 'publicado').length,
|
||||||
draft: posts.filter((p) => p.estado === 'borrador').length,
|
draft: posts.filter((p) => (p.estado || '').toLowerCase() === 'borrador').length,
|
||||||
pending: posts.filter((p) => p.estado === 'pendiente').length,
|
pending: posts.filter((p) => (p.estado || '').toLowerCase() === 'pendiente').length,
|
||||||
}), [posts]);
|
}), [posts]);
|
||||||
|
|
||||||
// ── HANDLERS ────────────────────────────────
|
// ── HANDLERS ────────────────────────────────
|
||||||
@ -153,51 +200,114 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* 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 */}
|
{/* Status Filter */}
|
||||||
<div className="flex items-center gap-2">
|
<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-500" />
|
<Filter size={14} className="text-slate-400" />
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={currentStatus}
|
||||||
onChange={(e) => setStatusFilter(e.target.value)}
|
onChange={(e) => updateFilters({ status: e.target.value })}
|
||||||
className="bg-slate-900/60 border border-white/8 rounded-xl px-3 py-2.5 text-white text-sm
|
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||||
focus:border-cyan-500 focus:outline-none transition-all"
|
|
||||||
>
|
>
|
||||||
<option value="all">Todos los estados</option>
|
<option value="all" className="bg-slate-900 text-white">Todos los estados</option>
|
||||||
<option value="publicado">Publicados</option>
|
<option value="publicado" className="bg-slate-900 text-white">Publicados</option>
|
||||||
<option value="borrador">Borradores</option>
|
<option value="borrador" className="bg-slate-900 text-white">Borradores</option>
|
||||||
<option value="pendiente">Pendientes</option>
|
<option value="pendiente" className="bg-slate-900 text-white">Pendientes</option>
|
||||||
<option value="archivado">Archivados</option>
|
<option value="archivado" className="bg-slate-900 text-white">Archivados</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sort */}
|
{/* 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
|
<select
|
||||||
value={sortBy}
|
value={currentYear}
|
||||||
onChange={(e) => setSortBy(e.target.value as SortKey)}
|
onChange={(e) => updateFilters({ year: e.target.value, month: e.target.value ? currentMonth : '' })}
|
||||||
className="bg-slate-900/60 border border-white/8 rounded-xl px-3 py-2.5 text-white text-sm
|
className="bg-transparent border-0 text-white text-sm outline-none cursor-pointer pr-8"
|
||||||
focus:border-cyan-500 focus:outline-none transition-all"
|
|
||||||
>
|
>
|
||||||
<option value="fecha_publicacion">Más recientes</option>
|
<option value="" className="bg-slate-900 text-white">Todos los años</option>
|
||||||
<option value="titulo">Título A-Z</option>
|
{years.map(y => (
|
||||||
<option value="estado">Estado</option>
|
<option key={y} value={y.toString()} className="bg-slate-900 text-white">{y}</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
{/* 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'}
|
||||||
|
>
|
||||||
|
{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 */}
|
{/* New post button */}
|
||||||
<PermissionGuard allowedRoles={['editor', 'admin']} userRole={userRole}>
|
<PermissionGuard allowedRoles={['editor', 'admin']} userRole={userRole}>
|
||||||
<Link
|
<Link
|
||||||
href={`/dashboard/${entity}/new`}
|
href={`/dashboard/${entity}/new`}
|
||||||
className="flex items-center gap-2 px-5 py-2.5 bg-cyan-500 hover:bg-cyan-400
|
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-slate-950 font-black text-sm rounded-xl transition-all
|
text-white font-bold text-sm rounded-xl transition-all
|
||||||
shadow-[0_0_20px_rgba(6,182,212,0.25)] whitespace-nowrap"
|
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} />
|
<Plus size={16} />
|
||||||
Nueva publicación
|
Nueva publicación
|
||||||
</Link>
|
</Link>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Grid de publicaciones */}
|
{/* Grid de publicaciones */}
|
||||||
{isPending && (
|
{isPending && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user