Added countdown to each card
This commit is contained in:
parent
883b3d93f7
commit
ee830965db
25
app/page.tsx
25
app/page.tsx
|
|
@ -1,25 +1,10 @@
|
||||||
import EventCard from "@/components/EventCard";
|
// app/page.tsx
|
||||||
import dynamic from 'next/dynamic';
|
import MissionControl from '@/components/MissionControl';
|
||||||
|
|
||||||
const Starfield = dynamic(() => import('@/components/Starfield'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen flex flex-col items-center justify-center p-8">
|
<main>
|
||||||
<Starfield />
|
<MissionControl />
|
||||||
|
|
||||||
<h1 className="text-white font-mono text-2xl mb-12 tracking-[0.2em] uppercase">
|
|
||||||
Mission Control // Ground Station
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-4xl">
|
|
||||||
<EventCard
|
|
||||||
title="ISS Pass: Home"
|
|
||||||
targetDate={new Date()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,32 +1,67 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { motion } from "framer-motion";
|
import { useState, useEffect } from 'react';
|
||||||
import { Rocket } from "lucide-react";
|
import { EventCardProps } from '@/types/space';
|
||||||
import { EventCardProps } from "@/types/space"
|
|
||||||
|
|
||||||
|
|
||||||
export default function EventCard({ title, targetDate, icon }: EventCardProps) {
|
export default function EventCard({ title, targetDate, icon }: EventCardProps) {
|
||||||
|
// 1. Setup the state to hold our strings
|
||||||
|
const [timeLeft, setTimeLeft] = useState({ h: "00", m: "00", s: "00" });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTimer = () => {
|
||||||
|
const now = new Date().getTime();
|
||||||
|
const distance = targetDate.getTime() - now;
|
||||||
|
|
||||||
|
// If the time has passed, keep it at zero
|
||||||
|
if (distance < 0) {
|
||||||
|
setTimeLeft({ h: "00", m: "00", s: "00" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Math to convert milliseconds to Hours, Minutes, and Seconds
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 2. Update the state (padStart ensures we always see '05' instead of '5')
|
||||||
|
setTimeLeft({
|
||||||
|
h: h.toString().padStart(2, '0'),
|
||||||
|
m: m.toString().padStart(2, '0'),
|
||||||
|
s: s.toString().padStart(2, '0')
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run the timer every 1000ms (1 second)
|
||||||
|
const interval = setInterval(updateTimer, 1000);
|
||||||
|
|
||||||
|
// Call it once immediately so the user doesn't see 00:00:00 for the first second
|
||||||
|
updateTimer();
|
||||||
|
|
||||||
|
// Important: Clean up the timer if the component disappears
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [targetDate]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<div className="relative p-6 rounded-xl border border-slate-800 bg-slate-900/40 backdrop-blur-md overflow-hidden group">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
animate={{ opacity: 1, y: 0 }}
|
|
||||||
className="relative p-6 rounded-xl border border-slate-800 bg-slate-900/50 backdrop-blur-md overflow-hidden group"
|
|
||||||
>
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/20 to-purple-500/20 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none" />
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-4 mb-4">
|
||||||
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-400">
|
<div className="p-2 bg-blue-500/10 rounded-lg text-blue-400">
|
||||||
{icon || <Rocket size={20} />}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-slate-300 font-mono tracking-widest uppercase text-sm">{title}</h3>
|
<h3 className="text-slate-300 font-mono tracking-widest uppercase text-xs">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-4xl font-mono text-white flex gap-2">
|
<div className="text-4xl font-mono text-white flex gap-2">
|
||||||
<span>02</span><span className="text-slate-500">:</span>
|
<span>{timeLeft.h}</span>
|
||||||
<span>14</span><span className="text-slate-500">:</span>
|
<span className="text-slate-500">:</span>
|
||||||
<span>55</span>
|
<span>{timeLeft.m}</span>
|
||||||
|
<span className="text-slate-500">:</span>
|
||||||
|
<span>{timeLeft.s}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-xs text-slate-500 mt-2 font-mono uppercase">T-Minus to Horizon</p>
|
<p className="text-[10px] text-slate-500 mt-2 font-mono uppercase tracking-widest leading-none">
|
||||||
</motion.div>
|
T-Minus to Horizon
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
65
components/MissionControl.tsx
Normal file
65
components/MissionControl.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import EventCard from '@/components/EventCard';
|
||||||
|
import { Satellite, Rocket, Moon, Sparkles } from 'lucide-react';
|
||||||
|
|
||||||
|
// Now we can safely call dynamic with ssr:false here
|
||||||
|
const Starfield = dynamic(() => import('@/components/Starfield'), {
|
||||||
|
ssr: false
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function MissionControl() {
|
||||||
|
// Use the BASE_TIME from your terminal (Jan 2026 value)
|
||||||
|
const BASE_TIME = 1769610273000;
|
||||||
|
|
||||||
|
const events = useMemo(() => [
|
||||||
|
{
|
||||||
|
title: "ISS Overhead: Home",
|
||||||
|
date: new Date(BASE_TIME + 1000 * 60 * 60 * 2),
|
||||||
|
icon: <Satellite size={20} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Next Lunar Phase: Full",
|
||||||
|
date: new Date(BASE_TIME + 1000 * 60 * 60 * 24 * 3),
|
||||||
|
icon: <Moon size={20} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Starlink Train",
|
||||||
|
date: new Date(BASE_TIME + 1000 * 60 * 45),
|
||||||
|
icon: <Rocket size={20} />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Meteor Shower",
|
||||||
|
date: new Date(BASE_TIME + 1000 * 60 * 60 * 24 * 10),
|
||||||
|
icon: <Sparkles size={20} />,
|
||||||
|
}
|
||||||
|
], []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative min-h-screen flex flex-col items-center p-8 overflow-hidden">
|
||||||
|
<Starfield />
|
||||||
|
|
||||||
|
<header className="z-10 text-center mb-16 mt-10">
|
||||||
|
<h1 className="text-4xl md:text-6xl font-mono font-bold text-white tracking-tighter mb-4 uppercase">
|
||||||
|
Mission<span className="text-blue-500 font-black">Control</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-400 font-mono text-xs tracking-[0.3em] uppercase opacity-70">
|
||||||
|
Ground Station // [55.6761° N, 12.5683° E]
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="z-10 grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-5xl">
|
||||||
|
{events.map((event, index) => (
|
||||||
|
<EventCard
|
||||||
|
key={index}
|
||||||
|
title={event.title}
|
||||||
|
targetDate={event.date}
|
||||||
|
icon={event.icon}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue