822c0b7c0b
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from 8685140 on 2026-09-13. Licensed under the Functional Source License (FSL-1.1-ALv2).
45 lines
2.4 KiB
TypeScript
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>
|
|
);
|
|
}
|