- 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.
154 lines
7.1 KiB
TypeScript
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>
|
|
)
|
|
}
|