Files
threadcount-community/components/DemoBanner.tsx
T
ThreadCount 057da00fd2 ThreadCount Community edition
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from e2d6d42 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-13 11:16:36 +10:00

45 lines
2.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import { useSnap } from "@/lib/client";
const RESET_MS = 20 * 60 * 1000;
/** Shown on every /app screen of the shared demo facility: what it is, when it resets, and a role switch. */
export default function DemoBanner() {
const { s } = useSnap();
/* Seeded in an effect, not in useState's initialiser.
*
* This component renders on the server too, where Date.now() is the server's clock at render
* time — so the first paint carried one countdown and hydration immediately replaced it with a
* different one from the visitor's clock, which is both a visible jump and a hydration mismatch
* on a value React has no way to reconcile. Null until mounted means the server and the first
* client render agree on "no clock yet", and the countdown appears a tick later from the only
* clock that is actually ticking.
*/
const [now, setNow] = useState<number | null>(null);
useEffect(() => {
setNow(Date.now());
const t = setInterval(() => setNow(Date.now()), 1000);
return () => clearInterval(t);
}, []);
if (!s.demo) return null;
const base = s.demo.resetAt ? new Date(s.demo.resetAt).getTime() : now;
const left = now === null || base === null ? null : Math.max(0, RESET_MS - ((now - base) % RESET_MS));
const mm = left === null ? null : Math.floor(left / 60000), ss = left === null ? null : Math.floor((left % 60000) / 1000);
const other = s.session.role === "Admin" ? "issuer" : "admin";
return (
<div style={{ display: "flex", alignItems: "center", gap: 14, padding: "8px 16px", background: "var(--color-text)", color: "#fff", fontSize: 12.5, flexWrap: "wrap" }}>
<span style={{ width: 8, height: 8, background: "var(--color-accent)", flex: "none" }} />
<b>Working demo</b>
<span style={{ opacity: 0.85 }}>
{s.settings.facility} is fictional and shared with other visitors.{" "}
{mm === null ? `Resets every ${RESET_MS / 60000} minutes.` : <>Resets in <b style={{ fontVariantNumeric: "tabular-nums" }}>{mm}:{String(ss).padStart(2, "0")}</b>.</>}
</span>
<span style={{ marginLeft: "auto", display: "flex", gap: 14 }}>
<a href={`/api/auth/demo?as=${other}`} style={{ color: "#fff", fontWeight: 700 }}>Switch to {other === "admin" ? "Admin" : "Issuer"}</a>
<a href="/auth?mode=signup" style={{ color: "#fff", fontWeight: 700 }}>Create your own facility</a>
</span>
</div>
);
}