Add vpinball: a browser Visual Pinball player

Next.js app that plays real .vpx tables via @valknar/vpinball-wasm
(WebAssembly Visual Pinball). Demo mode with the bundled default
table, drag-and-drop custom table upload, a cabinet-styled UI with a
coin-door HUD (fullscreen/stats/info/eject), a custom 404 page, PWA
support (hand-rolled service worker), and a static Docker Compose
deployment behind nginx.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012hQoM3jJT1Lx7CMTciMzvD
This commit is contained in:
2026-08-23 05:25:15 +02:00
co-authored by Claude Sonnet 5
commit e493a12e68
51 changed files with 5570 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
"use client";
import { useEffect, useRef, useState } from "react";
/** True once no pointer/keyboard activity has been seen for `timeoutMs`. */
export function useIdleTimer(timeoutMs = 3000): boolean {
const [isIdle, setIsIdle] = useState(false);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
const reset = () => {
setIsIdle(false);
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => setIsIdle(true), timeoutMs);
};
reset();
const events: Array<keyof WindowEventMap> = [
"mousemove",
"pointerdown",
"touchstart",
"keydown",
];
events.forEach((event) => window.addEventListener(event, reset, { passive: true }));
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
events.forEach((event) => window.removeEventListener(event, reset));
};
}, [timeoutMs]);
return isIdle;
}