fix API crash and added UI to card

This commit is contained in:
AI Bot 2026-08-23 16:12:25 -05:00
parent fbc7343730
commit 58025866be
2 changed files with 71 additions and 38 deletions

View File

@ -5,7 +5,15 @@ import { createAdminClient } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import { APPWRITE_ALMEIRA_CONFIG, endpoint, projectId } from '@/lib/appwrite';
import { collectionMap } from '@/lib/schema';
import { getAppwriteImageUrl } from '@/components/dashboard/MediaGridSelector';
function getAppwriteImageUrl(fileId: string, bucketId?: string, width = 400, height = 250, isVideo = false) {
if (!fileId) return '';
const finalBucketId = bucketId || APPWRITE_ALMEIRA_CONFIG.mediaBucketId;
if (isVideo) {
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/view?project=${projectId}`;
}
return `${endpoint}/storage/buckets/${finalBucketId}/files/${fileId}/preview?project=${projectId}&width=${width}&height=${height}&quality=80`;
}
export const runtime = 'nodejs';
@ -109,33 +117,44 @@ export async function GET(request: Request) {
const plainTextResumen = (post.resumen_redes || post.contenido || '').replace(/<[^>]*>?/gm, ' ').replace(/\s+/g, ' ').trim();
const safeResumen = plainTextResumen.length > 120 ? plainTextResumen.substring(0, 120) + '...' : plainTextResumen;
// USING @vercel/og syntax to create a React Node inline without JSX in a .ts file ?
// WAIT! This file is route.ts, JSX is not supported in .ts unless it's .tsx!
// Let's use react.createElement
const React = require('react');
const cardNode = React.createElement('div', {
style: {
display: 'flex', flexDirection: 'column', width: 600, height: 400,
backgroundColor: '#0f172a', color: 'white', borderRadius: 24,
overflow: 'hidden', border: '2px solid #1e293b', fontFamily: 'sans-serif'
display: 'flex', flexDirection: 'column', width: 600, height: 600,
backgroundColor: '#0f172a', borderRadius: 24,
overflow: 'hidden', border: '1px solid rgba(255,255,255,0.1)', fontFamily: 'sans-serif'
}
},
imageUrl
? React.createElement('img', { src: imageUrl, style: { width: 600, height: 230, objectFit: 'cover' } })
: React.createElement('div', { style: { width: 600, height: 230, backgroundColor: '#1e293b', display: 'flex', alignItems: 'center', justifyContent: 'center' } },
React.createElement('span', { style: { color: '#475569', fontSize: 24 } }, 'Sin Imagen')
),
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', padding: 24, gap: 8 } },
React.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
React.createElement('span', { style: { color: '#06b6d4', fontSize: 14, fontWeight: 'bold', textTransform: 'uppercase' } }, post.estado || 'Borrador'),
React.createElement('span', { style: { color: '#64748b', fontSize: 14 } }, fechaFormat)
),
React.createElement('h3', { style: { fontSize: 22, fontWeight: 'bold', margin: 0, lineHeight: 1.2, color: '#f8fafc' } }, post.titulo || 'Sin título'),
React.createElement('p', { style: { fontSize: 16, color: '#94a3b8', margin: 0, lineHeight: 1.4 } }, safeResumen)
React.createElement('div', {
style: { display: 'flex', width: 600, height: 337, backgroundColor: '#1e293b', position: 'relative' }
},
imageUrl
? React.createElement('img', { src: imageUrl, style: { width: 600, height: 337, objectFit: 'cover' } })
: React.createElement('div', { style: { width: 600, height: 337, display: 'flex', alignItems: 'center', justifyContent: 'center' } },
React.createElement('span', { style: { color: '#475569', fontSize: 24, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 4 } }, 'Almeira Web')
),
React.createElement('div', { style: { position: 'absolute', top: 16, left: 16, display: 'flex' } },
React.createElement('span', {
style: { padding: '6px 12px', borderRadius: 9999, fontSize: 12, fontWeight: 'bold', letterSpacing: 1, backgroundColor: 'rgba(0,0,0,0.6)', color: '#67e8f9', border: '1px solid rgba(255,255,255,0.2)' }
}, 'NOTICIA')
)
),
React.createElement('div', { style: { display: 'flex', flexDirection: 'column', padding: 32, flexGrow: 1 } },
React.createElement('h3', { style: { fontSize: 26, fontWeight: 'bold', margin: '0 0 16px 0', lineHeight: 1.3, color: 'white' } }, post.titulo || 'Sin título'),
React.createElement('p', { style: { fontSize: 16, color: '#94a3b8', margin: '0 0 32px 0', lineHeight: 1.6 } }, safeResumen),
React.createElement('div', { style: { marginTop: 'auto', paddingTop: 20, borderTop: '1px solid rgba(255,255,255,0.1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
React.createElement('div', { style: { display: 'flex', alignItems: 'center' } },
React.createElement('span', { style: { color: '#06b6d4', fontSize: 13, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1, marginRight: 8 } }, 'ESTADO:'),
React.createElement('span', { style: { color: '#94a3b8', fontSize: 13, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 } }, post.estado || 'Borrador')
),
React.createElement('span', { style: { color: '#06b6d4', fontSize: 13, fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 } }, fechaFormat)
)
)
);
const cardImage = new ImageResponse(cardNode, { width: 600, height: 400 });
const cardImage = new ImageResponse(cardNode, { width: 600, height: 600 });
const imgBuffer = await cardImage.arrayBuffer();
docChildren.push(

View File

@ -1,5 +1,5 @@
import Link from 'next/link';
import { ArrowUpRight, Calendar } from 'lucide-react';
import { ArrowUpRight, Calendar, Edit2, Trash2 } from 'lucide-react';
import { decodeHtmlEntities } from '@/lib/utils';
interface NewsCardProps {
@ -18,13 +18,13 @@ interface NewsCardProps {
imageAspect?: string;
}
export const NewsCard = ({
id,
slug,
titulo: rawTitulo,
resumen: rawResumen,
tipo,
fecha,
export const NewsCard = ({
id,
slug,
titulo: rawTitulo,
resumen: rawResumen,
tipo,
fecha,
imagenUrl,
showCategory = false,
showFooterDate = false,
@ -34,7 +34,7 @@ export const NewsCard = ({
imageAspect = 'aspect-video'
}: NewsCardProps) => {
const titulo = decodeHtmlEntities(rawTitulo);
// Strip HTML tags safely for a plain text excerpt
const plainTextResumen = (rawResumen || '').replace(/<[^>]*>?/gm, ' ').replace(/\s+/g, ' ').trim();
const resumen = decodeHtmlEntities(plainTextResumen);
@ -50,16 +50,30 @@ export const NewsCard = ({
<span className="sr-only">Ver {titulo}</span>
</Link>
{/* Edit Button for Admins (Elevado en el stacking context) */}
{/* Admin Controls (Elevado en el stacking context) - Dynamic Island style */}
{isAdmin && (
<div className="absolute top-4 right-4 z-30 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto">
<Link
href={`/dashboard/posts/${id}`}
className="flex items-center gap-2 px-3 py-1.5 rounded-full text-[10px] font-black uppercase tracking-wider bg-slate-950/80 backdrop-blur border border-cyan-500/50 text-cyan-400 hover:bg-cyan-500 hover:text-slate-950 transition-all shadow-lg cursor-pointer hover:scale-105"
title="Editar en Dashboard"
<div className="absolute top-4 right-4 z-30 flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity translate-y-[-10px] group-hover:translate-y-0 duration-300 pointer-events-auto">
<div className="flex items-center bg-slate-950/80 backdrop-blur-md rounded-full shadow-2xl border border-white/10 p-1">
<Link
href={`/dashboard/posts/${id}`}
className="flex items-center justify-center px-4 py-2 rounded-full bg-slate-800/50 hover:bg-cyan-500 text-cyan-400 hover:text-slate-950 transition-all hover:scale-105 tooltip-trigger group/btn"
title="Editar Publicación"
>
<span className="hidden sm:inline pointer-events-none">Editar</span>
<Edit2 size={14} className="group-hover/btn:animate-pulse" />
<span className="ml-2 text-[10px] font-black uppercase tracking-wider hidden sm:inline">Editar</span>
</Link>
<div className="w-[1px] h-4 bg-white/20 mx-1"></div>
<Link
href={`/dashboard/posts/${id}`}
className="flex items-center justify-center w-8 h-8 rounded-full bg-transparent hover:bg-red-500/20 text-red-500 hover:text-red-400 transition-all hover:scale-110 tooltip-trigger"
title="Eliminar Publicación"
>
<Trash2 size={14} />
<span className="sr-only">Eliminar</span>
</Link>
</div>
</div>
)}
@ -79,7 +93,7 @@ export const NewsCard = ({
<span className="text-[10px] font-black uppercase tracking-widest text-slate-700">Almeira Web</span>
</div>
)}
{/* Badge/Tag - Habilitable por prop */}
{showCategory && (
<div className="absolute top-4 left-4 z-30">
@ -95,7 +109,7 @@ export const NewsCard = ({
<h3 className="text-xl font-bold font-outfit text-white leading-snug mb-3 group-hover:text-cyan-400 transition-colors line-clamp-2">
{titulo}
</h3>
<p className="text-slate-400 text-sm leading-relaxed mb-6 line-clamp-3">
{resumen}
</p>
@ -105,10 +119,10 @@ export const NewsCard = ({
<div className="mt-auto pt-5 border-t border-white/10 flex items-center justify-between">
<span className="text-xs font-black tracking-widest uppercase text-cyan-500 group-hover:text-cyan-300 transition-colors">
{showFooterDate ? (
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<Calendar size={12} />
{fechaFormateada}
</div>
</div>
) : (
footerText || ""
)}