Code — how this page is built
Under the hood
The actual source of the components on this page, read straight from the repo. Copy it, read it, or open it on GitHub.
FullMoonGameview on GitHub ↗
Reads SunCalc's moon phase + illuminated fraction, maps the phase to a name/emoji, and estimates days-to-full from the synodic month.
'use client';
import * as SunCalc from 'suncalc';
import * as React from 'react';
const SYNODIC = 29.530588;
const PHASES: [number, string, string][] = [
[0.0625, 'New Moon', '🌑'],
[0.1875, 'Waxing Crescent', '🌒'],
[0.3125, 'First Quarter', '🌓'],
[0.4375, 'Waxing Gibbous', '🌔'],
[0.5625, 'Full Moon', '🌕'],
[0.6875, 'Waning Gibbous', '🌖'],
[0.8125, 'Last Quarter', '🌗'],
[0.9375, 'Waning Crescent', '🌘'],
];
export function FullMoonGame() {
const [data, setData] = React.useState<{ phase: number; fraction: number } | null>(null);
React.useEffect(() => {
const m = SunCalc.getMoonIllumination(new Date());
setData({ phase: m.phase, fraction: m.fraction });
}, []);
if (!data) return null;
const { phase, fraction } = data;
const [, name, emoji] = PHASES.find(([p]) => phase < p) ?? PHASES[0];
const isFull = fraction > 0.97;
const daysToFull = (((0.5 - phase + 1) % 1) * SYNODIC).toFixed(1);
return (
<div className="flex w-full max-w-md flex-col items-center gap-6 text-center">
<div className="text-[140px] leading-none">{emoji}</div>
<div>
<div className="text-3xl font-bold tracking-tight">{name}</div>
<div className="mt-1 font-mono text-sm opacity-70">{Math.round(fraction * 100)}% illuminated</div>
</div>
{isFull ? (
<div className="rounded-md bg-[var(--accent-gold)]/20 px-4 py-2 font-mono text-sm">
it's a full moon tonight — go look up
</div>
) : (
<div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-50">
next full moon in ~{daysToFull} days
</div>
)}
</div>
);
}