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
170 lines
9.7 KiB
TypeScript
170 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useRef } from 'react';
|
|
|
|
export default function ProgrammingSchedule({ matrixData }: { matrixData: Record<string, string> }) {
|
|
const [currentTime, setCurrentTime] = useState(new Date());
|
|
const [viewMode, setViewMode] = useState<'day' | 'week'>('day');
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
// Actualizar cada minuto cada que cambie la hora
|
|
const interval = setInterval(() => setCurrentTime(new Date()), 60000);
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
const allDays = [
|
|
{ key: 'mon', label: 'Lunes', index: 1 },
|
|
{ key: 'tue', label: 'Martes', index: 2 },
|
|
{ key: 'wed', label: 'Miércoles', index: 3 },
|
|
{ key: 'thu', label: 'Jueves', index: 4 },
|
|
{ key: 'fri', label: 'Viernes', index: 5 },
|
|
{ key: 'sat', label: 'Sábado', index: 6 },
|
|
{ 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 h = Math.floor(i/2).toString().padStart(2, '0');
|
|
const m = i % 2 === 0 ? '00' : '30';
|
|
return `${h}:${m}`;
|
|
});
|
|
|
|
const currentHour = currentTime.getHours();
|
|
const currentMinute = currentTime.getMinutes();
|
|
const currentSlotIndex = currentHour * 2 + (currentMinute >= 30 ? 1 : 0);
|
|
|
|
// Calc rowSpans to unify cells visually
|
|
const spans: Record<string, number[]> = {};
|
|
allDays.forEach(d => {
|
|
const list = slots.map(time => matrixData[`${time}_${d.key}`] || '');
|
|
const spanList = new Array(48).fill(1);
|
|
for(let i=0; i<48; i++){
|
|
if (spanList[i] === 0) continue;
|
|
let count = 1;
|
|
const val = list[i];
|
|
while(i + count < 48 && list[i + count] === val && val !== '') {
|
|
spanList[i + count] = 0;
|
|
count++;
|
|
}
|
|
spanList[i] = count;
|
|
}
|
|
spans[d.key] = spanList;
|
|
});
|
|
|
|
// Auto-scroll a la hora actual al cargar
|
|
useEffect(() => {
|
|
if (containerRef.current) {
|
|
const rowHeight = 40; // approx
|
|
const scrollAmnt = Math.max(0, (currentSlotIndex - 3) * rowHeight);
|
|
containerRef.current.scrollTop = scrollAmnt;
|
|
}
|
|
}, [currentSlotIndex, viewMode]);
|
|
|
|
return (
|
|
<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 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">
|
|
<span className="w-3 h-3 rounded-full bg-rose-500 animate-pulse shadow-[0_0_10px_#f43f5e]"></span>
|
|
Parrilla de Programación
|
|
</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 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-[300px] md:min-w-[900px] relative">
|
|
<thead className="sticky top-0 z-30 ring-1 ring-slate-800">
|
|
<tr>
|
|
<th className="p-3 w-16 md:w-20 text-center bg-slate-950/95 backdrop-blur-md">
|
|
<span className="text-[10px] font-black tracking-widest text-slate-500 uppercase">Hora</span>
|
|
</th>
|
|
{days.map(d => {
|
|
const isToday = d.index === currentDayIndex;
|
|
return (
|
|
<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>
|
|
{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>
|
|
)
|
|
})}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="relative">
|
|
{slots.map((time, idx) => {
|
|
const isHalfHour = time.endsWith('30');
|
|
const isCurrentSlot = idx === currentSlotIndex;
|
|
|
|
return (
|
|
<tr key={idx} className={`group transition-colors relative ${isCurrentSlot ? 'bg-rose-950/10' : ''}`}>
|
|
|
|
{/* Indicador de Línea de Tiempo Roja */}
|
|
{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"
|
|
style={{ top: `${((currentMinute % 30) / 30) * 100}%` }}></td>
|
|
)}
|
|
|
|
<td className={`p-2 border-r border-slate-800 bg-slate-950 text-center sticky left-0 z-20 ${isHalfHour ? 'border-b border-dashed border-slate-800/50' : 'border-b border-solid'}`}>
|
|
<span className={`font-mono text-xs tracking-widest font-black ${isCurrentSlot ? 'text-rose-400' : isHalfHour ? 'text-slate-600' : 'text-slate-400'}`}>
|
|
{time}
|
|
</span>
|
|
</td>
|
|
|
|
{days.map(d => {
|
|
const span = spans[d.key][idx];
|
|
if (span === 0) return null; // Es parte de un bloque unificado, no se renderiza
|
|
|
|
const program = matrixData[`${time}_${d.key}`];
|
|
const isToday = d.index === currentDayIndex;
|
|
|
|
// Es en vivo si la ranura actual recae DENTRO del span unificado
|
|
const isLiveNow = isToday && currentSlotIndex >= idx && currentSlotIndex < idx + span;
|
|
|
|
return (
|
|
<td key={d.key} rowSpan={span} className={`
|
|
p-2 border-r border-slate-800/50 relative text-center
|
|
${isHalfHour && span === 1 ? 'border-b border-dashed border-slate-800/20' : 'border-b border-solid'}
|
|
${isLiveNow ? 'bg-rose-900/30 ring-1 ring-inset ring-rose-500 shadow-inner' : isToday ? 'bg-slate-800/30 hover:bg-slate-800/50 -z-10' : 'hover:bg-slate-800/40'}
|
|
`}>
|
|
{isLiveNow && (
|
|
<div className="absolute top-1 right-1 flex items-center gap-1 bg-rose-500 justify-center text-white text-[9px] font-black tracking-widest uppercase px-1.5 rounded animate-pulse z-10 shadow-lg">
|
|
Al Aire
|
|
</div>
|
|
)}
|
|
<div className={`text-sm font-bold flex flex-col items-center justify-center h-full min-h-[30px] w-full ${isLiveNow ? 'text-white drop-shadow-[0_0_8px_rgba(255,255,255,0.7)]' : program ? 'text-slate-300' : 'text-slate-700/50 font-normal italic'}`}>
|
|
<span>{program || '-'}</span>
|
|
{span > 1 && program && <span className="text-[10px] text-cyan-400 font-black tracking-widest uppercase mt-1 opacity-70">({span * 30} MINS)</span>}
|
|
</div>
|
|
</td>
|
|
)
|
|
})}
|
|
</tr>
|
|
)
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|