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 61a234b52b0d602c516e23eabd1dfe5a0f87fca6 118 lines 3.9 kB view raw
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 &#9776; 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};