fix: media library pagination, preview URLs and dashboard layout optimization

This commit is contained in:
Carlos (Almeira) 2026-03-29 22:27:07 -05:00
parent a99355f76e
commit 4892a3d798
7 changed files with 73 additions and 32 deletions

View File

@ -1,7 +1,7 @@
'use server'
import { Client, Databases, Storage, ID } from 'node-appwrite';
import { databaseId } from '@/lib/appwrite';
import { APPWRITE_ALMEIRA_CONFIG, databaseId } from '@/lib/appwrite';
import { revalidatePath } from 'next/cache';
import { InputFile } from 'node-appwrite/file';
@ -75,7 +75,7 @@ export async function uploadMediaServer(formData: FormData) {
for (const file of files) {
const buffer = Buffer.from(await file.arrayBuffer());
const inputFile = InputFile.fromBuffer(buffer, file.name);
const result = await storage.createFile('media', ID.unique(), inputFile);
const result = await storage.createFile(APPWRITE_ALMEIRA_CONFIG.mediaBucketId, ID.unique(), inputFile);
results.push(result);
}
@ -102,11 +102,11 @@ export async function uploadMediaDocumentServer(formData: FormData) {
for (const file of files) {
const buffer = Buffer.from(await file.arrayBuffer());
const inputFile = InputFile.fromBuffer(buffer, file.name);
const uploadedFile = await storage.createFile('media', ID.unique(), inputFile);
const uploadedFile = await storage.createFile(APPWRITE_ALMEIRA_CONFIG.mediaBucketId, ID.unique(), inputFile);
const doc = await databases.createDocument(DATABASE_ID, 'media', ID.unique(), {
file_id: uploadedFile.$id,
bucket_id: 'media',
bucket_id: APPWRITE_ALMEIRA_CONFIG.mediaBucketId,
titulo: file.name,
alt_text: file.name.split('.')[0],
tipo: file.type.startsWith('image/') ? 'imagen' : 'video'

View File

@ -144,6 +144,7 @@ export default async function EntityPage({
{entity === 'posts' ? (
<ErrorBoundary>
<PublicationsList
key={`list-${entity}-${page}-${q}-${sort}-${dir}`}
initialPosts={documents as unknown as Post[]}
user={user as AuthUser}
relationData={relationData}

View File

@ -10,7 +10,7 @@ import { uploadMediaDocumentServer } from '@/app/actions/appwrite';
/** Utility to generate frontend Appwrite image previews */
export function getAppwriteImageUrl(fileId: string, width = 400, height = 250) {
if (!fileId) return '';
return `${endpoint}/storage/buckets/media/files/${fileId}/preview?project=${projectId}&width=${width}&height=${height}&quality=80`;
return `${endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${fileId}/preview?project=${projectId}&width=${width}&height=${height}&quality=80`;
}
interface MediaGridSelectorProps {
@ -35,6 +35,7 @@ export const MediaGridSelector = ({
const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null);
@ -46,13 +47,23 @@ export const MediaGridSelector = ({
if (isOpen) fetchMedia();
}, [isOpen]);
const fetchMedia = async () => {
const fetchMedia = async (more = false) => {
setIsLoading(true);
try {
const currentOffset = more ? mediaItems.length : 0;
const res = await databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [
Query.limit(100), Query.orderDesc('$createdAt')
Query.limit(20),
Query.offset(currentOffset),
Query.orderDesc('$createdAt')
]);
setMediaItems(res.documents);
if (more) {
setMediaItems(prev => [...prev, ...res.documents]);
} else {
setMediaItems(res.documents);
}
setHasMore(res.documents.length === 20);
} catch (error) {
console.error('Error fetching media:', error);
} finally {
@ -200,8 +211,19 @@ export const MediaGridSelector = ({
)}
</div>
<div className="p-4 bg-slate-950 border-t border-slate-800 text-center">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">{filteredItems.length} Archivos Disponibles</span>
<div className="p-4 bg-slate-950 border-t border-slate-800 flex justify-between items-center px-8">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-widest">{mediaItems.length} Archivos Disponibles</span>
{hasMore && (
<button
onClick={() => fetchMedia(true)}
disabled={isLoading}
className="text-[10px] font-black uppercase tracking-widest text-cyan-500 hover:text-cyan-400 disabled:opacity-50 transition-colors flex items-center gap-2"
>
{isLoading ? <Loader2 size={12} className="animate-spin" /> : null}
Cargar más resultados ↓
</button>
)}
</div>
</div>
</div>

View File

@ -13,11 +13,21 @@ export const StreamOverlays = () => {
// 1. Fetch Approved Messages for Ticker
const fetchApprovedMessages = async () => {
try {
const res = await databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'whatsapp_messages',
[Query.equal('status', 'approved'), Query.limit(10), Query.orderDesc('timestamp')]
);
let res;
try {
res = await databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'whatsapp_messages',
[Query.equal('status', 'approved'), Query.limit(10), Query.orderDesc('timestamp')]
);
} catch (queryError) {
// Fallback for missing 'status' attribute
res = await databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'whatsapp_messages',
[Query.limit(10), Query.orderDesc('$createdAt')]
);
}
setApprovedMessages(res.documents);
} catch (error) {
console.error('Error fetching approved messages:', error);
@ -51,12 +61,13 @@ export const StreamOverlays = () => {
if (response.events.includes('databases.*.collections.*.documents.*.update') ||
response.events.includes('databases.*.collections.*.documents.*.create')) {
const payload = response.payload as any;
if (payload.status === 'approved') {
// Only process as approved if the status exists, otherwise treat all as approved for the ticker if schema is simple
if (payload.status === 'approved' || !payload.hasOwnProperty('status')) {
setApprovedMessages(prev => {
const filtered = prev.filter(m => m.$id !== payload.$id);
return [payload, ...filtered].slice(0, 10);
});
} else {
} else if (payload.status && payload.status !== 'approved') {
setApprovedMessages(prev => prev.filter(m => m.$id !== payload.$id));
}
}
@ -121,17 +132,26 @@ export const StreamOverlays = () => {
{activeItem && (
<div className="bg-slate-900/40 backdrop-blur-2xl border border-white/10 rounded-2xl overflow-hidden shadow-2xl animate-in slide-in-from-left duration-700">
<div className="relative aspect-video">
{activeItem.imagen_destacada_id ? (
<img
src={`${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/media/files/${activeItem.imagen_destacada_id}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`}
className="w-full h-full object-cover"
alt={activeItem.titulo}
/>
) : (
<div className="w-full h-full bg-slate-800 flex items-center justify-center">
<Radio className="text-slate-700" size={48} />
</div>
)}
{(() => {
const mediaId = typeof activeItem.imagen_destacada_id === 'object'
? activeItem.imagen_destacada_id?.$id
: activeItem.imagen_destacada_id;
if (mediaId) {
return (
<img
src={`${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${mediaId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`}
className="w-full h-full object-cover"
alt={activeItem.titulo}
/>
);
}
return (
<div className="w-full h-full bg-slate-800 flex items-center justify-center">
<Radio className="text-slate-700" size={48} />
</div>
);
})()}
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-950/20 to-transparent px-4 py-3 flex flex-col justify-end">
<h4 className="text-sm font-black text-white leading-tight uppercase tracking-tight line-clamp-2">
{activeItem.titulo}

View File

@ -202,9 +202,6 @@ export const PublicationLayout: React.FC<PublicationLayoutProps> = ({
<h1 className="text-2xl font-black font-outfit text-white uppercase tracking-tighter">
{initialData?.$id ? 'Editar Publicación' : 'Nueva Publicación'}
</h1>
<p className="text-slate-500 text-sm font-medium">
{initialData?.$id ? `ID: ${initialData.$id}` : 'Completa todos los campos requeridos.'}
</p>
</div>
</div>

View File

@ -246,7 +246,7 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
)}
{filteredAndSortedPosts.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{filteredAndSortedPosts.map((post) => (
<PostCard
key={post.$id}

View File

@ -18,6 +18,7 @@ export const APPWRITE_ALMEIRA_CONFIG = {
endpoint,
projectId,
databaseId,
mediaBucketId: process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID || 'media',
collections: {
// Existentes
posts: 'posts',