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

View File

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

View File

@ -10,7 +10,7 @@ import { uploadMediaDocumentServer } from '@/app/actions/appwrite';
/** Utility to generate frontend Appwrite image previews */ /** Utility to generate frontend Appwrite image previews */
export function getAppwriteImageUrl(fileId: string, width = 400, height = 250) { export function getAppwriteImageUrl(fileId: string, width = 400, height = 250) {
if (!fileId) return ''; 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 { interface MediaGridSelectorProps {
@ -35,6 +35,7 @@ export const MediaGridSelector = ({
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
@ -46,13 +47,23 @@ export const MediaGridSelector = ({
if (isOpen) fetchMedia(); if (isOpen) fetchMedia();
}, [isOpen]); }, [isOpen]);
const fetchMedia = async () => { const fetchMedia = async (more = false) => {
setIsLoading(true); setIsLoading(true);
try { try {
const currentOffset = more ? mediaItems.length : 0;
const res = await databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [ 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) { } catch (error) {
console.error('Error fetching media:', error); console.error('Error fetching media:', error);
} finally { } finally {
@ -200,8 +211,19 @@ export const MediaGridSelector = ({
)} )}
</div> </div>
<div className="p-4 bg-slate-950 border-t border-slate-800 text-center"> <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">{filteredItems.length} Archivos Disponibles</span> <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> </div>
</div> </div>

View File

@ -13,11 +13,21 @@ export const StreamOverlays = () => {
// 1. Fetch Approved Messages for Ticker // 1. Fetch Approved Messages for Ticker
const fetchApprovedMessages = async () => { const fetchApprovedMessages = async () => {
try { try {
const res = await databases.listDocuments( let res;
APPWRITE_ALMEIRA_CONFIG.databaseId, try {
'whatsapp_messages', res = await databases.listDocuments(
[Query.equal('status', 'approved'), Query.limit(10), Query.orderDesc('timestamp')] 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); setApprovedMessages(res.documents);
} catch (error) { } catch (error) {
console.error('Error fetching approved messages:', error); console.error('Error fetching approved messages:', error);
@ -51,12 +61,13 @@ export const StreamOverlays = () => {
if (response.events.includes('databases.*.collections.*.documents.*.update') || if (response.events.includes('databases.*.collections.*.documents.*.update') ||
response.events.includes('databases.*.collections.*.documents.*.create')) { response.events.includes('databases.*.collections.*.documents.*.create')) {
const payload = response.payload as any; 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 => { setApprovedMessages(prev => {
const filtered = prev.filter(m => m.$id !== payload.$id); const filtered = prev.filter(m => m.$id !== payload.$id);
return [payload, ...filtered].slice(0, 10); return [payload, ...filtered].slice(0, 10);
}); });
} else { } else if (payload.status && payload.status !== 'approved') {
setApprovedMessages(prev => prev.filter(m => m.$id !== payload.$id)); setApprovedMessages(prev => prev.filter(m => m.$id !== payload.$id));
} }
} }
@ -121,17 +132,26 @@ export const StreamOverlays = () => {
{activeItem && ( {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="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"> <div className="relative aspect-video">
{activeItem.imagen_destacada_id ? ( {(() => {
<img const mediaId = typeof activeItem.imagen_destacada_id === 'object'
src={`${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/media/files/${activeItem.imagen_destacada_id}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`} ? activeItem.imagen_destacada_id?.$id
className="w-full h-full object-cover" : activeItem.imagen_destacada_id;
alt={activeItem.titulo}
/> if (mediaId) {
) : ( return (
<div className="w-full h-full bg-slate-800 flex items-center justify-center"> <img
<Radio className="text-slate-700" size={48} /> src={`${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${mediaId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`}
</div> 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"> <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"> <h4 className="text-sm font-black text-white leading-tight uppercase tracking-tight line-clamp-2">
{activeItem.titulo} {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"> <h1 className="text-2xl font-black font-outfit text-white uppercase tracking-tighter">
{initialData?.$id ? 'Editar Publicación' : 'Nueva Publicación'} {initialData?.$id ? 'Editar Publicación' : 'Nueva Publicación'}
</h1> </h1>
<p className="text-slate-500 text-sm font-medium">
{initialData?.$id ? `ID: ${initialData.$id}` : 'Completa todos los campos requeridos.'}
</p>
</div> </div>
</div> </div>

View File

@ -246,7 +246,7 @@ export const PublicationsList: React.FC<PublicationsListProps> = ({
)} )}
{filteredAndSortedPosts.length > 0 ? ( {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) => ( {filteredAndSortedPosts.map((post) => (
<PostCard <PostCard
key={post.$id} key={post.$id}

View File

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