Added labels and versioning

This commit is contained in:
GeorgeWebberley 2026-01-29 15:51:38 +01:00
parent 341763baaf
commit 5e6ac733c8
6 changed files with 148 additions and 74 deletions

View file

@ -2,14 +2,15 @@ variables:
- &app_name "mission-control"
when:
event: [push]
branch: main
- event: release
steps:
build-web:
image: woodpeckerci/plugin-docker-buildx
privileged: true
settings:
build_args:
APP_VERSION: ${CI_COMMIT_TAG}
platforms: linux/amd64
registry: git.georgew.dev
repo: git.georgew.dev/georgew/mission-control-web

View file

@ -1,7 +1,11 @@
ARG APP_VERSION
FROM --platform=linux/amd64 node:20-bookworm AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
ARG APP_VERSION
ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION
COPY . .
RUN npm run build

View file

@ -2,6 +2,9 @@ export const dynamic = 'force-dynamic';
import MissionControl from '@/components/MissionControl';
import db from '@/lib/db';
import Footer from '@/components/Footer';
import Starfield from '@/components/Starfield';
export default function Home() {
const issRow = db.prepare(`
@ -40,13 +43,23 @@ export default function Home() {
const cosmicEnd = cosmicStart ? new Date(cosmicStart.getTime() + 14400000) : null;
return (
<main>
<main className="min-h-screen flex flex-col items-center mt-8 px-4 relative">
<Starfield />
<header className="z-10 text-center mb-12 mt-16">
<h1 className="text-4xl md:text-6xl font-mono font-bold text-white tracking-tighter mb-4 uppercase">
Georgew<span className="text-blue-500 font-black">Observatory</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>
<MissionControl
iss={{ start: issStart, end: issEnd }}
moon={{ title: moonRow?.title || "Moon Phase", start: moonStart, end: moonEnd }}
cosmic={{ title: cosmicRow?.title || "Cosmic Event", start: cosmicStart, end: cosmicEnd }}
launch={{ title: launchRow?.title || "Rocket Launch", start: launchStart, end: launchEnd }}
/>
<Footer />
</main>
);
}

View file

@ -1,8 +1,10 @@
"use client";
import React from 'react';
import { useState, useEffect, useRef } from 'react';
import { EventCardProps } from '@/types/space';
import { useRouter } from 'next/navigation';
import { Sparkles } from 'lucide-react';
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);
@ -85,15 +87,39 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
);
}
return (
<div className={`relative p-6 rounded-xl border transition-all duration-500 ${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'}`}>
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 py-4 animate-pulse">
<Sparkles className="text-yellow-400 mb-2" size={32} />
<h3 className="text-xl font-bold text-white text-center">
<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-300 text-sm text-center mt-1">
<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.`}
@ -101,52 +127,47 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
</div>
) : (
<>
<div className="flex items-center gap-4 mb-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-xs">
<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">
<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>
</>
) : parseInt(timeLeft.d) === 1 ? (
<>
<span>01</span><span className="text-slate-500 text-sm">d</span>
<span className="text-slate-500">:</span>
<span>{timeLeft.h}</span><span className="text-slate-500 text-sm">h</span>
</>
) : (
<>
<span>{timeLeft.h}</span>
<span className="text-slate-500">:</span>
<span>{timeLeft.m}</span>
<span className="text-slate-500">:</span>
<span>{timeLeft.s}</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 mt-2 font-mono uppercase tracking-widest leading-none">
{isLive ? (
"Mission in Progress"
) : id === 'iss' ? (
"T-Minus to Horizon"
) : id === 'launch' ? (
"T-Minus to Ignition"
) : id === 'moon' ? (
"Until Lunar Phase"
) : (
"Days to Peak Phase"
)}
<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>
);
}

29
components/Footer.tsx Normal file
View file

@ -0,0 +1,29 @@
export default function Footer() {
return (
<footer className="w-full max-w-4xl border-t border-slate-800/50 pt-8 mt-12 mb-16 px-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-6">
<div className="flex items-center gap-6">
<div className="flex items-center gap-2">
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Pipeline Status</p>
<img
src="https://ci.georgew.dev/api/badges/11/status.svg"
alt="Build Status"
className="h-3 grayscale opacity-50 hover:opacity-100 hover:grayscale-0 transition-all"
/>
</div>
<div className="hidden md:block w-[1px] h-3 bg-neutral-800" />
<p className="text-[10px] font-mono text-slate-500 uppercase tracking-widest">Engine: Next.js 15 (Standalone)</p>
</div>
<div className="flex items-center gap-3 bg-slate-900/50 border border-slate-800 rounded-full px-4 py-1.5 shadow-inner">
<div className="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse" />
<span className="text-[10px] font-mono text-slate-400 uppercase tracking-widest">
Deploy: {process.env.NEXT_PUBLIC_APP_VERSION || 'v1.0.0-dev'}
</span>
</div>
</div>
</footer>
);
}

View file

@ -1,15 +1,10 @@
"use client";
import { useMemo } from 'react';
import dynamic from 'next/dynamic';
import EventCard from '@/components/EventCard';
import { Satellite, Rocket, Moon, Sparkles } from 'lucide-react';
import { MissionControlProps } from '@/types/space';
const Starfield = dynamic(() => import('@/components/Starfield'), {
ssr: false
});
import { motion } from 'framer-motion';
export default function MissionControl({ iss, moon, cosmic, launch }: MissionControlProps) {
@ -45,28 +40,39 @@ export default function MissionControl({ iss, moon, cosmic, launch }: MissionCon
], [iss, moon, cosmic, launch]);
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">
Georgew<span className="text-blue-500 font-black">Observatory</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) => (
<div className="relative flex flex-col items-center p-8 overflow-hidden">
<div className="z-10 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-10 w-full max-w-5xl">
{events.map((event, index) => (
<motion.div
key={event.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{
duration: 1.2,
delay: index * 0.15,
ease: [0.16, 1, 0.3, 1]
}}
className="flex flex-col gap-3"
>
<div key={event.id} className="flex flex-col gap-3">
<div className="flex items-center gap-2 px-1">
<div className="h-[1px] w-4 bg-blue-500/50" />
<span className="text-[10px] font-bold font-mono tracking-[0.2em] text-blue-400/70 uppercase">
{event.id === 'iss' && "ISS Zenith // CPH"}
{event.id === 'moon' && "Lunar Cycle"}
{event.id === 'cosmic' && "Celestial Event"}
{event.id === 'launch' && "Launch Schedule"}
</span>
</div>
<EventCard
key={`${event.id}-${event.date?.getTime() || 'none'}`}
id={event.id}
title={event.title}
targetDate={event.date}
endDate={event.endDate}
icon={event.icon}
/>
</div>
</motion.div>
))}
</div>
</div>