this repo has no description
0
fork

Configure Feed

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

at export-config 283 lines 7.2 kB view raw
1import { Hono } from "hono"; 2import { html } from "hono/html"; 3import { layout } from "../views/layouts/main"; 4import { requireAuth, type Session } from "../lib/session"; 5import { csrfField } from "../lib/csrf"; 6import type { AppVariables } from "../types"; 7 8export const publicationRoutes = new Hono<{ Variables: AppVariables }>(); 9 10const PUBLICATION_COLLECTION = "site.standard.publication"; 11 12// View/manage publication 13publicationRoutes.get("/", async (c) => { 14 let session: Session; 15 try { 16 session = requireAuth(c); 17 } catch { 18 return c.redirect("/auth/login"); 19 } 20 21 try { 22 // Fetch existing publication 23 const response = await session.agent!.com.atproto.repo.listRecords({ 24 repo: session.did!, 25 collection: PUBLICATION_COLLECTION, 26 limit: 1, 27 }); 28 29 const publication = response.data.records[0]; 30 31 if (publication) { 32 const pub = publication.value as any; 33 const content = html` 34 <div class="publication"> 35 <h1>Your Publication</h1> 36 37 <div class="pub-details"> 38 <h2>${pub.name}</h2> 39 <p class="url"> 40 <a href="${pub.url}" target="_blank">${pub.url}</a> 41 </p> 42 ${ 43 pub.description 44 ? html`<p class="description">${pub.description}</p>` 45 : "" 46 } 47 </div> 48 49 <div class="actions"> 50 <a href="/publication/edit" class="btn btn-primary" 51 >Edit Publication</a 52 > 53 </div> 54 </div> 55 `; 56 return c.html( 57 layout(content, { title: "Publication - sitebase", session }), 58 ); 59 } 60 61 // No publication exists, show create form 62 return c.redirect("/publication/new"); 63 } catch (error) { 64 console.error("Error fetching publication:", error); 65 return c.redirect("/publication/new"); 66 } 67}); 68 69// New publication form 70publicationRoutes.get("/new", async (c) => { 71 let session: Session; 72 try { 73 session = requireAuth(c); 74 } catch { 75 return c.redirect("/auth/login"); 76 } 77 78 const csrfToken = c.get("csrfToken") as string; 79 80 const content = html` 81 <div class="form-page"> 82 <h1>Create Publication</h1> 83 84 <form action="/publication/new" method="POST"> 85 ${csrfField(csrfToken)} 86 <div class="form-group"> 87 <label for="name">Name *</label> 88 <input type="text" id="name" name="name" required maxlength="128" /> 89 </div> 90 91 <div class="form-group"> 92 <label for="url">URL *</label> 93 <input 94 type="url" 95 id="url" 96 name="url" 97 placeholder="https://yourblog.com" 98 required 99 /> 100 <small 101 >The base URL of your publication (without trailing slash)</small 102 > 103 </div> 104 105 <div class="form-group"> 106 <label for="description">Description</label> 107 <textarea 108 id="description" 109 name="description" 110 rows="3" 111 maxlength="300" 112 ></textarea> 113 </div> 114 115 <button type="submit" class="btn btn-primary"> 116 Create Publication 117 </button> 118 </form> 119 </div> 120 `; 121 122 return c.html( 123 layout(content, { title: "New Publication - sitebase", session }), 124 ); 125}); 126 127// Handle publication creation 128publicationRoutes.post("/new", async (c) => { 129 let session: Session; 130 try { 131 session = requireAuth(c); 132 } catch { 133 return c.redirect("/auth/login"); 134 } 135 136 const body = await c.req.parseBody(); 137 const name = body.name as string; 138 const url = (body.url as string).replace(/\/$/, ""); // Remove trailing slash 139 const description = (body.description as string) || undefined; 140 141 try { 142 // Generate a TID for the record key 143 const rkey = generateTID(); 144 145 await session.agent!.com.atproto.repo.createRecord({ 146 repo: session.did!, 147 collection: PUBLICATION_COLLECTION, 148 rkey, 149 record: { 150 $type: PUBLICATION_COLLECTION, 151 name, 152 url, 153 ...(description && { description }), 154 }, 155 }); 156 157 return c.redirect("/publication"); 158 } catch (error) { 159 console.error("Error creating publication:", error); 160 return c.redirect("/publication/new?error=create_failed"); 161 } 162}); 163 164// Edit publication form 165publicationRoutes.get("/edit", async (c) => { 166 let session: Session; 167 try { 168 session = requireAuth(c); 169 } catch { 170 return c.redirect("/auth/login"); 171 } 172 173 try { 174 const response = await session.agent!.com.atproto.repo.listRecords({ 175 repo: session.did!, 176 collection: PUBLICATION_COLLECTION, 177 limit: 1, 178 }); 179 180 const publication = response.data.records[0]; 181 if (!publication) { 182 return c.redirect("/publication/new"); 183 } 184 185 const pub = publication.value as any; 186 const rkey = publication.uri.split("/").pop(); 187 188 const csrfToken = c.get("csrfToken") as string; 189 190 const content = html` 191 <div class="form-page"> 192 <h1>Edit Publication</h1> 193 194 <form action="/publication/edit" method="POST"> 195 ${csrfField(csrfToken)} 196 <input type="hidden" name="rkey" value="${rkey}" /> 197 198 <div class="form-group"> 199 <label for="name">Name *</label> 200 <input 201 type="text" 202 id="name" 203 name="name" 204 value="${pub.name}" 205 required 206 maxlength="128" 207 /> 208 </div> 209 210 <div class="form-group"> 211 <label for="url">URL *</label> 212 <input type="url" id="url" name="url" value="${pub.url}" required /> 213 </div> 214 215 <div class="form-group"> 216 <label for="description">Description</label> 217 <textarea 218 id="description" 219 name="description" 220 rows="3" 221 maxlength="300" 222 > 223${pub.description || ""}</textarea 224 > 225 </div> 226 227 <button type="submit" class="btn btn-primary">Save Changes</button> 228 <a href="/publication" class="btn btn-secondary">Cancel</a> 229 </form> 230 </div> 231 `; 232 233 return c.html( 234 layout(content, { title: "Edit Publication - sitebase", session }), 235 ); 236 } catch (error) { 237 console.error("Error fetching publication:", error); 238 return c.redirect("/publication"); 239 } 240}); 241 242// Handle publication update 243publicationRoutes.post("/edit", async (c) => { 244 let session: Session; 245 try { 246 session = requireAuth(c); 247 } catch { 248 return c.redirect("/auth/login"); 249 } 250 251 const body = await c.req.parseBody(); 252 const rkey = body.rkey as string; 253 const name = body.name as string; 254 const url = (body.url as string).replace(/\/$/, ""); 255 const description = (body.description as string) || undefined; 256 257 try { 258 await session.agent!.com.atproto.repo.putRecord({ 259 repo: session.did!, 260 collection: PUBLICATION_COLLECTION, 261 rkey, 262 record: { 263 $type: PUBLICATION_COLLECTION, 264 name, 265 url, 266 ...(description && { description }), 267 }, 268 }); 269 270 return c.redirect("/publication"); 271 } catch (error) { 272 console.error("Error updating publication:", error); 273 return c.redirect("/publication/edit?error=update_failed"); 274 } 275}); 276 277// Generate a TID (timestamp-based ID) 278function generateTID(): string { 279 const now = Date.now() * 1000; // microseconds 280 const clockId = Math.floor(Math.random() * 1024); 281 const tid = (BigInt(now) << 10n) | BigInt(clockId); 282 return tid.toString(36).padStart(13, "0"); 283}