modified: src/app/[slug]/page.tsx

modified:   src/app/clasificados/page.tsx
modified:   src/app/dashboard/[entity]/[id]/page.tsx
modified:   src/app/dashboard/[entity]/new/page.tsx
modified:   src/app/dashboard/[entity]/page.tsx
modified:   src/app/dashboard/entity/page.tsx
modified:   src/app/dashboard/profile/page.tsx
modified:   src/app/dashboard/settings/page.tsx
modified:   src/app/eventos/page.tsx
modified:   src/app/globals.css
modified:   src/app/page.tsx
modified:   src/components/NewsTicker.tsx
modified:   src/components/ProgrammingSchedule.tsx
This commit is contained in:
Carlos (Almeira) 2026-03-27 06:51:16 -05:00
parent 80a6f77b74
commit e3024af88b
13 changed files with 203 additions and 100 deletions

View File

@ -35,8 +35,9 @@ async function fetchPostData(slugOrId: string) {
} }
} }
export default async function NoticiaDetail({ params }: { params: { slug: string } }) { export default async function NoticiaDetail({ params }: { params: Promise<{ slug: string }> }) {
const post = await fetchPostData(params.slug); const { slug } = await params;
const post = await fetchPostData(slug);
const hasSidebar = await hasSidebarContent(); const hasSidebar = await hasSidebarContent();
if (!post) { if (!post) {

View File

@ -13,10 +13,10 @@ async function fetchClasificadosData() {
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []); const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {}; const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
const postsCount = settings.classifieds_count || 9; const postsCount = settings.feed_news_count || 12;
const cardSettings = { const cardSettings = {
showCategory: settings.news_card_show_category ?? false, showCategory: settings.news_card_show_category ?? false,
showFooterDate: settings.news_card_show_footer_date ?? true, showFooterDate: settings.show_news_date ?? false,
showFooterButton: settings.news_card_show_footer_button ?? true, showFooterButton: settings.news_card_show_footer_button ?? true,
footerText: settings.news_card_footer_text || "" footerText: settings.news_card_footer_text || ""
}; };

View File

@ -5,7 +5,7 @@ import Link from 'next/link';
import { updateDocumentServer } from '@/app/actions/appwrite'; import { updateDocumentServer } from '@/app/actions/appwrite';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export default async function EditEntityPage({ params }: { params: { entity: string, id: string } }) { export default async function EditEntityPage({ params }: { params: Promise<{ entity: string, id: string }> }) {
const { entity, id } = await params; const { entity, id } = await params;
const collectionId = collectionMap[entity]; const collectionId = collectionMap[entity];

View File

@ -5,7 +5,7 @@ import Link from 'next/link';
import { createDocumentServer } from '@/app/actions/appwrite'; import { createDocumentServer } from '@/app/actions/appwrite';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export default async function NewEntityPage({ params }: { params: { entity: string } }) { export default async function NewEntityPage({ params }: { params: Promise<{ entity: string }> }) {
const { entity } = await params; const { entity } = await params;
const collectionId = collectionMap[entity]; const collectionId = collectionMap[entity];

View File

@ -11,8 +11,8 @@ export default async function EntityPage({
params, params,
searchParams searchParams
}: { }: {
params: { entity: string }, params: Promise<{ entity: string }>,
searchParams: { q?: string, page?: string } searchParams: Promise<{ q?: string, page?: string }>
}) { }) {
const { entity } = await params; const { entity } = await params;
const sParams = await searchParams; const sParams = await searchParams;

View File

@ -1,13 +1,26 @@
import { getEntity, updateEntity } from '@/app/actions/entity'; import { getEntity, updateEntity } from '@/app/actions/entity';
import { Building, Image as ImageIcon, Link as LinkIcon, Share2, Tv, RadioTower } from 'lucide-react'; import { Building, Image as ImageIcon, Link as LinkIcon, Share2, Tv, RadioTower, Facebook, Instagram, Twitter, Youtube, Send, Mail, Globe, MapPin, Phone, MessageSquare } from 'lucide-react';
import { createSessionClient } from '@/lib/appwrite.server';
import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
export default async function EntityPage() { export default async function EntityPage() {
let entity: any = null; let entity: any = null;
let cities: any[] = [];
try { try {
entity = await getEntity(); entity = await getEntity();
const sessionClient = await createSessionClient();
if (sessionClient) {
const citiesRes = await sessionClient.databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'cities',
[]
);
cities = citiesRes.documents;
}
} catch (e) { } catch (e) {
console.error("No se pudo cargar config de entidad:", e); console.error("No se pudo cargar la información de la entidad o ciudades:", e);
} }
async function updateEntityClient(formData: FormData) { async function updateEntityClient(formData: FormData) {
@ -17,7 +30,7 @@ export default async function EntityPage() {
return ( return (
<div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12"> <div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12">
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl"> <div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5">
<div className="p-3 bg-blue-500/10 rounded-xl text-blue-400"> <div className="p-3 bg-blue-500/10 rounded-xl text-blue-400">
<Building size={32} /> <Building size={32} />
</div> </div>
@ -25,7 +38,7 @@ export default async function EntityPage() {
<h1 className="text-3xl font-black font-outfit uppercase tracking-tighter text-white"> <h1 className="text-3xl font-black font-outfit uppercase tracking-tighter text-white">
Perfil de Entidad Perfil de Entidad
</h1> </h1>
<p className="text-sm font-medium text-slate-400">Identidad Corporativa, Contacto y Canales de Streaming</p> <p className="text-sm font-medium text-slate-400 font-bold uppercase tracking-wider">Identidad Corporativa, Contacto y Canales</p>
</div> </div>
</div> </div>
@ -36,7 +49,7 @@ export default async function EntityPage() {
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><ImageIcon size={18} className="text-cyan-400" /> Identidad Visual</h3> <h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><ImageIcon size={18} className="text-cyan-400" /> Identidad Visual</h3>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Razón Social / Nombre Entidad *</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Razón Social / Nombre Entidad *</label>
<input <input
type="text" type="text"
name="razon_social" name="razon_social"
@ -47,7 +60,7 @@ export default async function EntityPage() {
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Sube el Logotipo (Dejar vacío para conservar el actual)</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Logotipo Corporativo</label>
{entity?.logotipo && ( {entity?.logotipo && (
<div className="mb-3 p-3 bg-slate-950 rounded-xl inline-block border border-slate-800"> <div className="mb-3 p-3 bg-slate-950 rounded-xl inline-block border border-slate-800">
<img src={entity.logotipo} alt="Logo actual" className="h-16 object-contain" /> <img src={entity.logotipo} alt="Logo actual" className="h-16 object-contain" />
@ -57,7 +70,7 @@ export default async function EntityPage() {
type="file" type="file"
name="logotipo" name="logotipo"
accept="image/*" accept="image/*"
className="w-full bg-slate-950/50 border border-slate-800 rounded-xl file:mr-4 file:py-3 file:px-4 file:rounded-l-xl file:border-0 file:text-sm file:font-semibold file:bg-cyan-500/10 file:text-cyan-400 hover:file:bg-cyan-500/20 text-slate-300 text-sm transition-all" className="w-full bg-slate-950/50 border border-slate-800 rounded-xl file:mr-4 file:py-3 file:px-4 file:rounded-l-xl file:border-0 file:text-[10px] file:font-black file:uppercase file:bg-cyan-500/10 file:text-cyan-400 hover:file:bg-cyan-500/20 text-slate-300 text-sm transition-all"
/> />
</div> </div>
@ -71,36 +84,55 @@ export default async function EntityPage() {
{/* Contacto Físico */} {/* Contacto Físico */}
<div className="space-y-6"> <div className="space-y-6">
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><LinkIcon size={18} className="text-green-400" /> Contacto Oficial</h3> <h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><MapPin size={18} className="text-green-400" /> Contacto Oficial</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Ciudad</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Ciudad</label>
<input type="text" name="ciudad" defaultValue={entity?.ciudad || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <select
name="ciudad"
defaultValue={entity?.ciudad || ''}
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all"
>
<option value="">Seleccionar Ciudad...</option>
{cities.map(city => (
<option key={city.$id} value={city.descripcion}>{city.descripcion}</option>
))}
</select>
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Dirección Físico</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Dirección Física</label>
<input type="text" name="direccion" defaultValue={entity?.direccion || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <input type="text" name="direccion" defaultValue={entity?.direccion || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Teléfono</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Teléfono</label>
<input type="text" name="numero_contacto" defaultValue={entity?.numero_contacto || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <input type="text" name="numero_contacto" defaultValue={entity?.numero_contacto || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">WhatsApp</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">WhatsApp</label>
<input type="text" name="numero_whatsapp" defaultValue={entity?.numero_whatsapp || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <input type="text" name="numero_whatsapp" defaultValue={entity?.numero_whatsapp || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4">
<div> {/* Email y Sitio Web en líneas separadas como solicitó el usuario */}
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Email</label> <div>
<input type="email" name="email" defaultValue={entity?.email || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Email Corporativo</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-600 group-focus-within:text-cyan-500 transition-colors">
<Mail size={18} />
</div>
<input type="email" name="email" defaultValue={entity?.email || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-3 text-white focus:border-cyan-500 transition-all font-medium" />
</div> </div>
<div> </div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Sitio Web URL</label> <div>
<input type="url" name="sitio_web" defaultValue={entity?.sitio_web || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2 text-white focus:border-cyan-500 transition-all" /> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Sitio Web URL</label>
<div className="relative group">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-600 group-focus-within:text-cyan-500 transition-colors">
<Globe size={18} />
</div>
<input type="url" name="sitio_web" defaultValue={entity?.sitio_web || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl pl-10 pr-4 py-3 text-white focus:border-cyan-500 transition-all font-medium" />
</div> </div>
</div> </div>
</div> </div>
@ -111,7 +143,7 @@ export default async function EntityPage() {
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Tv size={18} className="text-rose-400" /> Canales en Vivo (Streaming)</h3> <h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Tv size={18} className="text-rose-400" /> Canales en Vivo (Streaming)</h3>
<div className="grid md:grid-cols-2 gap-8"> <div className="grid md:grid-cols-2 gap-8">
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-cyan-400">URL Streaming TV (YouTube/HLS/iFrame Source)</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2 text-cyan-400">URL Streaming TV</label>
<input <input
type="text" type="text"
name="tv_url" name="tv_url"
@ -119,10 +151,9 @@ export default async function EntityPage() {
placeholder="https://www.youtube.com/embed/..." placeholder="https://www.youtube.com/embed/..."
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 transition-all font-mono text-sm" className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 transition-all font-mono text-sm"
/> />
<p className="text-[10px] text-slate-500 mt-2">Esta URL se usará para el reproductor principal de la sección TV.</p>
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-rose-400">URL Streaming Radio (Audio Stream / Icecast)</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2 text-rose-400">URL Streaming Radio (Audio Stream)</label>
<input <input
type="text" type="text"
name="radio_url" name="radio_url"
@ -130,43 +161,52 @@ export default async function EntityPage() {
placeholder="https://stream.server.com:8000/live" placeholder="https://stream.server.com:8000/live"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-rose-500 transition-all font-mono text-sm" className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-rose-500 transition-all font-mono text-sm"
/> />
<p className="text-[10px] text-slate-500 mt-2">Enlace directo al flujo de audio para la sección Radio.</p>
</div> </div>
</div> </div>
</div> </div>
{/* Redes Sociales */} {/* Redes Sociales con Iconos */}
<div className="space-y-6 pt-4 border-t border-slate-800"> <div className="space-y-6 pt-4 border-t border-slate-800">
<h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Share2 size={18} className="text-purple-400" /> Vínculos Sociales</h3> <h3 className="text-lg font-bold text-white flex items-center gap-2 border-b border-slate-800 pb-2"><Share2 size={18} className="text-purple-400" /> Vínculos Sociales</h3>
<div className="grid md:grid-cols-5 gap-4"> <div className="grid md:grid-cols-5 gap-4">
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-blue-500">Facebook</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-blue-500">
<Facebook size={12} /> Facebook
</label>
<input type="url" name="facebook_url" defaultValue={entity?.facebook_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-blue-500 text-sm transition-all" placeholder="https://..." /> <input type="url" name="facebook_url" defaultValue={entity?.facebook_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-blue-500 text-sm transition-all" placeholder="https://..." />
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-pink-500">Instagram</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-pink-500">
<Instagram size={12} /> Instagram
</label>
<input type="url" name="instagram_url" defaultValue={entity?.instagram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-pink-500 text-sm transition-all" placeholder="https://..." /> <input type="url" name="instagram_url" defaultValue={entity?.instagram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-pink-500 text-sm transition-all" placeholder="https://..." />
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-slate-400">X (Twitter)</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-slate-200">
<Twitter size={12} /> X (Twitter)
</label>
<input type="url" name="x_url" defaultValue={entity?.x_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-slate-500 text-sm transition-all" placeholder="https://..." /> <input type="url" name="x_url" defaultValue={entity?.x_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-slate-500 text-sm transition-all" placeholder="https://..." />
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-red-500">YouTube</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-red-500">
<Youtube size={12} /> YouTube
</label>
<input type="url" name="youtube_url" defaultValue={entity?.youtube_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-red-500 text-sm transition-all" placeholder="https://..." /> <input type="url" name="youtube_url" defaultValue={entity?.youtube_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-red-500 text-sm transition-all" placeholder="https://..." />
</div> </div>
<div> <div>
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2 text-cyan-500">Telegram</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase flex items-center gap-2 mb-2 text-cyan-500">
<Send size={12} /> Telegram
</label>
<input type="url" name="telegram_url" defaultValue={entity?.telegram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-cyan-500 text-sm transition-all" placeholder="https://..." /> <input type="url" name="telegram_url" defaultValue={entity?.telegram_url || ''} className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-2.5 text-white focus:border-cyan-500 text-sm transition-all" placeholder="https://..." />
</div> </div>
</div> </div>
</div> </div>
<div className="pt-6 border-t border-slate-800"> <div className="pt-6 border-t border-slate-800">
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-8 rounded-xl transition-all border border-blue-500/50 hover:border-blue-400 ml-auto shadow-[0_0_20px_rgba(59,130,246,0.3)]"> <button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-12 rounded-xl transition-all border border-blue-500/50 hover:border-blue-400 ml-auto shadow-[0_0_20px_rgba(59,130,246,0.3)]">
<div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-indigo-500 opacity-90 group-hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 bg-gradient-to-r from-blue-600 to-indigo-500 opacity-90 group-hover:opacity-100 transition-opacity"></div>
<Building size={18} className="relative z-10" /> <Building size={18} className="relative z-10" />
<span className="relative z-10 text-sm">Guardar Perfil de Entidad</span> <span className="relative z-10 text-sm">Guardar</span>
</button> </button>
</div> </div>
</form> </form>

View File

@ -140,7 +140,7 @@ export default function ProfilePage() {
<button disabled={loading} type="submit" className="w-full relative group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-4 px-6 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 shadow-[0_0_20px_rgba(6,182,212,0.1)]"> <button disabled={loading} type="submit" className="w-full relative group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-4 px-6 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 shadow-[0_0_20px_rgba(6,182,212,0.1)]">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-600 opacity-80 group-hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-600 opacity-80 group-hover:opacity-100 transition-opacity"></div>
<span className="relative z-10 flex items-center justify-center gap-2"> <span className="relative z-10 flex items-center justify-center gap-2">
{loading ? <Loader className="animate-spin" size={18} /> : <>Guardar Cambios <ArrowRight size={18} /></>} {loading ? <Loader className="animate-spin" size={18} /> : <>Guardar <ArrowRight size={18} /></>}
</span> </span>
</button> </button>
</form> </form>

View File

@ -35,15 +35,26 @@ export default async function SettingsPage() {
const updateData = { const updateData = {
ticker_count: parseInt(formData.get('ticker_count') as string), ticker_count: parseInt(formData.get('ticker_count') as string),
rotation_seconds: parseInt(formData.get('rotation_seconds') as string) feed_news_count: parseInt(formData.get('feed_news_count') as string),
rotation_seconds: parseInt(formData.get('rotation_seconds') as string),
show_news_date: formData.get('show_news_date') === 'true'
}; };
await sessionClient.databases.updateDocument( if (formData.get('docId')) {
APPWRITE_ALMEIRA_CONFIG.databaseId, await sessionClient.databases.updateDocument(
'settings', APPWRITE_ALMEIRA_CONFIG.databaseId,
formData.get('docId') as string, 'settings',
updateData formData.get('docId') as string,
); updateData
);
} else {
await sessionClient.databases.createDocument(
APPWRITE_ALMEIRA_CONFIG.databaseId,
'settings',
'unique()',
updateData
);
}
revalidatePath('/dashboard/settings'); revalidatePath('/dashboard/settings');
revalidatePath('/'); // Revalidate frontpage revalidatePath('/'); // Revalidate frontpage
@ -56,7 +67,7 @@ export default async function SettingsPage() {
return ( return (
<div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12"> <div className="max-w-5xl mx-auto space-y-6 animate-fade-in pb-12">
<div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl"> <div className="flex items-center gap-4 bg-slate-900 border border-slate-800 p-6 rounded-2xl shadow-xl shadow-cyan-500/5">
<div className="p-3 bg-cyan-500/10 rounded-xl text-cyan-400"> <div className="p-3 bg-cyan-500/10 rounded-xl text-cyan-400">
<Settings size={32} /> <Settings size={32} />
</div> </div>
@ -75,58 +86,83 @@ export default async function SettingsPage() {
<input type="hidden" name="docId" value={docId} /> <input type="hidden" name="docId" value={docId} />
{/* Configurador del Ticker */} {/* Configurador del Ticker */}
<div className="space-y-4"> <div className="space-y-6">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 border-b border-slate-800 pb-2">
<Menu className="text-pink-500" /> <Menu className="text-cyan-500" size={20} />
<h2 className="text-xl font-bold uppercase tracking-widest text-white">Ticker de Noticias</h2> <h2 className="text-lg font-bold uppercase tracking-widest text-white">Noticias y Feed</h2>
</div> </div>
<p className="text-slate-400 text-sm h-12">
Gobierna el cintillo de 'Últimas Noticias' que rota bajo el encabezado superior estableciendo su límite histórico.
</p>
<div> <div className="grid grid-cols-2 gap-4">
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Cantidad a Mostrar</label> <div>
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Ticker (Cintillo)</label>
<input
type="number"
name="ticker_count"
defaultValue={settings.ticker_count || 10}
min="1" max="50"
required
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 text-xl font-black font-mono transition-all"
/>
</div>
<div>
<label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Feed (Inicio)</label>
<input
type="number"
name="feed_news_count"
defaultValue={(settings as any).feed_news_count || 12}
min="4" max="100"
required
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-400 text-xl font-black font-mono transition-all"
/>
</div>
</div>
<div className="flex items-center gap-3 bg-slate-950 p-4 border border-slate-800 rounded-xl hover:border-cyan-500/30 transition-colors">
<input <input
type="number" type="checkbox"
name="ticker_count" name="show_news_date"
defaultValue={settings.ticker_count} value="true"
min="1" max="50" defaultChecked={(settings as any).show_news_date === true}
required id="show_news_date"
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-cyan-500 text-xl font-black font-mono transition-all" className="w-5 h-5 accent-cyan-500 cursor-pointer"
/> />
<label htmlFor="show_news_date" className="text-sm font-bold text-slate-300 cursor-pointer select-none">
Mostrar fecha de publicación en las tarjetas del feed
</label>
</div> </div>
</div> </div>
{/* Configurador de Rotación (Banners) */} {/* Configurador de Rotación (Banners) */}
<div className="space-y-4"> <div className="space-y-6">
<div className="flex items-center gap-2 mb-4"> <div className="flex items-center gap-2 border-b border-slate-800 pb-2">
<Clock className="text-purple-500" /> <Clock className="text-purple-500" size={20} />
<h2 className="text-xl font-bold uppercase tracking-widest text-white">Velocidad Carrusel</h2> <h2 className="text-lg font-bold uppercase tracking-widest text-white">Visualización</h2>
</div> </div>
<p className="text-slate-400 text-sm h-12">
Regula el tiempo en pantalla antes de que cambie la lámina (Afecta Banner Principal y Sliders Patrocinados).
</p>
<div className="relative"> <div className="relative">
<label className="text-xs font-bold tracking-widest text-slate-300 uppercase block mb-2">Segundos en Pantalla</label> <label className="text-[10px] font-black tracking-widest text-slate-500 uppercase block mb-2">Segundos en Pantalla (Banners)</label>
<input <input
type="number" type="number"
name="rotation_seconds" name="rotation_seconds"
defaultValue={settings.rotation_seconds} defaultValue={settings.rotation_seconds || 5}
min="1" max="60" min="1" max="60"
required required
className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-purple-500 text-xl font-black font-mono transition-all" className="w-full bg-slate-950 border border-slate-800 rounded-xl px-4 py-3 text-white focus:border-purple-500 text-xl font-black font-mono transition-all"
/> />
<span className="absolute right-4 bottom-3 text-slate-500 font-bold tracking-widest uppercase text-sm">Segundos</span> <span className="absolute right-4 bottom-3 text-slate-600 font-bold tracking-widest uppercase text-[10px]">Segundos</span>
</div> </div>
<p className="text-[11px] text-slate-500 italic leading-relaxed">
Estas configuraciones afectan el comportamiento dinámico del sitio web principal y los tiempos de carga de recursos del lado del servidor.
</p>
</div> </div>
</div> </div>
<div className="pt-6 border-t border-slate-800"> <div className="pt-6 border-t border-slate-800">
<button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-8 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 ml-auto"> <button type="submit" className="w-full md:w-auto relative flex items-center gap-2 justify-center group overflow-hidden bg-slate-800 text-white font-black uppercase tracking-widest py-3 px-10 rounded-xl transition-all border border-cyan-500/50 hover:border-cyan-400 ml-auto shadow-[0_0_20px_rgba(6,182,212,0.2)]">
<div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-500 opacity-90 group-hover:opacity-100 transition-opacity"></div> <div className="absolute inset-0 bg-gradient-to-r from-cyan-600 to-blue-500 opacity-90 group-hover:opacity-100 transition-opacity"></div>
<Save size={18} className="relative z-10" /> <Save size={18} className="relative z-10" />
<span className="relative z-10 text-sm">Guardar Motor Web</span> <span className="relative z-10 text-sm">Guardar</span>
</button> </button>
</div> </div>
</form> </form>

View File

@ -13,10 +13,10 @@ async function fetchEventosData() {
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []); const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {}; const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
const postsCount = settings.events_count || 9; const postsCount = settings.feed_news_count || 12;
const cardSettings = { const cardSettings = {
showCategory: settings.news_card_show_category ?? false, showCategory: settings.news_card_show_category ?? false,
showFooterDate: settings.news_card_show_footer_date ?? true, showFooterDate: settings.show_news_date ?? false,
showFooterButton: settings.news_card_show_footer_button ?? true, showFooterButton: settings.news_card_show_footer_button ?? true,
footerText: settings.news_card_footer_text || "" footerText: settings.news_card_footer_text || ""
}; };

View File

@ -86,14 +86,17 @@
/* Scrollbar futurista */ /* Scrollbar futurista */
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 8px; width: 8px;
height: 8px;
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background: #020617; background: #020617;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: #1e293b; background: #06b6d4; /* Cyan */
border-radius: 4px; background: linear-gradient(180deg, #06b6d4 0%, #3b82f6 100%);
border-radius: 10px;
border: 2px solid #020617;
} }
::-webkit-scrollbar-thumb:hover { ::-webkit-scrollbar-thumb:hover {
background: #334155; background: #22d3ee;
} }

View File

@ -13,10 +13,10 @@ async function fetchHomeData() {
const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []); const settingsRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'settings', []);
const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {}; const settings: any = settingsRes.documents.length > 0 ? settingsRes.documents[0] : {};
const postsCount = settings.feed_count || settings.post_count || 9; const postsCount = settings.feed_news_count || 12;
const cardSettings = { const cardSettings = {
showCategory: settings.news_card_show_category ?? false, showCategory: settings.news_card_show_category ?? false,
showFooterDate: settings.news_card_show_footer_date ?? true, showFooterDate: settings.show_news_date ?? false,
showFooterButton: settings.news_card_show_footer_button ?? true, showFooterButton: settings.news_card_show_footer_button ?? true,
footerText: settings.news_card_footer_text || "" footerText: settings.news_card_footer_text || ""
}; };

View File

@ -42,16 +42,16 @@ export default async function NewsTicker() {
]); ]);
// Resolve Media // Resolve Media
const mediaIds = postsRes.documents.map(p => p.imagen_destacada_id).filter(id => id && typeof id === 'string'); const mediaIds = postsRes.documents.map((p: any) => p.imagen_destacada_id).filter((id: any) => id && typeof id === 'string');
const mediaMap = new Map(); const mediaMap = new Map();
if (mediaIds.length > 0) { if (mediaIds.length > 0) {
const mediaRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [ const mediaRes = await client.databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', [
Query.equal('$id', mediaIds) Query.equal('$id', mediaIds)
]); ]);
mediaRes.documents.forEach(m => mediaMap.set(m.$id, m)); mediaRes.documents.forEach((m: any) => mediaMap.set(m.$id, m));
} }
newsItems = postsRes.documents.map(post => { newsItems = postsRes.documents.map((post: any) => {
const media = mediaMap.get(post.imagen_destacada_id); const media = mediaMap.get(post.imagen_destacada_id);
let imagenUrl = post.og_image_url || null; let imagenUrl = post.og_image_url || null;
if (media) { if (media) {
@ -92,7 +92,7 @@ export default async function NewsTicker() {
> >
{noticia.imagenUrl && ( {noticia.imagenUrl && (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img src={noticia.imagenUrl} alt={noticia.titulo} className="w-10 h-10 object-cover rounded-lg shadow-[0_0_10px_rgba(6,182,212,0.3)] group-hover:scale-110 group-hover:shadow-[0_0_15px_rgba(6,182,212,0.5)] transition-all duration-300 border border-white/10" /> <img src={noticia.imagenUrl} alt={noticia.titulo} className="w-16 h-9 aspect-video object-cover rounded shadow-[0_0_10px_rgba(6,182,212,0.3)] group-hover:scale-105 group-hover:shadow-[0_0_15px_rgba(6,182,212,0.5)] transition-all duration-300 border border-white/10" />
)} )}
<span className="text-sm font-bold text-slate-300 group-hover:text-cyan-400 transition-colors uppercase tracking-widest flex items-center gap-2"> <span className="text-sm font-bold text-slate-300 group-hover:text-cyan-400 transition-colors uppercase tracking-widest flex items-center gap-2">
{!noticia.imagenUrl && <span className="w-2 h-2 rounded-full bg-rose-500 animate-pulse"></span>} {!noticia.imagenUrl && <span className="w-2 h-2 rounded-full bg-rose-500 animate-pulse"></span>}

View File

@ -4,15 +4,16 @@ import { useEffect, useState, useRef } from 'react';
export default function ProgrammingSchedule({ matrixData }: { matrixData: Record<string, string> }) { export default function ProgrammingSchedule({ matrixData }: { matrixData: Record<string, string> }) {
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [viewMode, setViewMode] = useState<'day' | 'week'>('day');
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
// Actualizar cada minuto // Actualizar cada minuto cada que cambie la hora
const interval = setInterval(() => setCurrentTime(new Date()), 60000); const interval = setInterval(() => setCurrentTime(new Date()), 60000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const days = [ const allDays = [
{ key: 'mon', label: 'Lunes', index: 1 }, { key: 'mon', label: 'Lunes', index: 1 },
{ key: 'tue', label: 'Martes', index: 2 }, { key: 'tue', label: 'Martes', index: 2 },
{ key: 'wed', label: 'Miércoles', index: 3 }, { key: 'wed', label: 'Miércoles', index: 3 },
@ -22,20 +23,26 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
{ key: 'sun', label: 'Domingo', index: 0 } { key: 'sun', label: 'Domingo', index: 0 }
]; ];
const currentDayIndex = currentTime.getDay();
// Si estamos en modo día, solo filtramos el día actual
const days = viewMode === 'day'
? allDays.filter(d => d.index === currentDayIndex)
: allDays;
const slots = Array.from({length: 48}).map((_, i) => { const slots = Array.from({length: 48}).map((_, i) => {
const h = Math.floor(i/2).toString().padStart(2, '0'); const h = Math.floor(i/2).toString().padStart(2, '0');
const m = i % 2 === 0 ? '00' : '30'; const m = i % 2 === 0 ? '00' : '30';
return `${h}:${m}`; return `${h}:${m}`;
}); });
const currentDayIndex = currentTime.getDay();
const currentHour = currentTime.getHours(); const currentHour = currentTime.getHours();
const currentMinute = currentTime.getMinutes(); const currentMinute = currentTime.getMinutes();
const currentSlotIndex = currentHour * 2 + (currentMinute >= 30 ? 1 : 0); const currentSlotIndex = currentHour * 2 + (currentMinute >= 30 ? 1 : 0);
// Calc rowSpans to unify cells visually // Calc rowSpans to unify cells visually
const spans: Record<string, number[]> = {}; const spans: Record<string, number[]> = {};
days.forEach(d => { allDays.forEach(d => {
const list = slots.map(time => matrixData[`${time}_${d.key}`] || ''); const list = slots.map(time => matrixData[`${time}_${d.key}`] || '');
const spanList = new Array(48).fill(1); const spanList = new Array(48).fill(1);
for(let i=0; i<48; i++){ for(let i=0; i<48; i++){
@ -58,30 +65,46 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
const scrollAmnt = Math.max(0, (currentSlotIndex - 3) * rowHeight); const scrollAmnt = Math.max(0, (currentSlotIndex - 3) * rowHeight);
containerRef.current.scrollTop = scrollAmnt; containerRef.current.scrollTop = scrollAmnt;
} }
}, [currentSlotIndex]); }, [currentSlotIndex, viewMode]);
return ( return (
<div className="w-full bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-2xl relative"> <div className="w-full bg-slate-900 border border-slate-800 rounded-3xl overflow-hidden shadow-2xl relative group/matrix">
<div className="p-4 bg-slate-950 border-b border-slate-800 flex justify-between items-center"> <div className="p-4 bg-slate-950 border-b border-slate-800 flex flex-col md:flex-row justify-between items-center gap-4">
<h3 className="text-xl font-black font-outfit uppercase tracking-tighter text-white flex items-center gap-3"> <h3 className="text-xl font-black font-outfit uppercase tracking-tighter text-white flex items-center gap-3">
<span className="w-3 h-3 rounded-full bg-rose-500 animate-pulse shadow-[0_0_10px_#f43f5e]"></span> <span className="w-3 h-3 rounded-full bg-rose-500 animate-pulse shadow-[0_0_10px_#f43f5e]"></span>
Parrilla de Programación Parrilla de Programación
</h3> </h3>
{/* Switch Futurista Día / Semana */}
<div className="flex items-center gap-2 bg-slate-900 p-1 rounded-xl border border-slate-800">
<button
onClick={() => setViewMode('day')}
className={`px-4 py-1.5 rounded-lg text-xs font-black uppercase tracking-widest transition-all ${viewMode === 'day' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.4)]' : 'text-slate-500 hover:text-slate-300'}`}
>
Hoy
</button>
<button
onClick={() => setViewMode('week')}
className={`px-4 py-1.5 rounded-lg text-xs font-black uppercase tracking-widest transition-all ${viewMode === 'week' ? 'bg-cyan-500 text-slate-950 shadow-[0_0_15px_rgba(6,182,212,0.4)]' : 'text-slate-500 hover:text-slate-300'}`}
>
Semana
</button>
</div>
</div> </div>
<div ref={containerRef} className="overflow-x-auto overflow-y-auto max-h-[600px] relative custom-scrollbar scroll-smooth"> <div ref={containerRef} className="overflow-x-auto overflow-y-auto max-h-[600px] relative custom-scrollbar scroll-smooth">
<table className="w-full text-left border-collapse min-w-[900px] relative"> <table className="w-full text-left border-collapse min-w-[300px] md:min-w-[900px] relative">
<thead className="sticky top-0 z-30 ring-1 ring-slate-800"> <thead className="sticky top-0 z-30 ring-1 ring-slate-800">
<tr> <tr>
<th className="p-3 w-20 text-center bg-slate-950/90 backdrop-blur-md"> <th className="p-3 w-16 md:w-20 text-center bg-slate-950/95 backdrop-blur-md">
<span className="text-xs font-black tracking-widest text-slate-500 uppercase">Hora</span> <span className="text-[10px] font-black tracking-widest text-slate-500 uppercase">Hora</span>
</th> </th>
{days.map(d => { {days.map(d => {
const isToday = d.index === currentDayIndex; const isToday = d.index === currentDayIndex;
return ( return (
<th key={d.key} className={`p-4 text-center border-b border-slate-800 bg-slate-900/90 backdrop-blur-md ${isToday ? 'text-cyan-400' : 'text-slate-300'}`}> <th key={d.key} className={`p-4 text-center border-b border-slate-800 bg-slate-900/95 backdrop-blur-md ${isToday ? 'text-cyan-400' : 'text-slate-300'}`}>
<span className={`text-sm tracking-widest uppercase font-black`}>{d.label}</span> <span className={`text-sm tracking-widest uppercase font-black`}>{d.label}</span>
{isToday && <div className="text-[10px] bg-cyan-500/20 text-cyan-300 rounded-full px-2 py-0.5 mt-1 inline-block animate-pulse">HOY</div>} {isToday && viewMode === 'week' && <div className="text-[9px] bg-cyan-500/20 text-cyan-300 rounded-full px-2 py-0.5 mt-1 inline-block animate-pulse">HOY</div>}
</th> </th>
) )
})} })}
@ -95,7 +118,7 @@ export default function ProgrammingSchedule({ matrixData }: { matrixData: Record
return ( return (
<tr key={idx} className={`group transition-colors relative ${isCurrentSlot ? 'bg-rose-950/10' : ''}`}> <tr key={idx} className={`group transition-colors relative ${isCurrentSlot ? 'bg-rose-950/10' : ''}`}>
{/* Indicador de Línea de Tiempo Rojo (Midiéndose internamente en sus 30 mins) */} {/* Indicador de Línea de Tiempo Roja */}
{isCurrentSlot && ( {isCurrentSlot && (
<td className="absolute left-0 right-0 h-0.5 bg-rose-500 z-20 pointer-events-none shadow-[0_0_10px_rgba(244,63,94,0.8)] transition-all ease-linear" <td className="absolute left-0 right-0 h-0.5 bg-rose-500 z-20 pointer-events-none shadow-[0_0_10px_rgba(244,63,94,0.8)] transition-all ease-linear"
style={{ top: `${((currentMinute % 30) / 30) * 100}%` }}></td> style={{ top: `${((currentMinute % 30) / 30) * 100}%` }}></td>