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:
@@ -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)}` : "--:--:--"}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user