almeira-web/src/components/NewsTicker.tsx
Carlos (Almeira) 5279af2003 refactor: global project hardening and integrity verification
- Add safety checks for direct Appwrite document index access across all root pages.
- Harden NewsTicker and AdWrappers with robust fallback settings and length checks.
- Apply strict typing to dashboard settings page and resolve icon conflicts.
- Final global audit for deprecated Zod .errors usage (0 occurrences remaining).
- Standardize PublicationLayout across creation and edit workflows for posts.
2026-03-28 10:07:32 -05:00

154 lines
7.1 KiB
TypeScript

import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
import { createSessionClient } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import Link from 'next/link';
import { MessageCircle } from 'lucide-react';
import { Settings } from '@/types/settings';
export default async function NewsTicker() {
let tickerItems: any[] = [];
try {
let client: any = await createSessionClient();
if (!client) {
const { createAdminClient } = await import('@/lib/appwrite.server');
client = await createAdminClient();
}
if (!client) throw new Error("No Appwrite client available");
// 1. Fetch Settings
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
const settings = (settingsRes.documents.length > 0 ? settingsRes.documents[0] : {
ticker_count: 10,
rotation_seconds: 5,
feed_news_count: 12,
show_news_date: true
}) as Settings;
const tickerCount = settings.ticker_count || 10;
// 2. Fetch News (NOTICIA)
const typeRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'publication_types', [
Query.equal('codigo', 'NOTICIA')
]);
if (typeRes.documents.length > 0) {
const firstType = typeRes.documents[0];
const typeId = firstType?.$id;
if (!typeId) throw new Error("Could not find typeId for NOTICIA");
const postsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'posts', [
Query.equal('tipo_publicacion_id', typeId),
Query.equal('estado', 'Publicado'),
Query.orderDesc('fecha_publicacion'),
Query.limit(tickerCount)
]);
// Resolve Media
const mediaIds = postsRes.documents.map((p: any) => p.imagen_destacada_id).filter((id: any) => id && typeof id === 'string');
const mediaMap = new Map();
if (mediaIds.length > 0) {
const mediaRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [
Query.equal('$id', mediaIds)
]);
mediaRes.documents.forEach((m: any) => mediaMap.set(m.$id, m));
}
postsRes.documents.forEach((post: any) => {
const media = mediaMap.get(post.imagen_destacada_id);
let imagenUrl = post.og_image_url || null;
if (media) {
imagenUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${media.bucket_id}/files/${media.file_id}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}`;
}
tickerItems.push({
type: 'news',
id: post.$id,
titulo: post.titulo,
slug: post.slug || post.$id,
imagenUrl
});
});
}
// 3. Fetch Approved WhatsApp Messages
try {
const waRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'whatsapp_messages', [
Query.equal('status', 'approved'),
Query.orderDesc('timestamp'),
Query.limit(5)
]);
waRes.documents.forEach((msg: any) => {
tickerItems.push({
type: 'whatsapp',
id: msg.$id,
titulo: `${msg.sender_name}: ${msg.content}`,
slug: null,
imagenUrl: msg.avatar || null
});
});
} catch (waError) {
console.warn("Skipping WhatsApp messages in ticker:", waError);
}
} catch (e) {
console.error("Ticker fetch error", e);
}
if (tickerItems.length === 0) return null;
return (
<div className="w-full bg-cyan-950/80 backdrop-blur-md border-y border-cyan-800/50 overflow-hidden relative flex items-center h-12 z-40">
<div className="absolute left-0 z-20 bg-gradient-to-r from-cyan-950 to-transparent w-20 h-full"></div>
<div className="absolute right-0 z-20 bg-gradient-to-l from-cyan-950 to-transparent w-20 h-full"></div>
<div className="flex animate-marquee whitespace-nowrap items-center group-hover:[animation-play-state:paused]">
{/* Doble render para bucle infinito visual */}
{[...tickerItems, ...tickerItems].map((item, idx) => {
const isWA = item.type === 'whatsapp';
const Content = (
<div className="flex items-center gap-4 mx-8 group cursor-pointer border-r border-cyan-800/30 pr-8 py-1">
{isWA ? (
<div className="w-9 h-9 rounded-full bg-green-500/20 flex items-center justify-center border border-green-500/30">
<MessageCircle size={14} className="text-green-400 fill-current" />
</div>
) : (
item.imagenUrl && (
// eslint-disable-next-line @next/next/no-img-element
<img src={item.imagenUrl} alt={item.titulo} className="w-12 h-7 aspect-video object-cover rounded shadow-[0_0_10px_rgba(6,182,212,0.3)] group-hover:scale-110 transition-all duration-500 border border-white/10" />
)
)}
<span className={`text-[11px] font-black uppercase tracking-widest flex items-center gap-2 transition-colors duration-300 ${
isWA ? 'text-green-400 group-hover:text-white' : 'text-slate-200 group-hover:text-cyan-400'
}`}>
{isWA && <span className="text-[9px] font-bold px-1.5 py-0.5 bg-green-500/20 rounded border border-green-500/30 text-green-400">COMUNIDAD</span>}
{item.titulo.length > 80 ? item.titulo.substring(0, 80) + '...' : item.titulo}
</span>
</div>
);
if (item.slug) {
return <Link key={idx} href={`/${item.slug}`} className="flex items-center">{Content}</Link>;
}
return <div key={idx} className="flex items-center">{Content}</div>;
})}
</div>
<style dangerouslySetInnerHTML={{__html: `
@keyframes marquee {
0% { transform: translateX(0%); }
100% { transform: translateX(-50%); }
}
.animate-marquee {
animation: marquee 40s linear infinite;
}
.animate-marquee:hover {
animation-play-state: paused;
}
`}} />
</div>
)
}