an app to share curated trails sidetrail.app
1

Configure Feed

Select the types of activity you want to include in your feed.

1"use server"; 2 3import "server-only"; 4import { getDb, drafts, type DraftRecord } from "@/data/db"; 5import { eq, and, sql } from "drizzle-orm"; 6import { getCurrentDid } from "@/auth"; 7import { generateTid } from "../tid"; 8 9// ============================================================================ 10// Types 11// ============================================================================ 12 13export type DraftInput = { 14 title: string; 15 description: string; 16 stops: Array<{ 17 tid: string; 18 title: string; 19 content: string; 20 buttonText?: string; 21 external?: { 22 uri: string; 23 title?: string; 24 description?: string; 25 thumb?: string; 26 }; 27 }>; 28 accentColor: string; 29 backgroundColor: string; 30}; 31 32export type SaveDraftResult = { 33 success: true; 34 version: number; 35 warning?: "overwrote_newer"; 36}; 37 38// ============================================================================ 39// Auth Helper 40// ============================================================================ 41 42async function requireAuth(): Promise<string> { 43 const did = await getCurrentDid(); 44 if (!did) { 45 throw new Error("Authentication required"); 46 } 47 return did; 48} 49 50// ============================================================================ 51// Color Presets (same as client-side) 52// ============================================================================ 53 54const COLOR_PRESETS = [ 55 { accent: "#e8a87c", background: "#fdf8f3" }, 56 { accent: "#c38d9e", background: "#fdf5f7" }, 57 { accent: "#41b3a3", background: "#f0faf9" }, 58 { accent: "#659dbd", background: "#f3f8fb" }, 59 { accent: "#e27d60", background: "#fdf6f4" }, 60 { accent: "#8d8741", background: "#f9f9f3" }, 61 { accent: "#5d5c61", background: "#f5f5f6" }, 62 { accent: "#7395ae", background: "#f4f7f9" }, 63]; 64 65function randomColorPreset() { 66 return COLOR_PRESETS[Math.floor(Math.random() * COLOR_PRESETS.length)]; 67} 68 69// ============================================================================ 70// Actions 71// ============================================================================ 72 73export async function createDraft(): Promise<string> { 74 const did = await requireAuth(); 75 const db = getDb(); 76 const rkey = generateTid(); 77 const id = `${did}:${rkey}`; 78 const now = new Date(); 79 const colors = randomColorPreset(); 80 81 const record: DraftRecord = { 82 $type: "app.sidetrail.draft", 83 title: "", 84 description: "", 85 stops: [ 86 { tid: generateTid(), title: "", content: "" }, 87 { tid: generateTid(), title: "", content: "" }, 88 ], 89 accentColor: colors.accent, 90 backgroundColor: colors.background, 91 createdAt: now.toISOString(), 92 updatedAt: now.toISOString(), 93 }; 94 95 await db.insert(drafts).values({ 96 id, 97 authorDid: did, 98 rkey, 99 record, 100 createdAt: now, 101 updatedAt: now, 102 version: 1, 103 }); 104 105 return rkey; 106} 107 108export async function saveDraft( 109 rkey: string, 110 input: DraftInput, 111 expectedVersion?: number, 112): Promise<SaveDraftResult> { 113 const did = await requireAuth(); 114 const db = getDb(); 115 const id = `${did}:${rkey}`; 116 const now = new Date(); 117 118 // Check for conflict if expectedVersion provided 119 let warning: "overwrote_newer" | undefined; 120 if (expectedVersion !== undefined) { 121 const [existing] = await db 122 .select({ version: drafts.version }) 123 .from(drafts) 124 .where(eq(drafts.id, id)) 125 .limit(1); 126 127 if (existing && existing.version > expectedVersion) { 128 warning = "overwrote_newer"; 129 } 130 } 131 132 const record: DraftRecord = { 133 $type: "app.sidetrail.draft", 134 title: input.title, 135 description: input.description, 136 stops: input.stops, 137 accentColor: input.accentColor, 138 backgroundColor: input.backgroundColor, 139 createdAt: now.toISOString(), 140 updatedAt: now.toISOString(), 141 }; 142 143 // Upsert the draft 144 const result = await db 145 .insert(drafts) 146 .values({ 147 id, 148 authorDid: did, 149 rkey, 150 record, 151 createdAt: now, 152 updatedAt: now, 153 version: 1, 154 }) 155 .onConflictDoUpdate({ 156 target: drafts.id, 157 set: { 158 record, 159 updatedAt: now, 160 version: sql`${drafts.version} + 1`, 161 }, 162 }) 163 .returning({ version: drafts.version }); 164 165 return warning 166 ? { success: true, version: result[0].version, warning } 167 : { success: true, version: result[0].version }; 168} 169 170export async function deleteDraft(rkey: string): Promise<void> { 171 const did = await requireAuth(); 172 const db = getDb(); 173 174 await db.delete(drafts).where(and(eq(drafts.authorDid, did), eq(drafts.rkey, rkey))); 175}