an app to share curated trails
sidetrail.app
1import { saveDraft, type DraftInput } from "./actions";
2
3function hasContent(d: DraftInput): boolean {
4 return !!(
5 d.title.trim() ||
6 d.description.trim() ||
7 d.stops.some((s) => s.title.trim() || s.content?.trim() || s.buttonText?.trim() || s.external)
8 );
9}
10
11export class DraftSaver {
12 private rkey: string;
13 private version: number;
14 private onWarning: (msg: string) => void;
15
16 private pending: DraftInput | null = null;
17 private lastSaved: string;
18 private saving = false;
19
20 constructor(
21 rkey: string,
22 version: number,
23 initial: DraftInput,
24 onWarning: (msg: string) => void,
25 ) {
26 this.rkey = rkey;
27 this.version = version;
28 this.lastSaved = JSON.stringify(initial);
29 this.onWarning = onWarning;
30 }
31
32 save(draft: DraftInput): void {
33 const json = JSON.stringify(draft);
34 if (json === this.lastSaved || !hasContent(draft)) return;
35
36 this.pending = draft;
37 this.flush();
38 }
39
40 async saveNow(draft: DraftInput): Promise<void> {
41 this.pending = draft;
42 await this.flush();
43 }
44
45 private async flush(): Promise<void> {
46 if (!this.pending || this.saving) return;
47
48 this.saving = true;
49 const draft = this.pending;
50 const json = JSON.stringify(draft);
51
52 try {
53 const result = await saveDraft(this.rkey, draft, this.version);
54 if (result.success) {
55 this.version = result.version;
56 this.lastSaved = json;
57 if (this.pending === draft) this.pending = null;
58 if (result.warning === "overwrote_newer") {
59 this.onWarning("saved, but overwrote changes from another device");
60 }
61 }
62 } catch (e) {
63 console.error("Failed to save draft:", e);
64 } finally {
65 this.saving = false;
66 if (this.pending) this.flush();
67 }
68 }
69}