an app to share curated trails sidetrail.app
1

Configure Feed

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

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