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.
CookieClickerGameview on GitHub ↗
Tracks count + owned upgrades; per-click and per-second derive from ownership, prices scale geometrically, and progress persists to localStorage.
'use client';
import * as React from 'react';
const KEY = 'kaspirius:cookie-clicker:save';
interface Upgrade {
id: string;
name: string;
baseCost: number;
perSec?: number;
perClick?: number;
}
const UPGRADES: Upgrade[] = [
{ id: 'finger', name: 'Stronger finger (+1 / click)', baseCost: 15, perClick: 1 },
{ id: 'cursor', name: 'Auto-cursor (+1 / sec)', baseCost: 25, perSec: 1 },
{ id: 'grandma', name: 'Grandma (+6 / sec)', baseCost: 150, perSec: 6 },
{ id: 'farm', name: 'Cookie farm (+40 / sec)', baseCost: 1000, perSec: 40 },
];
const fmt = (n: number) => Math.floor(n).toLocaleString('en-US');
const costOf = (u: Upgrade, owned: number) => Math.ceil(u.baseCost * Math.pow(1.15, owned));
export function CookieClickerGame() {
const [count, setCount] = React.useState(0);
const [owned, setOwned] = React.useState<Record<string, number>>({});
const [pop, setPop] = React.useState(0);
React.useEffect(() => {
try {
const s = JSON.parse(localStorage.getItem(KEY) ?? 'null');
if (s) {
setCount(s.count ?? 0);
setOwned(s.owned ?? {});
}
} catch {
/* ignore */
}
}, []);
const perClick = 1 + (owned.finger ?? 0) * (UPGRADES[0].perClick ?? 0);
const perSec = UPGRADES.reduce((sum, u) => sum + (u.perSec ?? 0) * (owned[u.id] ?? 0), 0);
React.useEffect(() => {
if (perSec <= 0) return;
const id = window.setInterval(() => setCount((c) => c + perSec / 10), 100);
return () => window.clearInterval(id);
}, [perSec]);
React.useEffect(() => {
const id = window.setTimeout(
() => localStorage.setItem(KEY, JSON.stringify({ count, owned })),
300,
);
return () => window.clearTimeout(id);
}, [count, owned]);
const buy = (u: Upgrade) => {
const c = costOf(u, owned[u.id] ?? 0);
if (count < c) return;
setCount((x) => x - c);
setOwned((o) => ({ ...o, [u.id]: (o[u.id] ?? 0) + 1 }));
};
return (
<div className="flex w-full max-w-md flex-col items-center gap-5">
<div className="text-center">
<div className="text-4xl font-bold tracking-tight">{fmt(count)}</div>
<div className="mt-1 font-mono text-[11px] uppercase tracking-[0.12em] opacity-55">
cookies · {fmt(perSec)}/sec
</div>
</div>
<button
type="button"
onClick={() => {
setCount((c) => c + perClick);
setPop((p) => p + 1);
}}
className="select-none text-8xl transition-transform active:scale-90"
aria-label="cookie"
>
<span key={pop} className="inline-block animate-[bump_.1s_ease-out]">🍪</span>
</button>
<div className="flex w-full flex-col gap-2">
{UPGRADES.map((u) => {
const cost = costOf(u, owned[u.id] ?? 0);
const can = count >= cost;
return (
<button
key={u.id}
type="button"
onClick={() => buy(u)}
disabled={!can}
className={`flex items-center justify-between rounded-md border border-foreground/20 px-4 py-2.5 text-left text-sm transition-colors ${
can ? 'bg-[var(--tile)]' : 'opacity-50'
}`}
>
<span>
{u.name}
{owned[u.id] ? <span className="ml-2 opacity-50">×{owned[u.id]}</span> : null}
</span>
<span className="font-mono text-xs">{fmt(cost)}</span>
</button>
);
})}
</div>
<style>{`@keyframes bump{from{transform:scale(1.15)}to{transform:scale(1)}}`}</style>
</div>
);
}