Added labels and versioning
This commit is contained in:
parent
341763baaf
commit
5e6ac733c8
|
|
@ -2,14 +2,15 @@ variables:
|
||||||
- &app_name "mission-control"
|
- &app_name "mission-control"
|
||||||
|
|
||||||
when:
|
when:
|
||||||
event: [push]
|
- event: release
|
||||||
branch: main
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
build-web:
|
build-web:
|
||||||
image: woodpeckerci/plugin-docker-buildx
|
image: woodpeckerci/plugin-docker-buildx
|
||||||
privileged: true
|
privileged: true
|
||||||
settings:
|
settings:
|
||||||
|
build_args:
|
||||||
|
APP_VERSION: ${CI_COMMIT_TAG}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
registry: git.georgew.dev
|
registry: git.georgew.dev
|
||||||
repo: git.georgew.dev/georgew/mission-control-web
|
repo: git.georgew.dev/georgew/mission-control-web
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
|
ARG APP_VERSION
|
||||||
|
|
||||||
FROM --platform=linux/amd64 node:20-bookworm AS builder
|
FROM --platform=linux/amd64 node:20-bookworm AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
|
ARG APP_VERSION
|
||||||
|
ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN npm run build
|
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 MissionControl from '@/components/MissionControl';
|
||||||
import db from '@/lib/db';
|
import db from '@/lib/db';
|
||||||
|
import Footer from '@/components/Footer';
|
||||||
|
import Starfield from '@/components/Starfield';
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const issRow = db.prepare(`
|
const issRow = db.prepare(`
|
||||||
|
|
@ -40,13 +43,23 @@ export default function Home() {
|
||||||
const cosmicEnd = cosmicStart ? new Date(cosmicStart.getTime() + 14400000) : null;
|
const cosmicEnd = cosmicStart ? new Date(cosmicStart.getTime() + 14400000) : null;
|
||||||
|
|
||||||
return (
|
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
|
<MissionControl
|
||||||
iss={{ start: issStart, end: issEnd }}
|
iss={{ start: issStart, end: issEnd }}
|
||||||
moon={{ title: moonRow?.title || "Moon Phase", start: moonStart, end: moonEnd }}
|
moon={{ title: moonRow?.title || "Moon Phase", start: moonStart, end: moonEnd }}
|
||||||
cosmic={{ title: cosmicRow?.title || "Cosmic Event", start: cosmicStart, end: cosmicEnd }}
|
cosmic={{ title: cosmicRow?.title || "Cosmic Event", start: cosmicStart, end: cosmicEnd }}
|
||||||
launch={{ title: launchRow?.title || "Rocket Launch", start: launchStart, end: launchEnd }}
|
launch={{ title: launchRow?.title || "Rocket Launch", start: launchStart, end: launchEnd }}
|
||||||
/>
|
/>
|
||||||
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import React from 'react';
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { EventCardProps } from '@/types/space';
|
import { EventCardProps } from '@/types/space';
|
||||||
import { useRouter } from 'next/navigation';
|
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) {
|
export default function EventCard({ id, title, targetDate, endDate, icon }: EventCardProps) {
|
||||||
const [isLive, setIsLive] = useState(false);
|
const [isLive, setIsLive] = useState(false);
|
||||||
|
|
@ -85,15 +87,39 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 ? (
|
{isLive ? (
|
||||||
<div className="flex flex-col items-center justify-center py-4 animate-pulse">
|
<div className="flex flex-col items-center justify-center h-full relative z-10">
|
||||||
<Sparkles className="text-yellow-400 mb-2" size={32} />
|
<motion.div
|
||||||
<h3 className="text-xl font-bold text-white text-center">
|
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'}
|
{id === 'iss' ? 'Look Up!' : 'Event in Progress'}
|
||||||
</h3>
|
</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'
|
{id === 'iss'
|
||||||
? 'The ISS is directly above. Give them a wave! 👋'
|
? 'The ISS is directly above. Give them a wave! 👋'
|
||||||
: `The ${title} is occurring right now.`}
|
: `The ${title} is occurring right now.`}
|
||||||
|
|
@ -101,52 +127,47 @@ export default function EventCard({ id, title, targetDate, endDate, icon }: Even
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-4 mb-4">
|
<div className="flex items-center gap-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}</div>
|
||||||
{icon}
|
<h3 className="text-slate-300 font-mono tracking-widest uppercase text-[10px] truncate">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-slate-300 font-mono tracking-widest uppercase text-xs">
|
|
||||||
{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 ? (
|
{parseInt(timeLeft.d) >= 2 ? (
|
||||||
<>
|
<>
|
||||||
<span>{timeLeft.d}</span>
|
<span>{timeLeft.d}</span>
|
||||||
<span className="text-slate-500 text-xl lowercase">Days</span>
|
<span className="text-slate-500 text-xl lowercase">Days</span>
|
||||||
</>
|
</>
|
||||||
) : parseInt(timeLeft.d) === 1 ? (
|
) : (
|
||||||
<>
|
<div className="flex gap-1">
|
||||||
<span>01</span><span className="text-slate-500 text-sm">d</span>
|
<span>{timeLeft.h}</span><span className="text-slate-500">:</span>
|
||||||
<span className="text-slate-500">:</span>
|
<span>{timeLeft.m}</span><span className="text-slate-500">:</span>
|
||||||
<span>{timeLeft.h}</span><span className="text-slate-500 text-sm">h</span>
|
<div className="w-[1ch] relative">
|
||||||
</>
|
<AnimatePresence mode="popLayout">
|
||||||
) : (
|
<motion.span
|
||||||
<>
|
key={timeLeft.s}
|
||||||
<span>{timeLeft.h}</span>
|
initial={{ opacity: 0, y: 10 }}
|
||||||
<span className="text-slate-500">:</span>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<span>{timeLeft.m}</span>
|
exit={{ opacity: 0, y: -10 }}
|
||||||
<span className="text-slate-500">:</span>
|
transition={{ duration: 0.3 }}
|
||||||
<span>{timeLeft.s}</span>
|
className="absolute"
|
||||||
</>
|
>
|
||||||
)}
|
{timeLeft.s}
|
||||||
</div>
|
</motion.span>
|
||||||
<p className="text-[10px] text-slate-500 mt-2 font-mono uppercase tracking-widest leading-none">
|
</AnimatePresence>
|
||||||
{isLive ? (
|
</div>
|
||||||
"Mission in Progress"
|
</div>
|
||||||
) : id === 'iss' ? (
|
)}
|
||||||
"T-Minus to Horizon"
|
</div>
|
||||||
) : id === 'launch' ? (
|
|
||||||
"T-Minus to Ignition"
|
<p className="text-[10px] text-slate-500 font-mono uppercase tracking-widest leading-none">
|
||||||
) : id === 'moon' ? (
|
{id === 'iss' ? "T-Minus to Horizon" : id === 'launch' ? "T-Minus to Ignition" : id === 'moon' ? "Until Lunar Phase" : "Days to Peak Phase"}
|
||||||
"Until Lunar Phase"
|
</p>
|
||||||
) : (
|
|
||||||
"Days to Peak Phase"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import EventCard from '@/components/EventCard';
|
import EventCard from '@/components/EventCard';
|
||||||
import { Satellite, Rocket, Moon, Sparkles } from 'lucide-react';
|
import { Satellite, Rocket, Moon, Sparkles } from 'lucide-react';
|
||||||
import { MissionControlProps } from '@/types/space';
|
import { MissionControlProps } from '@/types/space';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
const Starfield = dynamic(() => import('@/components/Starfield'), {
|
|
||||||
ssr: false
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
export default function MissionControl({ iss, moon, cosmic, launch }: MissionControlProps) {
|
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]);
|
], [iss, moon, cosmic, launch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative min-h-screen flex flex-col items-center p-8 overflow-hidden">
|
<div className="relative flex flex-col items-center p-8 overflow-hidden">
|
||||||
<Starfield />
|
<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) => (
|
||||||
<header className="z-10 text-center mb-16 mt-10">
|
<motion.div
|
||||||
<h1 className="text-4xl md:text-6xl font-mono font-bold text-white tracking-tighter mb-4 uppercase">
|
key={event.id}
|
||||||
Georgew<span className="text-blue-500 font-black">Observatory</span>
|
initial={{ opacity: 0, y: 30 }}
|
||||||
</h1>
|
animate={{ opacity: 1, y: 0 }}
|
||||||
<p className="text-slate-400 font-mono text-xs tracking-[0.3em] uppercase opacity-70">
|
transition={{
|
||||||
Ground Station // [55.6761° N, 12.5683° E]
|
duration: 1.2,
|
||||||
</p>
|
delay: index * 0.15,
|
||||||
</header>
|
ease: [0.16, 1, 0.3, 1]
|
||||||
|
}}
|
||||||
<div className="z-10 grid grid-cols-1 md:grid-cols-2 gap-6 w-full max-w-5xl">
|
className="flex flex-col gap-3"
|
||||||
{events.map((event) => (
|
>
|
||||||
|
<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
|
<EventCard
|
||||||
key={`${event.id}-${event.date?.getTime() || 'none'}`}
|
|
||||||
id={event.id}
|
id={event.id}
|
||||||
title={event.title}
|
title={event.title}
|
||||||
targetDate={event.date}
|
targetDate={event.date}
|
||||||
endDate={event.endDate}
|
endDate={event.endDate}
|
||||||
icon={event.icon}
|
icon={event.icon}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue