an app to share curated trails
sidetrail.app
1import "server-only";
2import { cache } from "react";
3import { getDb, drafts } from "@/data/db";
4import { eq, and, desc } from "drizzle-orm";
5import { getCurrentDid } from "@/auth";
6
7// ============================================================================
8// Types
9// ============================================================================
10
11export type DraftListItem = {
12 rkey: string;
13 title: string;
14 description: string;
15 stopsCount: number;
16 accentColor: string;
17 backgroundColor: string;
18 updatedAt: Date;
19};
20
21export type DraftDetail = {
22 rkey: string;
23 version: number;
24 title: string;
25 description: string;
26 stops: Array<{
27 tid: string;
28 title: string;
29 content: string;
30 buttonText?: string;
31 external?: {
32 uri: string;
33 title?: string;
34 description?: string;
35 thumb?: string;
36 };
37 }>;
38 accentColor: string;
39 backgroundColor: string;
40 createdAt: Date;
41 updatedAt: Date;
42};
43
44// ============================================================================
45// Auth Helper
46// ============================================================================
47
48const requireAuth = cache(async function requireAuth(): Promise<string> {
49 const did = await getCurrentDid();
50 if (!did) {
51 throw new Error("Authentication required");
52 }
53 return did;
54});
55
56// ============================================================================
57// Queries
58// ============================================================================
59
60export const loadDrafts = cache(async function loadDrafts(): Promise<DraftListItem[]> {
61 const did = await requireAuth();
62 const db = getDb();
63
64 const rows = await db
65 .select()
66 .from(drafts)
67 .where(eq(drafts.authorDid, did))
68 .orderBy(desc(drafts.updatedAt));
69
70 return rows.map((row) => ({
71 rkey: row.rkey,
72 title: row.record.title,
73 description: row.record.description,
74 stopsCount: row.record.stops.length,
75 accentColor: row.record.accentColor,
76 backgroundColor: row.record.backgroundColor,
77 updatedAt: row.updatedAt,
78 }));
79});
80
81export const loadDraftDetail = cache(async function loadDraftDetail(
82 rkey: string,
83): Promise<DraftDetail | null> {
84 const did = await requireAuth();
85 const db = getDb();
86
87 const [row] = await db
88 .select()
89 .from(drafts)
90 .where(and(eq(drafts.authorDid, did), eq(drafts.rkey, rkey)))
91 .limit(1);
92
93 if (!row) return null;
94
95 return {
96 rkey: row.rkey,
97 version: row.version,
98 title: row.record.title,
99 description: row.record.description,
100 stops: row.record.stops,
101 accentColor: row.record.accentColor,
102 backgroundColor: row.record.backgroundColor,
103 createdAt: row.createdAt,
104 updatedAt: row.updatedAt,
105 };
106});
107
108export const loadDraftsBadges = cache(async function loadDraftsBadges(): Promise<
109 Array<{ accentColor: string; key: string }>
110> {
111 const did = await requireAuth();
112 const db = getDb();
113
114 const rows = await db
115 .select()
116 .from(drafts)
117 .where(eq(drafts.authorDid, did))
118 .orderBy(desc(drafts.updatedAt))
119 .limit(3);
120
121 return rows.map((row) => ({
122 key: row.rkey,
123 accentColor: row.record.accentColor,
124 }));
125});