Rebuild portfolio in Next.js and Tailwind

Static-export Next.js 16 app with the tactical-map hero, Dajt terrain
background, dossier pages and the projects node index.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-03 20:40:10 +02:00
co-authored by claude
parent 3ea4946164
commit 85dbdb3db4
37 changed files with 9042 additions and 16 deletions
+90
View File
@@ -0,0 +1,90 @@
"use client";
import { useSyncExternalStore } from "react";
// Real data: when this session opened, a random per-session hash, and the
// visitor's local clock. All of it is client-only and never leaves the page.
type Session = { start: number; hash: string };
let session: Session | null = null;
function getSession(): Session {
if (session) return session;
try {
const raw = sessionStorage.getItem("session");
if (raw) return (session = JSON.parse(raw) as Session);
} catch {}
const bytes = crypto.getRandomValues(new Uint8Array(8));
const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
session = { start: Date.now(), hash };
try {
sessionStorage.setItem("session", JSON.stringify(session));
} catch {}
return session;
}
const noop = () => () => {};
const tick = (cb: () => void) => {
const id = setInterval(cb, 1000);
return () => clearInterval(id);
};
const nowSeconds = () => Math.floor(Date.now() / 1000);
export function useSession() {
return useSyncExternalStore(noop, getSession, () => null);
}
export function useNow() {
const s = useSyncExternalStore(tick, nowSeconds, () => null);
return s === null ? null : new Date(s * 1000);
}
const p = (n: number) => String(n).padStart(2, "0");
export function formatTime(d: Date) {
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
}
export function formatOffset(d: Date) {
const off = -d.getTimezoneOffset();
const a = Math.abs(off);
return `UTC${off >= 0 ? "+" : "-"}${p(Math.floor(a / 60))}:${p(a % 60)}`;
}
function formatElapsed(ms: number) {
const s = Math.max(0, Math.floor(ms / 1000));
return `${p(Math.floor(s / 60))}:${p(s % 60)}`;
}
export function SessionClock() {
const s = useSession();
const now = useNow();
const id = s ? `${s.hash.slice(0, 4)}·${s.hash.slice(4, 8)}`.toUpperCase() : "····";
return (
<>
<div>SESSION {id}</div>
<div>
<span className="text-accent">● LIVE</span> T+{s && now ? formatElapsed(now.getTime() - s.start) : "--:--"}
</div>
</>
);
}
export function SessionStamp() {
const s = useSession();
return (
<>
<div>SESSION OPEN · {s ? new Date(s.start).toISOString().slice(0, 19) + "Z" : "…"}</div>
<div>HASH {s ? `${s.hash.slice(0, 8)}·${s.hash.slice(8)}` : "…"} · RES CITY-SCALE</div>
</>
);
}
export function LocalTime({ prefix = "LOCAL" }: { prefix?: string }) {
const now = useNow();
return (
<>
{prefix} {now ? `${formatTime(now)} ${formatOffset(now)}` : "--:--:--"}
</>
);
}