mission-control/components/EventCard.tsx
2026-01-29 15:51:38 +01:00

173 lines
6.4 KiB
TypeScript

"use client";
import React from 'react';
import { useState, useEffect, useRef } from 'react';
import { EventCardProps } from '@/types/space';
import { useRouter } from 'next/navigation';
import { motion, AnimatePresence } from 'framer-motion';
import { LucideProps } from 'lucide-react';
export default function EventCard({ id, title, targetDate, endDate, icon }: EventCardProps) {
const [isLive, setIsLive] = useState(false);
const router = useRouter();
const [timeLeft, setTimeLeft] = useState({ d: "00", h: "00", m: "00", s: "00" });
const lastRefreshedRef = useRef<string | null>(null);
useEffect(() => {
const updateTimer = () => {
if (!targetDate || !endDate) return;
const now = new Date();
const timeToStart = targetDate.getTime() - now.getTime();
const timeToEnd = endDate.getTime() - now.getTime();
if (timeToStart <= 0 && timeToEnd > 0) {
if (!isLive) setIsLive(true);
return;
}
if (timeToEnd <= 0) {
if (isLive) setIsLive(false);
const passId = endDate.toISOString();
if (lastRefreshedRef.current !== passId) {
const isStaleDataFromServer = endDate.getTime() < (new Date().getTime() - 5000);
if (!isStaleDataFromServer) {
console.log("🛰️ Pass complete. Syncing with Mission Control...");
lastRefreshedRef.current = passId;
router.refresh();
} else {
console.warn("⚠️ Server returned stale ISS data. Standing by for worker update...");
lastRefreshedRef.current = passId;
}
}
return;
}
const distance = timeToStart;
const d = Math.floor(distance / (1000 * 60 * 60 * 24));
const h = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
const m = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
const s = Math.floor((distance % (1000 * 60)) / 1000);
setTimeLeft({
d: d.toString().padStart(2, '0'),
h: h.toString().padStart(2, '0'),
m: m.toString().padStart(2, '0'),
s: s.toString().padStart(2, '0')
});
};
const interval = setInterval(updateTimer, 1000);
updateTimer();
return () => clearInterval(interval);
}, [targetDate, endDate, isLive, router]);
if (!targetDate) {
return (
<div className="relative p-6 rounded-xl border border-slate-800 bg-slate-900/20 backdrop-blur-md overflow-hidden opacity-60">
<div className="flex items-center gap-4 mb-4">
<div className="p-2 bg-slate-700/30 rounded-lg text-slate-500">
{icon}
</div>
<h3 className="text-slate-500 font-mono tracking-widest uppercase text-xs">
{title}
</h3>
</div>
<div className="py-2">
<div className="h-8 w-48 bg-slate-800 animate-pulse rounded" />
<p className="text-[10px] text-slate-600 mt-4 font-mono uppercase tracking-widest italic">
Waiting for orbital telemetry...
</p>
</div>
</div>
);
}
return (
<div className={`relative p-6 rounded-xl border transition-all duration-500 overflow-hidden h-full ${isLive ? 'border-blue-500 bg-blue-900/20 shadow-[0_0_20px_rgba(59,130,246,0.3)]' : 'border-slate-800 bg-slate-900/40'}`}>
{isLive && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: [0.1, 0.3, 0.1] }}
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
className="absolute inset-0 bg-blue-500 pointer-events-none"
/>
)}
<div className="relative z-10 h-[120px] flex flex-col justify-between">
{isLive ? (
<div className="flex flex-col items-center justify-center h-full relative z-10">
<motion.div
animate={{
scale: [1, 1.3, 1],
filter: ["drop-shadow(0 0 0px #fbbf24)", "drop-shadow(0 0 8px #fbbf24)", "drop-shadow(0 0 0px #fbbf24)"]
}}
transition={{ duration: 3, repeat: Infinity }}
className="text-yellow-400 mb-3"
>
{React.isValidElement(icon)
? React.cloneElement(icon as React.ReactElement<LucideProps>, { size: 35 })
: icon}
</motion.div>
<h3 className="text-lg font-bold text-white text-center leading-tight">
{id === 'iss' ? 'Look Up!' : 'Event in Progress'}
</h3>
<p className="text-blue-200 text-[14px] text-center mt-2 max-w-[220px] leading-tight opacity-90">
{id === 'iss'
? 'The ISS is directly above. Give them a wave! 👋'
: `The ${title} is occurring right now.`}
</p>
</div>
) : (
<>
<div className="flex items-center gap-4">
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-400">{icon}</div>
<h3 className="text-slate-300 font-mono tracking-widest uppercase text-[10px] truncate">
{title}
</h3>
</div>
<div className="text-4xl font-mono text-white flex items-baseline gap-2 py-2">
{parseInt(timeLeft.d) >= 2 ? (
<>
<span>{timeLeft.d}</span>
<span className="text-slate-500 text-xl lowercase">Days</span>
</>
) : (
<div className="flex gap-1">
<span>{timeLeft.h}</span><span className="text-slate-500">:</span>
<span>{timeLeft.m}</span><span className="text-slate-500">:</span>
<div className="w-[1ch] relative">
<AnimatePresence mode="popLayout">
<motion.span
key={timeLeft.s}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
className="absolute"
>
{timeLeft.s}
</motion.span>
</AnimatePresence>
</div>
</div>
)}
</div>
<p className="text-[10px] text-slate-500 font-mono uppercase tracking-widest leading-none">
{id === 'iss' ? "T-Minus to Horizon" : id === 'launch' ? "T-Minus to Ignition" : id === 'moon' ? "Until Lunar Phase" : "Days to Peak Phase"}
</p>
</>
)}
</div>
</div>
);
}