clone

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

feat/fix-sync-enrollment-startup
lofi / package / starter / src / islands / TaskList.tsx.txt
TaskList.tsx.txt 3.8K
1import { useState } from "preact/hooks";2import { settleUiMutation } from "@nzip/lofi";3import {4 type BootProgress,5 Notices,6 useBootProgress,7 usePendingWrites,8 useSyncStatus,9} from "@nzip/lofi/preact";10import { type Task, useTasks } from "./use-tasks.ts";1112// A cold first visit waits on the engine download, not on storage; name the13// wait it is actually in, with byte progress while the download runs.14function loadingLabel(boot: BootProgress): string {15 if (boot.phase !== "downloading") return "Opening persistent storage…";16 const mb = (bytes: number) => (bytes / (1024 * 1024)).toFixed(1);17 return boot.totalBytes === null18 ? `Downloading the app · ${mb(boot.loadedBytes)} MB…`19 : `Downloading the app · ${mb(boot.loadedBytes)} of ${mb(boot.totalBytes)} MB…`;20}2122/**23 * Author-owned starter UI. One island over one table: add a row through a24 * verb with effects, list rows with per-row sync badges, and toggle a25 * boolean. Replace it with your own schema and components — the26 * package-owned runtime stays the same.27 */28export default function TaskList() {29 const { status, error, durability, tasks, failureKind, create, setCompleted } = useTasks();30 const pending = usePendingWrites();31 const boot = useBootProgress();32 const [text, setText] = useState("");3334 return (35 <section id="tasks" class="island" data-island="tasks">36 <header>37 <p class="eyebrow">Local-first</p>38 <h2>Example Island: Tasks</h2>39 </header>40 <form41 onSubmit={(event) => {42 event.preventDefault();43 const next = text.trim();44 if (!next) return;45 setText("");46 // The verb's handle is thenable: settled here at saved, while the47 // sync fate and its effects continue in the background.48 void settleUiMutation(create(next));49 }}50 >51 <label for="new-task">New task</label>52 <div class="composer">53 <input54 id="new-task"55 value={text}56 onInput={(event) => setText(event.currentTarget.value)}57 autocomplete="off"58 />59 <button type="submit">Add</button>60 </div>61 </form>62 <p class="state" role="status">63 {status === "loading" && loadingLabel(boot)}64 {status === "error" &&65 `${failureKind === "read" ? "Read failed" : "Write failed"}: ${error}`}66 {status === "ready" && `${tasks.length} item(s) · ${67 durability === "global"68 ? "synced to your account"69 : durability === "local"70 ? "saved on this device"71 : durability === "failed"72 ? "write declined"73 : "ready"74 }`}75 </p>76 {pending.count > 0 && (77 <p class="state" data-pending-writes={pending.count}>78 {pending.count} change{pending.count === 1 ? "" : "s"} waiting to sync79 </p>80 )}81 <Notices />82 <ul aria-label="Tasks">83 {tasks.map((task) => <TaskItem key={task.id} task={task} setCompleted={setCompleted} />)}84 </ul>85 </section>86 );87}8889function TaskItem(90 { task, setCompleted }: {91 task: Task;92 setCompleted: (id: string, completed: boolean) => PromiseLike<unknown>;93 },94) {95 const syncStatus = useSyncStatus(task);96 return (97 <li class={task.completed ? "task task-complete" : "task"}>98 <label class="task-toggle">99 <input100 type="checkbox"101 checked={task.completed}102 aria-label={`Complete ${task.text}`}103 onChange={(event) =>104 void settleUiMutation(setCompleted(task.id, event.currentTarget.checked))}105 />106 <span>{task.text}</span>107 </label>108 {syncStatus !== "synced" && (109 <span class="state" data-sync-status={syncStatus}>110 {syncStatus === "waiting" ? "waiting to sync" : "declined"}111 </span>112 )}113 </li>114 );115}