feat: unify dashboard layout and implement pagination for publications
- Refactored EntityPage to share the same Header, Search, and Pagination UI across all modules. - Implemented server-side pagination for the specialized Publications view (12/page). - Added search support for Publications module. - Ensured 100% design consistency between specialized and generic views. - Fixed 4062 record display issue by implementing blocks of data fetch.
This commit is contained in:
parent
29c124580e
commit
c6cb9a0265
@ -1,5 +1,5 @@
|
|||||||
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
|
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
|
||||||
import { createAdminClient, createSessionClient, getLoggedInUser } from '@/lib/appwrite.server';
|
import { createAdminClient, getLoggedInUser } from '@/lib/appwrite.server';
|
||||||
import { Query } from 'node-appwrite';
|
import { Query } from 'node-appwrite';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { collectionMap, SCHEMA_MAP } from '@/lib/schema';
|
import { collectionMap, SCHEMA_MAP } from '@/lib/schema';
|
||||||
@ -8,107 +8,7 @@ import { getUserPreferences } from '@/lib/preferences';
|
|||||||
import { ErrorBoundary } from '@/components/publications/ErrorBoundary';
|
import { ErrorBoundary } from '@/components/publications/ErrorBoundary';
|
||||||
import { saveEntityAction } from '@/app/actions/entities';
|
import { saveEntityAction } from '@/app/actions/entities';
|
||||||
import type { Post, AuthUser } from '@/types/publications';
|
import type { Post, AuthUser } from '@/types/publications';
|
||||||
|
import { PublicationsList } from '@/components/publications/PublicationsList';
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
// POSTS‑SPECIFIC LAYOUT (card grid + drawer)
|
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the publications module with PublicationsList (cards + drawer)
|
|
||||||
* instead of the generic DataTable, when entity === 'posts'.
|
|
||||||
*/
|
|
||||||
async function PostsPage({
|
|
||||||
entity,
|
|
||||||
user,
|
|
||||||
}: {
|
|
||||||
entity: string;
|
|
||||||
user: AuthUser;
|
|
||||||
}) {
|
|
||||||
// Lazy import server-side only
|
|
||||||
const { PublicationsList } = await import('@/components/publications/PublicationsList');
|
|
||||||
|
|
||||||
const collectionId = collectionMap[entity];
|
|
||||||
let posts: Post[] = [];
|
|
||||||
let relationData: Record<string, Array<{ $id: string; [key: string]: unknown }>> = {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = await createAdminClient();
|
|
||||||
|
|
||||||
// Fetch all published + draft + pending posts
|
|
||||||
const res = await client.databases.listDocuments(
|
|
||||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
|
||||||
collectionId,
|
|
||||||
[Query.orderDesc('$createdAt'), Query.limit(100)]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Deep-clone/sanitize to avoid "Only plain objects..." Next.js error
|
|
||||||
posts = JSON.parse(JSON.stringify(res.documents)) as Post[];
|
|
||||||
|
|
||||||
// Pre-load relation data for the drawer/form
|
|
||||||
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;
|
|
||||||
try {
|
|
||||||
const relRes = await client.databases.listDocuments(
|
|
||||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
|
||||||
relColId,
|
|
||||||
[Query.limit(100)]
|
|
||||||
);
|
|
||||||
relationData[field.name] = relRes.documents as Array<{ $id: string; [key: string]: unknown }>;
|
|
||||||
} catch {
|
|
||||||
// Fail silently for individual relation fetches
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sanitize relationData too
|
|
||||||
relationData = JSON.parse(JSON.stringify(relationData));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('PostsPage fetch error:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const boundEditAction = saveEntityAction.bind(null, entity);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-6 animate-fade-in pb-10">
|
|
||||||
{/* Page Header */}
|
|
||||||
<div className="flex items-center gap-4 bg-slate-900/60 border border-white/8 p-6 rounded-2xl">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-black font-outfit uppercase tracking-tighter text-white">
|
|
||||||
Publicaciones
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm font-bold text-cyan-500 uppercase tracking-widest mt-1">
|
|
||||||
{posts.length} registros en el sistema
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ErrorBoundary>
|
|
||||||
<PublicationsList
|
|
||||||
initialPosts={posts}
|
|
||||||
user={user}
|
|
||||||
relationData={relationData}
|
|
||||||
submitEditAction={boundEditAction}
|
|
||||||
appwriteConfig={{
|
|
||||||
endpoint,
|
|
||||||
projectId,
|
|
||||||
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
|
|
||||||
}}
|
|
||||||
entity={entity}
|
|
||||||
/>
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
// GENERIC ENTITY PAGE (DataTable)
|
|
||||||
// ─────────────────────────────────────────────
|
|
||||||
|
|
||||||
export default async function EntityPage({
|
export default async function EntityPage({
|
||||||
params,
|
params,
|
||||||
@ -123,7 +23,7 @@ export default async function EntityPage({
|
|||||||
const page = Number(sParams.page) || 1;
|
const page = Number(sParams.page) || 1;
|
||||||
const sort = sParams.sort || '$createdAt';
|
const sort = sParams.sort || '$createdAt';
|
||||||
const dir = sParams.dir === 'asc' ? 'asc' : 'desc';
|
const dir = sParams.dir === 'asc' ? 'asc' : 'desc';
|
||||||
const limit = 15;
|
const limit = entity === 'posts' ? 12 : 15;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const collectionId = collectionMap[entity];
|
const collectionId = collectionMap[entity];
|
||||||
@ -151,31 +51,21 @@ export default async function EntityPage({
|
|||||||
const prefs = await getUserPreferences();
|
const prefs = await getUserPreferences();
|
||||||
const hiddenColumns = prefs.hiddenColumns?.[entity] || [];
|
const hiddenColumns = prefs.hiddenColumns?.[entity] || [];
|
||||||
|
|
||||||
// Redirect the 'posts' entity to the specialized card view
|
let documents: any[] = [];
|
||||||
if (entity === 'posts') {
|
|
||||||
return <PostsPage entity={entity} user={user as AuthUser} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generic DataTable for all other entities
|
|
||||||
let documents: Array<Record<string, unknown>> = [];
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
let relationData: Record<string, any[]> = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionClient = await createSessionClient();
|
const client = await createAdminClient();
|
||||||
if (!sessionClient) throw new Error('No sesión activa');
|
|
||||||
|
|
||||||
const queries = [Query.limit(limit), Query.offset(offset)];
|
const queries = [Query.limit(limit), Query.offset(offset)];
|
||||||
|
|
||||||
if (dir === 'asc') {
|
if (dir === 'asc') queries.push(Query.orderAsc(sort));
|
||||||
queries.push(Query.orderAsc(sort));
|
else queries.push(Query.orderDesc(sort));
|
||||||
} else {
|
|
||||||
queries.push(Query.orderDesc(sort));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (q) {
|
if (q) {
|
||||||
const searchableFields = fieldsConfig
|
const searchableFields = fieldsConfig
|
||||||
.filter((f) => f.type === 'text' || f.type === 'textarea')
|
.filter((f: any) => f.type === 'text' || f.type === 'textarea')
|
||||||
.map((f) => f.name)
|
.map((f: any) => f.name)
|
||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
|
|
||||||
if (searchableFields.length > 0) {
|
if (searchableFields.length > 0) {
|
||||||
@ -183,17 +73,40 @@ export default async function EntityPage({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await sessionClient.databases.listDocuments(
|
const response = await client.databases.listDocuments(
|
||||||
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||||
collectionId,
|
collectionId,
|
||||||
queries
|
queries
|
||||||
);
|
);
|
||||||
// Deep-clone/sanitize documents to avoid "Only plain objects..." Next.js error
|
|
||||||
documents = JSON.parse(JSON.stringify(response.documents));
|
documents = JSON.parse(JSON.stringify(response.documents));
|
||||||
total = response.total;
|
total = response.total;
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching from ${collectionId}:`, error);
|
if (entity === 'posts') {
|
||||||
|
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;
|
||||||
|
try {
|
||||||
|
const relRes = await client.databases.listDocuments(
|
||||||
|
APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||||
|
relColId,
|
||||||
|
[Query.limit(100)]
|
||||||
|
);
|
||||||
|
relationData[field.name] = JSON.parse(JSON.stringify(relRes.documents));
|
||||||
|
} catch { }
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error fetching entity ${entity}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundEditAction = saveEntityAction.bind(null, entity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in pb-10">
|
<div className="space-y-6 animate-fade-in pb-10">
|
||||||
@ -228,6 +141,22 @@ export default async function EntityPage({
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{entity === 'posts' ? (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<PublicationsList
|
||||||
|
initialPosts={documents as unknown as Post[]}
|
||||||
|
user={user as AuthUser}
|
||||||
|
relationData={relationData}
|
||||||
|
submitEditAction={boundEditAction}
|
||||||
|
appwriteConfig={{
|
||||||
|
endpoint,
|
||||||
|
projectId,
|
||||||
|
databaseId: APPWRITE_ALMEIRA_CONFIG.databaseId,
|
||||||
|
}}
|
||||||
|
entity={entity}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
) : (
|
||||||
<DataTable
|
<DataTable
|
||||||
entity={entity}
|
entity={entity}
|
||||||
collectionId={collectionId}
|
collectionId={collectionId}
|
||||||
@ -238,8 +167,8 @@ export default async function EntityPage({
|
|||||||
currentSortDir={dir}
|
currentSortDir={dir}
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
|
||||||
{total > limit && (
|
{total > limit && (
|
||||||
<div className="flex justify-center items-center gap-4 mt-8">
|
<div className="flex justify-center items-center gap-4 mt-8">
|
||||||
<Link
|
<Link
|
||||||
@ -248,7 +177,7 @@ export default async function EntityPage({
|
|||||||
>
|
>
|
||||||
ANTERIOR
|
ANTERIOR
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-slate-400 font-bold tracking-widest text-xs uppercase">
|
<span className="text-slate-400 font-bold tracking-widest text-xs uppercase text-center px-4">
|
||||||
Página {page} de {Math.ceil(total / limit)}
|
Página {page} de {Math.ceil(total / limit)}
|
||||||
</span>
|
</span>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user