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.
CoinFlipGameview on GitHub ↗
Decides the result with Math.random, then spins a CSS 3D coin (rotateY, transform-style: preserve-3d) several turns to land on the chosen face. Tally persisted to localStorage.
'use client';
import * as React from 'react';
import { Win98Button } from '@/components/ui/win-98-button';
const KEY = 'kaspirius:coin-flip:tally';
export function CoinFlipGame() {
const [rot, setRot] = React.useState(0);
const [result, setResult] = React.useState<'Heads' | 'Tails' | null>(null);
const [tally, setTally] = React.useState({ h: 0, t: 0 });
const [flipping, setFlipping] = React.useState(false);
React.useEffect(() => {
try {
const saved = JSON.parse(localStorage.getItem(KEY) ?? 'null');
if (saved && typeof saved.h === 'number') setTally(saved);
} catch {
/* ignore */
}
}, []);
const flip = () => {
if (flipping) return;
const heads = Math.random() < 0.5;
const spins = 5 + Math.floor(Math.random() * 4);
setFlipping(true);
setResult(null);
setRot((r) => r - (r % 360) + spins * 360 + (heads ? 0 : 180));
window.setTimeout(() => {
setResult(heads ? 'Heads' : 'Tails');
setTally((t) => {
const next = heads ? { h: t.h + 1, t: t.t } : { h: t.h, t: t.t + 1 };
localStorage.setItem(KEY, JSON.stringify(next));
return next;
});
setFlipping(false);
}, 1500);
};
return (
<div className="flex flex-col items-center gap-7">
<div style={{ perspective: '1000px' }}>
<div
className="relative h-52 w-52 transition-transform duration-[1500ms] [transform-style:preserve-3d] [transition-timing-function:cubic-bezier(.2,.7,.2,1)]"
style={{ transform: `rotateY(${rot}deg)` }}
>
<Face label="H" />
<Face label="T" back />
</div>
</div>
<div className="h-5 font-mono text-sm tracking-wide opacity-75">
{result ? `${result}!` : flipping ? '…' : 'call it'}
</div>
<Win98Button onClick={flip} disabled={flipping} className="h-10 min-w-32 px-7 text-sm">
Flip
</Win98Button>
<div className="font-mono text-[11px] uppercase tracking-[0.12em] opacity-50">
heads {tally.h} · tails {tally.t}
</div>
</div>
);
}
function Face({ label, back = false }: { label: string; back?: boolean }) {
return (
<div
className="absolute inset-0 flex items-center justify-center rounded-full border-2 border-foreground bg-[#faf7ef] text-7xl font-bold text-foreground [backface-visibility:hidden]"
style={back ? { transform: 'rotateY(180deg)' } : undefined}
>
{label}
</div>
);
}