learn and share notes on atproto (wip) 馃
malfestio.stormlightlabs.org/
readability
solid
axum
atproto
srs
1import { api } from "$lib/api";
2import type { LocalCard, LocalDeck, LocalNote, SyncQueueItem } from "$lib/db";
3import { syncStore } from "$lib/sync-store";
4import type { Column } from "$ui/DataTable";
5import { DataTable } from "$ui/DataTable";
6import { createResource, createSignal, Show } from "solid-js";
7
8type SyncRecord = {
9 id: string;
10 type: "deck" | "note" | "card";
11 title: string;
12 status: string;
13 version: number;
14 updatedAt: string;
15};
16
17type QueueRecord = {
18 id: string;
19 entityType: string;
20 entityId: string;
21 operation: string;
22 retryCount: number;
23 createdAt: string;
24 lastError?: string;
25};
26
27export function SyncDataTable() {
28 const [activeTab, setActiveTab] = createSignal<"records" | "queue">("records");
29 const [refreshKey, setRefreshKey] = createSignal(0);
30
31 const [data, { refetch }] = createResource(refreshKey, async () => {
32 const result = await syncStore.getAllLocalData();
33 return result;
34 });
35
36 const syncRecords = (): SyncRecord[] => {
37 const d = data();
38 if (!d) return [];
39
40 const decks: SyncRecord[] = d.decks.map((deck: LocalDeck) => ({
41 id: deck.id,
42 type: "deck" as const,
43 title: deck.title,
44 status: deck.syncStatus,
45 version: deck.localVersion,
46 updatedAt: deck.updatedAt,
47 }));
48
49 const notes: SyncRecord[] = d.notes.map((note: LocalNote) => ({
50 id: note.id,
51 type: "note" as const,
52 title: note.title,
53 status: note.syncStatus,
54 version: note.localVersion,
55 updatedAt: note.updatedAt,
56 }));
57
58 const cards: SyncRecord[] = d.cards.map((card: LocalCard) => ({
59 id: card.id,
60 type: "card" as const,
61 title: card.front.slice(0, 50) + (card.front.length > 50 ? "..." : ""),
62 status: card.syncStatus,
63 version: card.localVersion,
64 updatedAt: "",
65 }));
66
67 return [...decks, ...notes, ...cards];
68 };
69
70 const queueRecords = (): QueueRecord[] => {
71 const d = data();
72 if (!d) return [];
73
74 return d.queue.map((item: SyncQueueItem) => ({
75 id: String(item.id || ""),
76 entityType: item.entityType,
77 entityId: item.entityId,
78 operation: item.operation,
79 retryCount: item.retryCount,
80 createdAt: item.createdAt,
81 lastError: item.lastError,
82 }));
83 };
84
85 const handleSync = async (type: string, id: string) => {
86 await syncStore.queueForSync(type as "deck" | "card" | "note", id, "push");
87 await syncStore.processQueue();
88 setRefreshKey((k) => k + 1);
89 };
90
91 const handleResolve = async (
92 type: string,
93 id: string,
94 strategy: "last_write_wins" | "keep_local" | "keep_remote",
95 ) => {
96 await api.resolveConflict(type, id, strategy);
97 setRefreshKey((k) => k + 1);
98 };
99
100 const handleClear = async () => {
101 if (confirm("Clear all local sync data? This cannot be undone.")) {
102 await syncStore.clearAll();
103 setRefreshKey((k) => k + 1);
104 }
105 };
106
107 const recordColumns: Column<SyncRecord>[] = [
108 { key: "type", header: "Type", sortable: true, width: "80px" },
109 { key: "title", header: "Title", sortable: true },
110 {
111 key: "status",
112 header: "Status",
113 sortable: true,
114 width: "120px",
115 render: (row) => (
116 <span
117 class={`px-2 py-1 rounded text-xs ${
118 row.status === "synced"
119 ? "bg-green-900 text-green-300"
120 : row.status === "conflict"
121 ? "bg-red-900 text-red-300"
122 : row.status === "pending_push"
123 ? "bg-blue-900 text-blue-300"
124 : "bg-gray-700 text-gray-300"
125 }`}>
126 {row.status}
127 </span>
128 ),
129 },
130 { key: "version", header: "Ver", sortable: true, width: "60px" },
131 {
132 key: "actions",
133 header: "Actions",
134 width: "150px",
135 render: (row) => (
136 <div class="flex gap-2">
137 <Show when={row.status === "conflict"}>
138 <button
139 onClick={() => handleResolve(row.type, row.id, "keep_local")}
140 class="text-xs text-blue-400 hover:underline">
141 Keep Local
142 </button>
143 </Show>
144 <Show when={row.status !== "synced"}>
145 <button onClick={() => handleSync(row.type, row.id)} class="text-xs text-green-400 hover:underline">
146 Sync
147 </button>
148 </Show>
149 </div>
150 ),
151 },
152 ];
153
154 const queueColumns: Column<QueueRecord>[] = [
155 { key: "entityType", header: "Type", sortable: true, width: "80px" },
156 { key: "entityId", header: "Entity ID", sortable: true },
157 { key: "operation", header: "Op", width: "60px" },
158 { key: "retryCount", header: "Retries", sortable: true, width: "80px" },
159 { key: "lastError", header: "Error" },
160 ];
161
162 return (
163 <div class="space-y-4">
164 <div class="flex items-center justify-between">
165 <div class="flex gap-2">
166 <button
167 onClick={() => setActiveTab("records")}
168 class={`px-3 py-1.5 text-sm rounded ${
169 activeTab() === "records" ? "bg-blue-600 text-white" : "bg-gray-700 text-gray-300"
170 }`}>
171 Records ({syncRecords().length})
172 </button>
173 <button
174 onClick={() => setActiveTab("queue")}
175 class={`px-3 py-1.5 text-sm rounded ${
176 activeTab() === "queue" ? "bg-blue-600 text-white" : "bg-gray-700 text-gray-300"
177 }`}>
178 Queue ({queueRecords().length})
179 </button>
180 </div>
181 <div class="flex gap-2">
182 <button
183 onClick={() => refetch()}
184 class="px-3 py-1.5 text-sm bg-gray-700 text-gray-300 rounded hover:bg-gray-600">
185 Refresh
186 </button>
187 <button onClick={handleClear} class="px-3 py-1.5 text-sm bg-red-900 text-red-300 rounded hover:bg-red-800">
188 Clear All
189 </button>
190 </div>
191 </div>
192
193 <Show when={data.loading}>
194 <div class="text-gray-400 text-sm">Loading...</div>
195 </Show>
196
197 <Show when={!data.loading && activeTab() === "records"}>
198 <Show when={syncRecords().length > 0} fallback={<div class="text-gray-500 text-sm">No local records</div>}>
199 <DataTable columns={recordColumns} data={syncRecords()} getRowId={(r) => r.id} />
200 </Show>
201 </Show>
202
203 <Show when={!data.loading && activeTab() === "queue"}>
204 <Show
205 when={queueRecords().length > 0}
206 fallback={<div class="text-gray-500 text-sm">No pending queue items</div>}>
207 <DataTable columns={queueColumns} data={queueRecords()} getRowId={(r) => r.id} />
208 </Show>
209 </Show>
210 </div>
211 );
212}