fix: media library pagination, preview URLs and dashboard layout optimization
This commit is contained in:
parent
a99355f76e
commit
4892a3d798
@ -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'
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
Reference in New Issue
Block a user