feat(media): registrar imagenes en biblioteca de medios de Appwrite e insertar como imagen destacada con resolucion 640x360 y 640x640

This commit is contained in:
AI Bot 2026-08-24 12:36:05 -05:00
parent 0e9bb2365e
commit c6b825931f
2 changed files with 74 additions and 31 deletions

View File

@ -2,7 +2,7 @@ import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
import { createAdminClient } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import { GlobalSidebar, hasSidebarContent } from '@/components/AdWrappers';
import { Calendar, User, ArrowLeft, Newspaper } from 'lucide-react';
import { Calendar, User, ArrowLeft, Newspaper, Tag } from 'lucide-react';
import Link from 'next/link';
import { decodeHtmlEntities } from '@/lib/utils';
import DOMPurify from 'isomorphic-dompurify';
@ -222,13 +222,16 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
const PROJECT_ID = process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID || '69b84df5003bd8957c5c';
const ENDPOINT = process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT || 'https://appwrite.almeira.com.co/v1';
let imagenUrl = post.og_image_url || null;
const isSquare = post.tipo === 'evento' || post.tipo === 'aviso' ||
(typeof post.tipo_publicacion_id === 'object' && (post.tipo_publicacion_id.codigo === 'EVENTO' || post.tipo_publicacion_id.codigo === 'CLASIFICADO'));
const imgHeight = isSquare ? 640 : 360;
if (post.imagen_destacada_id) {
try {
const client = await createAdminClient();
const mediaRes = await client.databases.getDocument(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', post.imagen_destacada_id);
if (mediaRes) {
imagenUrl = `${ENDPOINT}/storage/buckets/${mediaRes.bucket_id}/files/${mediaRes.file_id}/preview?project=${PROJECT_ID}&width=640&height=360&quality=90`;
imagenUrl = `${ENDPOINT}/storage/buckets/${mediaRes.bucket_id}/files/${mediaRes.file_id}/preview?project=${PROJECT_ID}&width=640&height=${imgHeight}&quality=90`;
}
} catch(e) {}
}
@ -248,21 +251,22 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
<article className="flex-1 max-w-4xl overflow-hidden">
<Link href="/" className="inline-flex items-center gap-2 text-cyan-400 font-bold hover:text-cyan-300 transition-colors mb-8 group">
<ArrowLeft size={18} className="transition-transform group-hover:-translate-x-1" />
VOLVER A NOTICIAS
<span>Volver a la portada</span>
</Link>
<header className="mb-10">
<div className="flex items-center gap-4 mb-6">
<span className="px-3 py-1 rounded-full bg-cyan-500/10 border border-cyan-500/30 text-cyan-400 text-xs font-black tracking-widest uppercase">
{post.tipo?.toUpperCase() || 'NOTICIA'}
<header className="mb-10 space-y-6">
<div className="flex flex-wrap items-center gap-4 text-xs font-bold uppercase tracking-wider text-slate-400">
<span className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-cyan-500/10 text-cyan-400 border border-cyan-500/20">
<Tag size={14} />
{typeof post.tipo_publicacion_id === 'object' ? post.tipo_publicacion_id.descripcion : (post.tipo || 'Noticia')}
</span>
<span className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-slate-800/60 border border-white/5">
<Calendar size={14} />
{fechaFormateada}
</span>
<div className="flex items-center gap-2 text-slate-500 text-sm font-medium">
<Calendar size={16} />
<span>{fechaFormateada}</span>
</div>
</div>
<h1 className="text-4xl md:text-6xl font-outfit font-black text-white leading-tight mb-6">
<h1 className="text-3xl md:text-5xl font-black font-outfit text-white tracking-tight leading-tight">
{decodeHtmlEntities(post.titulo)}
</h1>
@ -283,12 +287,12 @@ export default async function NoticiaDetail({ params }: { params: Promise<{ slug
}} />
</div>
) : imagenUrl ? (
<div className="relative aspect-video w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl bg-black">
<div className={`relative ${isSquare ? 'aspect-square max-w-xl mx-auto' : 'aspect-video'} w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl bg-black`}>
<img src={imagenUrl} alt={post.titulo} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-gradient-to-t from-slate-950/50 to-transparent"></div>
</div>
) : (
<div className="relative aspect-video w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl bg-gradient-to-br from-slate-900 to-slate-950 flex flex-col items-center justify-center gap-4">
<div className={`relative ${isSquare ? 'aspect-square max-w-xl mx-auto' : 'aspect-video'} w-full rounded-3xl overflow-hidden mb-12 border border-white/10 shadow-2xl bg-gradient-to-br from-slate-900 to-slate-950 flex flex-col items-center justify-center gap-4`}>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--accent-glow))] opacity-5 pointer-events-none"></div>
<Newspaper size={48} className="text-cyan-500/40 animate-pulse" />
<span className="text-slate-500 font-bold uppercase tracking-widest text-xs font-outfit">Almeira Web</span>

View File

@ -110,26 +110,33 @@ export async function POST(req: Request) {
// LOGIC 2: PROCESAR NUEVO MENSAJE PARA GEMINI
// Evitámos tratar de procesar respuestas cortas o mensajes vacíos
if (!text && !messageData.imageMessage) {
if (!text && !messageData.imageMessage && !messageData.videoMessage && !messageData.documentMessage) {
return NextResponse.json({ success: true, message: 'Ignored empty message' }, { status: 200 });
}
// Verificamos si tiene multimedia
// Verificamos si tiene multimedia directa o en mensaje reciente
let base64Media = undefined;
let mimeType = undefined;
let imageId = undefined;
let storageFileId = undefined;
let featuredMediaDocId = undefined;
if (messageData.imageMessage || messageData.videoMessage || messageData.documentMessage) {
const media = await getBase64FromMediaMessage(data);
if (media) {
base64Media = media.base64;
mimeType = media.mimetype;
}
}
// Subimos a Appwrite Storage
// Llamar a Gemini para estructurar la publicación
const parsed = await processWhatsAppMessage(text, base64Media, mimeType);
// Si tenemos multimedia, subimos a Storage y creamos el registro en la Biblioteca de Medios (Colección 'media')
if (base64Media && mimeType) {
try {
const buffer = Buffer.from(base64Media, 'base64');
// Appwrite requiere un InputFile o similar. node-appwrite maneja InputFile.fromBuffer
const extension = mimeType.split('/')[1] || 'jpg';
const fileName = `wa_${Date.now()}.${extension}`;
const fileName = `wa_${parsed.tipo}_${Date.now()}.${extension}`;
const file = InputFile.fromBuffer(buffer, fileName);
const uploadedFile = await storage.createFile(
@ -137,12 +144,38 @@ export async function POST(req: Request) {
ID.unique(),
file
);
imageId = uploadedFile.$id;
storageFileId = uploadedFile.$id;
// Crear registro en la Biblioteca de Medios (Colección 'media')
const mediaDoc = await databases.createDocument(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'media',
ID.unique(),
{
file_id: storageFileId,
bucket_id: APPWRITE_ALMEIRA_CONFIG.mediaBucketId,
titulo: truncateString(parsed.titulo, 240),
alt_text: truncateString(parsed.titulo, 240),
descripcion_seo: truncateString(parsed.seo_descripcion, 450),
tipo: mimeType.startsWith('video') ? 'video' : 'imagen'
}
);
featuredMediaDocId = mediaDoc.$id;
console.log(`[WhatsApp Webhook] Imagen subida a Biblioteca de Medios (ID: ${featuredMediaDocId}, Storage: ${storageFileId})`);
} catch (err: any) {
console.error('[WhatsApp Webhook] Error subiendo multimedia a Appwrite:', err.message);
}
}
// Llamar a Gemini
const parsed = await processWhatsAppMessage(text, base64Media, mimeType);
// Determinar dimensiones según el tipo de publicación
// Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1)
const isSquare = parsed.tipo === 'evento' || parsed.tipo === 'aviso';
const imgHeight = isSquare ? 640 : 360;
let ogImageUrl = undefined;
if (storageFileId) {
ogImageUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${storageFileId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}&width=640&height=${imgHeight}&quality=85`;
}
// Determinar fecha de vencimiento
let fechaVencimiento = parsed.fecha_vencimiento;
@ -150,22 +183,26 @@ export async function POST(req: Request) {
fechaVencimiento = getLastDayOfMonth();
}
// Buscar un tipo de publicación por defecto (Requerido por Appwrite Schema)
// Buscar tipo de publicación correspondiente (NOTICIA, EVENTO, CLASIFICADO)
let tipoPublicacionId = '';
try {
const types = await databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
APPWRITE_ALMEIRA_CONFIG.collections.publicationTypes,
[]
[Query.limit(20)]
);
if (types.documents.length > 0) {
tipoPublicacionId = types.documents[0].$id;
const targetCode = parsed.tipo === 'evento' ? 'EVENTO' : parsed.tipo === 'aviso' ? 'CLASIFICADO' : 'NOTICIA';
const matched = types.documents.find((d: any) => d.codigo === targetCode) ||
types.documents.find((d: any) => d.codigo === 'NOTICIA') ||
types.documents[0];
if (matched) {
tipoPublicacionId = matched.$id;
}
} catch (e) {
console.warn('No se pudo obtener un tipo de publicación por defecto:', e);
console.warn('No se pudo obtener el tipo de publicación en Appwrite:', e);
}
// Crear Post en Appwrite
// Crear Post en Appwrite (Vinculando la imagen destacada de la biblioteca de medios)
const newPost = await databases.createDocument(
APPWRITE_ALMEIRA_CONFIG.databaseId,
APPWRITE_ALMEIRA_CONFIG.collections.posts,
@ -178,14 +215,16 @@ export async function POST(req: Request) {
seo_titulo: truncateString(parsed.titulo, 60),
seo_descripcion: truncateString(parsed.seo_descripcion, 155),
contenido: parsed.contenido,
estado: 'borrador', // Se requiere aprobación
estado: 'borrador', // Requiere aprobación
fecha_publicacion: new Date().toISOString(),
...(tipoPublicacionId && { tipo_publicacion_id: tipoPublicacionId }),
...(fechaVencimiento && { fecha_finalizacion: new Date(fechaVencimiento).toISOString() }),
...(imageId && { imagen_destacada_id: imageId })
...(featuredMediaDocId && { imagen_destacada_id: featuredMediaDocId }),
...(ogImageUrl && { og_image_url: ogImageUrl })
}
);
// Notificar al grupo de WhatsApp pidiendo aprobación
const requestText = `📰 *Publicación Procesada (Pendiente de Aprobación)*\n\n`
+ `*Título:* ${parsed.titulo}\n`