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
1import type { FC, PropsWithChildren } from "hono/jsx";
2import { tokensToCss } from "../lib/theme.js";
3import { sanitizeCss } from "@atbb/css-sanitizer";
4import type { ResolvedTheme } from "../lib/theme-resolution.js";
5import type { WebSession } from "../lib/session.js";
6
7const NavContent: FC<{ auth?: WebSession }> = ({ auth }) => (
8 <>
9 {auth?.authenticated ? (
10 <>
11 <span class="site-header__handle">{auth.handle}</span>
12 <form action="/logout" method="post" class="site-header__logout-form">
13 <button type="submit" class="site-header__logout-btn">
14 Log out
15 </button>
16 </form>
17 </>
18 ) : (
19 <a href="/login" class="site-header__login-link">
20 Log in
21 </a>
22 )}
23 </>
24);
25
26export const BaseLayout: FC<
27 PropsWithChildren<{
28 title?: string;
29 auth?: WebSession;
30 resolvedTheme: ResolvedTheme;
31 }>
32> = (props) => {
33 const { auth, resolvedTheme } = props;
34
35 let rootCss = "";
36 try {
37 rootCss = sanitizeCss(`:root { ${tokensToCss(resolvedTheme.tokens)} }`);
38 } catch (err) {
39 console.error("Failed to sanitize root CSS tokens — rendering without tokens", {
40 error: String(err),
41 });
42 }
43
44 let overridesCss: string | null = null;
45 if (resolvedTheme.cssOverrides) {
46 try {
47 overridesCss = sanitizeCss(resolvedTheme.cssOverrides);
48 } catch (err) {
49 console.error("Failed to sanitize CSS overrides — rendering without overrides", {
50 error: String(err),
51 });
52 }
53 }
54
55 return (
56 <html lang="en">
57 <head>
58 <meta charset="UTF-8" />
59 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
60 <meta http-equiv="Accept-CH" content="Sec-CH-Prefers-Color-Scheme" />
61 <title>{props.title ?? "atBB Forum"}</title>
62 <style dangerouslySetInnerHTML={{ __html: rootCss }} />
63 {overridesCss && (
64 <style dangerouslySetInnerHTML={{ __html: overridesCss }} />
65 )}
66 {resolvedTheme.fontUrls && resolvedTheme.fontUrls.length > 0 && (() => {
67 const safeFontUrls = resolvedTheme.fontUrls!.filter((url) => url.startsWith("https://"));
68 return safeFontUrls.length > 0 ? (
69 <>
70 <link rel="preconnect" href="https://fonts.googleapis.com" />
71 <link
72 rel="preconnect"
73 href="https://fonts.gstatic.com"
74 crossorigin="anonymous"
75 />
76 {safeFontUrls.map((url) => (
77 <link rel="stylesheet" href={url} />
78 ))}
79 </>
80 ) : null;
81 })()}
82 <link rel="stylesheet" href="/static/css/reset.css" />
83 <link rel="stylesheet" href="/static/css/theme.css" />
84 <link rel="icon" type="image/svg+xml" href="/static/favicon.svg" />
85 <script src="https://unpkg.com/htmx.org@2.0.4" defer />
86 </head>
87 <body>
88 <a href="#main-content" class="skip-link">
89 Skip to main content
90 </a>
91 <header class="site-header">
92 <div class="site-header__inner">
93 <a href="/" class="site-header__title">
94 atBB Forum
95 </a>
96 <nav class="desktop-nav" aria-label="Main navigation">
97 <NavContent auth={auth} />
98 </nav>
99 <details class="mobile-nav">
100 <summary class="mobile-nav__toggle" aria-label="Menu">
101 ☰
102 </summary>
103 <nav class="mobile-nav__menu" aria-label="Mobile navigation">
104 <NavContent auth={auth} />
105 </nav>
106 </details>
107 </div>
108 </header>
109 <main id="main-content" class="content-container">
110 {props.children}
111 </main>
112 <footer class="site-footer">
113 <p>Powered by atBB on the ATmosphere</p>
114 </footer>
115 </body>
116 </html>
117 );
118};