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]>
91 lines
2.5 KiB
TypeScript
91 lines
2.5 KiB
TypeScript
"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)}` : "--:--:--"}
|
|
</>
|
|
);
|
|
}
|