clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/141-effect-library
lofi / website / src / components / NodePlayground.tsx
NodePlayground.tsx 6.6K
1import type { ReactNode } from "react";2import { useState } from "react";34// The /node hero explainer: a node you operate. Each action is one real step5// of the lofi-node lifecycle (init → start → ticket → enroll), then the two6// things worth feeling: revocation closing a live socket, and pairing a7// second home. Pure state + CSS; no network, nothing measured.89type Stage = "fresh" | "inited" | "started" | "ticketed" | "enrolled";1011type Line = { kind: "cmd" | "out" | "warn"; text: string };1213const TICKET_PREVIEW = "lofisync1.eyJ2IjoxLCJhcHBJZCI6ImNmZTUyZ…";1415export default function NodePlayground(): ReactNode {16 const [stage, setStage] = useState<Stage>("fresh");17 const [revoked, setRevoked] = useState(false);18 const [paired, setPaired] = useState(false);19 const [lines, setLines] = useState<Line[]>([20 { kind: "out", text: "a machine of yours: laptop, NAS, corner server" },21 ]);2223 const say = (...next: Line[]) =>24 setLines((prev) => [...prev.slice(-5), ...next]);2526 const init = () => {27 setStage("inited");28 say(29 { kind: "cmd", text: "lofi-node init --port 4802" },30 { kind: "out", text: "config.json written · ticket-gated by default" },31 );32 };33 const start = () => {34 setStage("started");35 say(36 { kind: "cmd", text: "lofi-node start" },37 { kind: "out", text: "gate :4802 · store binds loopback-only" },38 );39 };40 const issue = () => {41 setStage("ticketed");42 setRevoked(false);43 say(44 { kind: "cmd", text: "lofi-node ticket issue --label phone" },45 { kind: "out", text: `${TICKET_PREVIEW} · shown once` },46 );47 };48 const enroll = () => {49 setStage("enrolled");50 say(51 { kind: "cmd", text: "(on the phone) paste the ticket into the app" },52 { kind: "out", text: "sync elected; existing local data pushes up" },53 );54 };55 const revoke = () => {56 setRevoked(true);57 say(58 { kind: "cmd", text: "lofi-node ticket revoke a1b2c3d4e5f6" },59 {60 kind: "warn",61 text: "live socket closed 4001 · unknown ≡ revoked to probers",62 },63 );64 };65 const pair = () => {66 setPaired(true);67 say(68 { kind: "cmd", text: "lofi-node pair endpoint… (at the studio)" },69 { kind: "out", text: "upstream elected · tunnel up · rtt 23ms, direct" },70 );71 };72 const reset = () => {73 setStage("fresh");74 setRevoked(false);75 setPaired(false);76 setLines([{77 kind: "out",78 text: "a machine of yours: laptop, NAS, corner server",79 }]);80 };8182 const linked = stage === "enrolled" && !revoked;8384 return (85 <div86 className="np"87 data-stage={stage}88 data-paired={paired ? "" : undefined}89 >90 <div className="np-scene" aria-hidden="true">91 <div className={`np-device ${stage === "enrolled" ? "is-on" : ""}`}>92 <svg className="np-device-glyph" viewBox="0 0 32 52">93 <rect94 x="2.5"95 y="2.5"96 width="27"97 height="47"98 rx="5"99 fill="none"100 stroke="currentColor"101 strokeWidth="3"102 />103 </svg>104 <span className="np-device-label">phone</span>105 {stage === "enrolled" && (106 <span className={`np-chip ${revoked ? "np-chip--dead" : ""}`}>107 {revoked ? "4001" : "lofisync1…"}108 </span>109 )}110 </div>111112 <div113 className={`np-link ${linked ? "is-live" : ""} ${114 revoked ? "is-cut" : ""115 }`}116 />117118 <div className={`np-home ${stage !== "fresh" ? "is-on" : ""}`}>119 {stage === "ticketed" && (120 <span className="np-chip np-chip--ticket">lofisync1…</span>121 )}122 <span className="np-home-roof" />123 <div className="np-home-body">124 <span125 className={`np-part np-part--gate ${126 stage !== "fresh" ? "is-on" : ""127 }`}128 >129 gate{stage !== "fresh" && <i className="np-led" />}130 </span>131 <span132 className={`np-part np-part--store ${133 stage !== "inited" && stage !== "fresh" ? "is-on" : ""134 }`}135 >136 store137 </span>138 </div>139 <span className="np-home-label">home</span>140 </div>141142 <div className={`np-link np-link--tunnel ${paired ? "is-live" : ""}`}>143 {paired && (144 <span className="np-rtt">145 rtt 23ms146 <br />147 direct148 </span>149 )}150 </div>151152 <div className={`np-home np-home--peer ${paired ? "is-on" : ""}`}>153 <span className="np-home-roof" />154 <div className="np-home-body">155 <span className="np-part is-on">store</span>156 </div>157 <span className="np-home-label">studio</span>158 </div>159 </div>160161 <div className="np-console" role="log" aria-live="polite">162 {lines.map((line, index) => (163 <p key={`${index}-${line.text}`} data-kind={line.kind}>164 {line.kind === "cmd" ? <b>$</b> : null}165 {line.text}166 </p>167 ))}168 </div>169170 <div className="np-actions">171 {stage === "fresh" && (172 <button type="button" className="np-btn np-btn--go" onClick={init}>173 lofi-node init174 </button>175 )}176 {stage === "inited" && (177 <button type="button" className="np-btn np-btn--go" onClick={start}>178 lofi-node start179 </button>180 )}181 {stage === "started" && (182 <button type="button" className="np-btn np-btn--go" onClick={issue}>183 ticket issue --label phone184 </button>185 )}186 {stage === "ticketed" && (187 <button type="button" className="np-btn np-btn--go" onClick={enroll}>188 paste it into the app189 </button>190 )}191 {stage === "enrolled" && !revoked && (192 <button type="button" className="np-btn" onClick={revoke}>193 revoke the ticket194 </button>195 )}196 {stage === "enrolled" && revoked && (197 <button type="button" className="np-btn np-btn--go" onClick={issue}>198 issue a fresh ticket199 </button>200 )}201 {stage === "enrolled" && !paired && (202 <button type="button" className="np-btn" onClick={pair}>203 pair a second home204 </button>205 )}206 {stage !== "fresh" && (207 <button208 type="button"209 className="np-btn np-btn--ghost"210 onClick={reset}211 >212 reset213 </button>214 )}215 </div>216 </div>217 );218}