Added labels and versioning
This commit is contained in:
parent
341763baaf
commit
5e6ac733c8
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
15
app/page.tsx
15
app/page.tsx
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -86,14 +88,38 @@ 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'}`}>
|
||||
<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
29
components/Footer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue