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.


BirthdayDoorGameview on GitHub ↗

Stores a month-day in localStorage and compares it to today; opens on a match, otherwise counts days to the next occurrence.

'use client';

import * as React from 'react';

import { Win98Button } from '@/components/ui/win-98-button';

const KEY = 'kaspirius:birthday:date'; // stored as MM-DD

export function BirthdayDoorGame() {
  const [bday, setBday] = React.useState<string | null>(null);
  const [input, setInput] = React.useState('');
  const [editing, setEditing] = React.useState(false);

  React.useEffect(() => {
    const v = localStorage.getItem(KEY);
    if (v) setBday(v);
    else setEditing(true);
  }, []);

  const save = () => {
    if (!input) return;
    const [, m, d] = input.split('-'); // input is YYYY-MM-DD
    const md = `${m}-${d}`;
    localStorage.setItem(KEY, md);
    setBday(md);
    setEditing(false);
  };

  const today = new Date();
  const todayMd = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
  const isBirthday = bday === todayMd;

  let daysAway = 0;
  if (bday && !isBirthday) {
    const [m, d] = bday.split('-').map(Number);
    let next = new Date(today.getFullYear(), m - 1, d);
    if (+next < +new Date(today.getFullYear(), today.getMonth(), today.getDate())) {
      next = new Date(today.getFullYear() + 1, m - 1, d);
    }
    daysAway = Math.ceil((+next - +new Date(today.getFullYear(), today.getMonth(), today.getDate())) / 86400000);
  }

  if (editing || !bday) {
    return (
      <div className="flex w-full max-w-xs flex-col items-center gap-4">
        <div className="text-7xl opacity-30">🚪</div>
        <p className="text-center text-sm opacity-70">Set your birthday. The door only opens on the day.</p>
        <input
          type="date"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          className="w-full border border-foreground/25 bg-[var(--tile)] px-3 py-2 text-center font-mono text-sm outline-none"
        />
        <Win98Button onClick={save} disabled={!input} className="h-10 min-w-28 px-6 text-sm">
          Save
        </Win98Button>
      </div>
    );
  }

  return (
    <div className="flex w-full max-w-xs flex-col items-center gap-5 text-center">
      <div className={`text-8xl transition-transform ${isBirthday ? 'animate-[bump_.6s_ease]' : ''}`}>
        {isBirthday ? '🎉' : '🚪'}
      </div>
      {isBirthday ? (
        <div>
          <div className="text-3xl font-bold">Happy birthday.</div>
          <div className="mt-1 text-sm opacity-70">the door is open — today is yours.</div>
        </div>
      ) : (
        <div>
          <div className="text-3xl font-bold">{daysAway}</div>
          <div className="mt-1 font-mono text-[11px] uppercase tracking-[0.12em] opacity-55">
            {daysAway === 1 ? 'day until the door opens' : 'days until the door opens'}
          </div>
        </div>
      )}
      <button onClick={() => setEditing(true)} className="font-mono text-[11px] uppercase tracking-[0.12em] underline opacity-45">
        change date
      </button>
      <style>{`@keyframes bump{0%{transform:scale(.4)}60%{transform:scale(1.15)}100%{transform:scale(1)}}`}</style>
    </div>
  );
}
↖ kaspirius