clone

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

fix/175-template-quality-pass
lofi / package / starter / src / islands / TaskList.tsx.txt
TaskList.tsx.txt 4.0K
1import { useState } from "preact/hooks";2import { settleUiMutation } from "@nzip/lofi";3import {4 type BootProgress,5 useBootProgress,6 usePendingWrites,7 useSyncStatus,8} from "@nzip/lofi/preact";9import { type Task, useTaskNotice, useTasks } from "./use-tasks.ts";1011// A cold first visit waits on the engine download, not on storage; name the12// wait it is actually in, with byte progress while the download runs.13function loadingLabel(boot: BootProgress): string {14 if (boot.phase !== "downloading") return "Opening persistent storage…";15 const mb = (bytes: number) => (bytes / (1024 * 1024)).toFixed(1);16 return boot.totalBytes === null17 ? `Downloading the app · ${mb(boot.loadedBytes)} MB…`18 : `Downloading the app · ${mb(boot.loadedBytes)} of ${mb(boot.totalBytes)} MB…`;19}2021/**22 * Author-owned starter UI. One island over one table: add a row through a23 * verb with effects, list rows with per-row sync badges, and toggle a24 * boolean. Replace it with your own schema and components — the25 * package-owned runtime stays the same.26 */27export default function TaskList() {28 const { status, error, durability, tasks, failureKind, create, setCompleted } = useTasks();29 const notice = useTaskNotice();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 {notice && (82 <p class="state" role="status" data-notice={notice.kind}>83 {notice.text}84 </p>85 )}86 <ul aria-label="Tasks">87 {tasks.map((task) => <TaskItem key={task.id} task={task} setCompleted={setCompleted} />)}88 </ul>89 </section>90 );91}9293function TaskItem(94 { task, setCompleted }: {95 task: Task;96 setCompleted: (id: string, completed: boolean) => PromiseLike<unknown>;97 },98) {99 const syncStatus = useSyncStatus(task);100 return (101 <li class={task.completed ? "task task-complete" : "task"}>102 <label class="task-toggle">103 <input104 type="checkbox"105 checked={task.completed}106 aria-label={`Complete ${task.text}`}107 onChange={(event) =>108 void settleUiMutation(setCompleted(task.id, event.currentTarget.checked))}109 />110 <span>{task.text}</span>111 </label>112 {syncStatus !== "synced" && (113 <span class="state" data-sync-status={syncStatus}>114 {syncStatus === "waiting" ? "waiting to sync" : "declined"}115 </span>116 )}117 </li>118 );119}