almeira-web/src/app/page.tsx

281 lines
16 KiB
TypeScript

import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
import { createAdminClient, getLoggedInUser } from '@/lib/appwrite.server';
import { Query } from 'node-appwrite';
import { cookies } from 'next/headers';
import { NewsCard } from '@/components/NewsCard';
import { GlobalSidebar, hasSidebarContent } from '@/components/AdWrappers';
import { RadioTower, Sparkles, ChevronDown } from 'lucide-react';
import { decodeHtmlEntities } from '@/lib/utils';
import { Settings } from '@/types/settings';
import { Post, MediaItem } from '@/types/publications';
async function fetchHomeData(page: number = 1, isAdmin: boolean = false, adminMonth: number | null = null) {
try {
const client = await createAdminClient();
// 1. Settings
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
const settings = JSON.parse(JSON.stringify(settingsRes.documents.length > 0 ? settingsRes.documents[0] : {
ticker_count: 10,
rotation_seconds: 5,
feed_news_count: 12,
show_news_date: true
})) as Settings;
const postsCount = settings.feed_news_count || 12;
const cardSettings = {
showCategory: settings.news_card_show_category ?? false,
showFooterDate: settings.show_news_date !== false,
showFooterButton: settings.news_card_show_footer_button ?? true,
footerText: settings.news_card_footer_text || ""
};
// 2. Main News (NOTICIA)
const typeRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'publication_types', [
Query.equal('codigo', 'NOTICIA')
]);
let posts: Post[] = [];
let totalPages = 1;
if (typeRes.documents.length > 0) {
const firstType = typeRes.documents[0];
const typeId = firstType?.$id;
if (!typeId) return { posts: [], cardSettings, totalPages: 1 };
const offset = (page - 1) * postsCount;
const queries = [
Query.equal('tipo_publicacion_id', typeId),
Query.orderDesc('fecha_publicacion'),
Query.limit(postsCount),
Query.offset(offset)
];
if (isAdmin) {
queries.push(Query.equal('estado', ['Publicado', 'publicado', 'borrador']));
if (adminMonth) {
const currentYear = new Date().getFullYear();
// First day of the selected month
const startDate = new Date(currentYear, adminMonth - 1, 1).toISOString();
// First day of the next month
const endDate = new Date(currentYear, adminMonth, 1).toISOString();
queries.push(Query.greaterThanEqual('fecha_publicacion', startDate));
queries.push(Query.lessThan('fecha_publicacion', endDate));
}
} else {
queries.push(Query.equal('estado', ['Publicado', 'publicado']));
}
const postsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'posts', queries);
posts = postsRes.documents as unknown as Post[];
totalPages = Math.ceil(postsRes.total / postsCount);
// 3. Resolve Media
const mediaIds = posts.map(p => p.imagen_destacada_id).filter(id => id && typeof id === 'string') as string[];
if (mediaIds.length > 0) {
const mediaRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [
Query.equal('$id', mediaIds as string[])
]);
const mediaMap = new Map<string, MediaItem>(mediaRes.documents.map(m => [m.$id, m as unknown as MediaItem]));
posts = posts.map(p => JSON.parse(JSON.stringify({
...p,
resolved_media: mediaMap.get(p.imagen_destacada_id as string)
})));
} else {
posts = JSON.parse(JSON.stringify(posts));
}
}
return { posts: JSON.parse(JSON.stringify(posts)) as Post[], cardSettings, totalPages };
} catch (e) {
console.error("Home fetch error", e);
return { posts: [], cardSettings: { showCategory: false, showFooterDate: true, showFooterButton: true, footerText: "" }, totalPages: 1 };
}
}
export const dynamic = "force-dynamic";
export default async function Home(props: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) {
const searchParams = await props.searchParams;
const page = Number(searchParams?.page) || 1;
const adminMonth = searchParams?.month ? Number(searchParams.month) : null;
const rawUser = await getLoggedInUser();
const user = rawUser ? JSON.parse(JSON.stringify(rawUser)) : null;
const isAdmin = (user as any)?.labels?.includes('admin') || (user as any)?.labels?.includes('editor') || user?.$id === 'admin-emergency';
const { posts, cardSettings, totalPages } = await fetchHomeData(page, isAdmin, adminMonth);
const hasSidebar = await hasSidebarContent();
const cookieStore = await cookies();
const hasVisited = cookieStore.has('almeira_visited');
return (
<main className="animate-fade-in relative">
{!hasVisited && (
<>
<script dangerouslySetInnerHTML={{ __html: `document.cookie = "almeira_visited=true; path=/; max-age=31536000";` }} />
{/* Sci-Fi Hero Section */}
<section className="relative flex flex-col items-center justify-center min-h-screen text-center px-4 overflow-hidden mesh-bg">
{/* Dynamic Orbs background */}
<div className="absolute top-1/4 left-1/4 w-[500px] h-[500px] bg-cyan-500/20 rounded-full blur-[120px] mix-blend-screen pointer-events-none animate-pulse"></div>
<div className="absolute bottom-1/4 right-1/4 w-[600px] h-[600px] bg-purple-600/20 rounded-full blur-[150px] mix-blend-screen pointer-events-none animate-float"></div>
<div className="relative z-10 flex flex-col items-center max-w-5xl mx-auto">
<div className="inline-flex items-center gap-2 px-4 py-2 border border-cyan-500/30 rounded-full bg-cyan-500/10 text-cyan-300 text-sm font-semibold mb-8 animate-float shadow-[0_0_20px_rgba(6,182,212,0.2)] backdrop-blur-md">
<RadioTower size={16} className="animate-pulse" />
<span className="tracking-widest uppercase">Señal En Vivo 24/7</span>
</div>
<h1 className="text-6xl md:text-8xl font-outfit font-black tracking-tighter mb-6 leading-[1.1]">
NOTICIAS AL <br/>
<span className="holographic-text block mt-2">SIGUIENTE NIVEL</span>
</h1>
<p className="text-lg md:text-xl text-slate-400 max-w-2xl mb-12 font-medium leading-relaxed">
El primer portal transaccional hiperconectado. Sumérgete en la actualidad de la región en tiempo real con una interfaz sin precedentes.
</p>
<a
href="#noticias"
className="group relative inline-flex items-center justify-center gap-3 px-8 py-4 rounded-full font-bold text-white text-lg overflow-hidden transition-all hover:scale-105"
>
<div className="absolute inset-0 bg-gradient-to-r from-cyan-500 to-blue-600 opacity-90 group-hover:opacity-100 transition-opacity"></div>
<div className="absolute -inset-1 bg-cyan-400 blur-xl opacity-40 group-hover:opacity-70 transition-opacity rounded-full animate-pulse"></div>
<Sparkles size={20} className="relative z-10" />
<span className="relative z-10 tracking-wide">EXPLORAR CONTENIDO</span>
</a>
</div>
{/* Scroll Indicator */}
<a href="#noticias" className="absolute bottom-10 left-1/2 -translate-x-1/2 flex flex-col items-center gap-2 text-slate-500 animate-bounce cursor-pointer hover:text-cyan-400 transition-colors">
<span className="text-xs font-bold uppercase tracking-widest">Descubrir</span>
<ChevronDown size={24} className="text-cyan-500" />
</a>
</section>
</>
)}
{/* Feed Section */}
<section id="noticias" className={`container mx-auto px-4 relative z-10 w-full ${hasVisited ? 'pt-8 pb-24' : 'py-24'}`}>
<div className="flex flex-col lg:flex-row gap-8 w-full items-start">
<div className="flex-1 w-full relative">
<div className={`flex flex-col md:flex-row items-end mb-12 gap-6 ${hasVisited && isAdmin ? 'justify-end' : 'justify-between'}`}>
{!hasVisited && (
<div>
<div className="flex items-center gap-3 mb-2">
<div className="w-12 h-1 bg-gradient-to-r from-cyan-400 to-blue-600 rounded-full"></div>
<span className="text-cyan-400 font-bold tracking-widest uppercase text-sm">Último Minuto</span>
</div>
<h2 className="text-4xl md:text-5xl font-outfit font-black text-white">Feed de <span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-slate-500">Noticias</span></h2>
</div>
)}
{isAdmin && (
<form action="/" method="GET" className="flex items-center gap-3 bg-slate-800/80 p-3 rounded-xl border border-white/10">
<input type="hidden" name="page" value="1" />
<span className="text-sm text-slate-400 font-bold uppercase tracking-wider">Filtro Admin:</span>
<select
name="month"
className="bg-slate-900 border border-white/10 text-white rounded-lg px-3 py-2 text-sm outline-none focus:border-cyan-500"
defaultValue={adminMonth || ""}
>
<option value="">Todos los meses</option>
<option value="1">Enero</option>
<option value="2">Febrero</option>
<option value="3">Marzo</option>
<option value="4">Abril</option>
<option value="5">Mayo</option>
<option value="6">Junio</option>
<option value="7">Julio</option>
<option value="8">Agosto</option>
<option value="9">Septiembre</option>
<option value="10">Octubre</option>
<option value="11">Noviembre</option>
<option value="12">Diciembre</option>
</select>
<button type="submit" className="bg-cyan-600 hover:bg-cyan-500 text-white font-bold px-4 py-2 rounded-lg text-sm transition-colors">
Filtrar
</button>
</form>
)}
</div>
{posts.length === 0 ? (
<div className="w-full py-32 flex flex-col items-center justify-center border border-white/5 rounded-3xl glass-panel relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-b from-cyan-500/5 to-transparent"></div>
<div className="p-6 rounded-full bg-slate-800/80 mb-6 border border-white/10 shadow-[0_0_40px_rgba(6,182,212,0.15)] relative z-10">
<RadioTower size={48} className="text-cyan-500" />
</div>
<h3 className="text-2xl font-black font-outfit text-white mb-2 relative z-10">Sin Conexión Reciente</h3>
<p className="text-slate-400 relative z-10">El portal se encuentra a la espera de señales desde el núcleo central.</p>
</div>
) : (
<div className={`grid grid-cols-1 md:grid-cols-2 ${hasSidebar ? 'lg:grid-cols-2 xl:grid-cols-3' : 'lg:grid-cols-3 xl:grid-cols-4'} gap-6`}>
{posts.map((post) => {
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 mediaObj = post.resolved_media || (typeof post.imagen_destacada_id === 'object' ? post.imagen_destacada_id : null) as MediaItem | null;
if (mediaObj) {
imagenUrl = `${ENDPOINT}/storage/buckets/${mediaObj.bucket_id}/files/${mediaObj.file_id}/preview?project=${PROJECT_ID}&width=800&height=500&quality=85`;
}
return (
<NewsCard
key={`post-${post.$id}`}
id={post.$id}
slug={post.slug || post.$id}
titulo={post.titulo}
resumen={post.resumen_redes || post.contenido || ''}
tipo={typeof post.tipo_publicacion_id === 'object' ? post.tipo_publicacion_id.descripcion : ''}
fecha={post.fecha_publicacion || post.$createdAt}
imagenUrl={imagenUrl}
showCategory={cardSettings.showCategory}
showFooterDate={cardSettings.showFooterDate}
showFooterButton={cardSettings.showFooterButton}
footerText={cardSettings.footerText}
isAdmin={isAdmin}
/>
)
})}
</div>
)}
{/* Pagination Controls */}
<div className="flex justify-center items-center gap-4 mt-12">
{page > 1 ? (
<a href={`/?page=${page - 1}${adminMonth ? `&month=${adminMonth}` : ''}#noticias`} className="px-6 py-3 rounded-xl bg-slate-800/80 text-white font-bold hover:bg-slate-700 transition-colors border border-white/10">
Anterior
</a>
) : (
<div className="px-6 py-3 rounded-xl bg-slate-800/40 text-slate-500 font-bold border border-white/5 cursor-not-allowed">
Anterior
</div>
)}
<span className="text-slate-400 font-medium">Página {page} de {totalPages}</span>
{page < totalPages ? (
<a href={`/?page=${page + 1}${adminMonth ? `&month=${adminMonth}` : ''}#noticias`} className="px-6 py-3 rounded-xl bg-cyan-600 text-white font-bold hover:bg-cyan-500 transition-colors shadow-[0_0_15px_rgba(6,182,212,0.3)]">
Siguiente
</a>
) : (
<div className="px-6 py-3 rounded-xl bg-slate-800/40 text-slate-500 font-bold border border-white/5 cursor-not-allowed">
Siguiente
</div>
)}
</div>
</div>
{hasSidebar && (
<aside className="w-full lg:w-[320px] xl:w-[360px] shrink-0 mt-8 lg:mt-0 sticky top-24">
<GlobalSidebar />
</aside>
)}
</div>
</section>
</main>
);
}