WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
4
fork

Configure Feed

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

at main 151 lines 4.9 kB view raw
1import { Hono } from "hono"; 2import { BaseLayout } from "../layouts/base.js"; 3import { 4 PageHeader, 5 EmptyState, 6 ErrorDisplay, 7 Card, 8} from "../components/index.js"; 9import { fetchApi } from "../lib/api.js"; 10import { getSession } from "../lib/session.js"; 11import { isProgrammingError, isNetworkError } from "../lib/errors.js"; 12import { logger } from "../lib/logger.js"; 13import { FALLBACK_THEME, type WebAppEnv } from "../lib/theme-resolution.js"; 14 15// API response type shapes 16interface ForumResponse { 17 id: string; 18 did: string; 19 name: string; 20 description: string | null; 21 indexedAt: string; 22} 23 24interface CategoryResponse { 25 id: string; 26 did: string; 27 name: string; 28 description: string | null; 29 slug: string | null; 30 sortOrder: number | null; 31} 32 33interface BoardResponse { 34 id: string; 35 did: string; 36 name: string; 37 description: string | null; 38 slug: string | null; 39 sortOrder: number | null; 40} 41 42interface CategoriesListResponse { 43 categories: CategoryResponse[]; 44} 45 46interface BoardsListResponse { 47 boards: BoardResponse[]; 48} 49 50 51export function createHomeRoutes(appviewUrl: string) { 52 return new Hono<WebAppEnv>().get("/", async (c) => { 53 const resolvedTheme = c.get("theme") ?? FALLBACK_THEME; 54 const auth = await getSession(appviewUrl, c.req.header("cookie")); 55 56 // Stage 1: fetch forum metadata and category list in parallel 57 let forum: ForumResponse; 58 let categories: CategoryResponse[]; 59 try { 60 const [forumData, categoriesData] = await Promise.all([ 61 fetchApi<ForumResponse>("/forum"), 62 fetchApi<CategoriesListResponse>("/categories"), 63 ]); 64 forum = forumData; 65 categories = categoriesData.categories; 66 } catch (error) { 67 if (isProgrammingError(error)) throw error; 68 logger.error("Failed to load forum homepage data (stage 1: forum + categories)", { 69 operation: "GET /", 70 error: error instanceof Error ? error.message : String(error), 71 }); 72 const status = isNetworkError(error) ? 503 : 500; 73 const message = 74 status === 503 75 ? "The forum is temporarily unavailable. Please try again later." 76 : "Something went wrong loading the forum. Please try again later."; 77 return c.html( 78 <BaseLayout title="Error — atBB Forum" auth={auth} resolvedTheme={resolvedTheme}> 79 <ErrorDisplay message={message} /> 80 </BaseLayout>, 81 status 82 ); 83 } 84 85 // Stage 2: fetch boards for each category in parallel 86 let boardsByCategory: BoardResponse[][]; 87 try { 88 boardsByCategory = await Promise.all( 89 categories.map((cat) => 90 fetchApi<BoardsListResponse>(`/categories/${cat.id}/boards`).then( 91 (data) => data.boards 92 ) 93 ) 94 ); 95 } catch (error) { 96 if (isProgrammingError(error)) throw error; 97 logger.error("Failed to load forum homepage data (stage 2: boards)", { 98 operation: "GET /", 99 error: error instanceof Error ? error.message : String(error), 100 }); 101 const status = isNetworkError(error) ? 503 : 500; 102 const message = 103 status === 503 104 ? "The forum is temporarily unavailable. Please try again later." 105 : "Something went wrong loading the forum. Please try again later."; 106 return c.html( 107 <BaseLayout title="Error — atBB Forum" auth={auth} resolvedTheme={resolvedTheme}> 108 <ErrorDisplay message={message} /> 109 </BaseLayout>, 110 status 111 ); 112 } 113 114 // Build category+boards pairs for rendering 115 const categorySections = categories.map((cat, i) => ({ 116 category: cat, 117 boards: boardsByCategory[i] ?? [], 118 })); 119 120 return c.html( 121 <BaseLayout title={`${forum.name} — atBB Forum`} auth={auth} resolvedTheme={resolvedTheme}> 122 <PageHeader title={forum.name} description={forum.description ?? undefined} /> 123 {categorySections.length === 0 ? ( 124 <EmptyState message="No categories yet." /> 125 ) : ( 126 categorySections.map(({ category, boards }) => ( 127 <section class="category-section" key={category.id}> 128 <h2 class="category-header">{category.name}</h2> 129 <div class="board-grid"> 130 {boards.length === 0 ? ( 131 <EmptyState message="No boards in this category yet." /> 132 ) : ( 133 boards.map((board) => ( 134 <a href={`/boards/${board.id}`} class="board-card" key={board.id}> 135 <Card> 136 <p class="board-card__name">{board.name}</p> 137 {board.description && ( 138 <p class="board-card__description">{board.description}</p> 139 )} 140 </Card> 141 </a> 142 )) 143 )} 144 </div> 145 </section> 146 )) 147 )} 148 </BaseLayout> 149 ); 150 }); 151}