Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

fmt

+230 -99
+3 -1
tui/screens/activity.py
··· 120 120 return 121 121 122 122 for item in self._items[:50]: 123 - title = item["thread_title"] if item["type"] == "reply" else "quoted your reply" 123 + title = ( 124 + item["thread_title"] if item["type"] == "reply" else "quoted your reply" 125 + ) 124 126 if item["type"] == "reply": 125 127 title = f"on: {title}" 126 128 await scroll.mount(
+3 -1
tui/screens/home.py
··· 40 40 hero.append("▀▀ ▀▀ ▀▀") 41 41 yield Static(hero, id="hero-title") 42 42 yield Static( 43 - "Bulletin boards on the AT Protocol.", classes="subtitle", id="hero-sub1" 43 + "Bulletin boards on the AT Protocol.", 44 + classes="subtitle", 45 + id="hero-sub1", 44 46 ) 45 47 yield Static( 46 48 "Build a community from your existing account. Tightly curated, fully portable, open by design.",
+1 -3
tui/screens/sysop/delete.py
··· 76 76 for record in records: 77 77 rkey = AtUri.parse(record["uri"]).rkey 78 78 try: 79 - await delete_record( 80 - client, session, collection, rkey, updater 81 - ) 79 + await delete_record(client, session, collection, rkey, updater) 82 80 except Exception: 83 81 failed.append(f"{collection}/{rkey}") 84 82 except Exception:
+3
tui/screens/sysop/menu.py
··· 29 29 name = event.item.name 30 30 if name == "edit": 31 31 from tui.screens.sysop.edit import SysopEditScreen 32 + 32 33 self.app.push_screen(SysopEditScreen(self.bbs, self.handle)) 33 34 elif name == "moderate": 34 35 from tui.screens.sysop.moderate import SysopModerateScreen 36 + 35 37 self.app.push_screen(SysopModerateScreen(self.bbs, self.handle)) 36 38 elif name == "delete": 37 39 from tui.screens.sysop.delete import SysopDeleteScreen 40 + 38 41 self.app.push_screen(SysopDeleteScreen(self.bbs, self.handle))
+6 -2
tui/screens/thread.py
··· 148 148 quote_text = None 149 149 if reply.quote and reply.quote in self._replies_map: 150 150 quoted = self._replies_map[reply.quote] 151 - body_preview = quoted.body[:200] + ("..." if len(quoted.body) > 200 else "") 151 + body_preview = quoted.body[:200] + ( 152 + "..." if len(quoted.body) > 200 else "" 153 + ) 152 154 quote_text = f"{quoted.author.handle}: {body_preview}" 153 155 154 156 await scroll.mount( ··· 167 169 ) 168 170 169 171 # Focus first reply 170 - replies = [post for post in self.query(Post) if post.collection == lexicon.REPLY] 172 + replies = [ 173 + post for post in self.query(Post) if post.collection == lexicon.REPLY 174 + ] 171 175 if replies: 172 176 replies[0].focus() 173 177
+21 -9
web/src/components/BBSPanel.tsx
··· 11 11 onDelete: () => void; 12 12 } 13 13 14 - export default function BBSPanel({ hasBBS, userHandle, onDelete }: BBSPanelProps) { 14 + export default function BBSPanel({ 15 + hasBBS, 16 + userHandle, 17 + onDelete, 18 + }: BBSPanelProps) { 15 19 if (!hasBBS) { 16 20 return ( 17 21 <> 18 - <p className="text-neutral-400 mb-4"> 19 - No BBS yet. 20 - </p> 21 - <ActionLink to="/account/create" icon={Plus}>create a bbs</ActionLink> 22 + <p className="text-neutral-400 mb-4">No BBS yet.</p> 23 + <ActionLink to="/account/create" icon={Plus}> 24 + create a bbs 25 + </ActionLink> 22 26 </> 23 27 ); 24 28 } ··· 26 30 return ( 27 31 <div className="grid grid-cols-2 gap-3 max-w-md"> 28 32 <Link to={`/bbs/${userHandle}`} className={cardStyle}> 29 - <div className="flex items-center gap-2 text-neutral-200 mb-1"><Monitor size={14} /> Browse</div> 33 + <div className="flex items-center gap-2 text-neutral-200 mb-1"> 34 + <Monitor size={14} /> Browse 35 + </div> 30 36 <div className="text-xs text-neutral-400">View your BBS.</div> 31 37 </Link> 32 38 <Link to="/account/edit" className={cardStyle}> 33 - <div className="flex items-center gap-2 text-neutral-200 mb-1"><Pencil size={14} /> Edit</div> 39 + <div className="flex items-center gap-2 text-neutral-200 mb-1"> 40 + <Pencil size={14} /> Edit 41 + </div> 34 42 <div className="text-xs text-neutral-400">Name, boards, intro.</div> 35 43 </Link> 36 44 <Link to="/account/moderate" className={cardStyle}> 37 - <div className="flex items-center gap-2 text-neutral-200 mb-1"><Shield size={14} /> Moderate</div> 45 + <div className="flex items-center gap-2 text-neutral-200 mb-1"> 46 + <Shield size={14} /> Moderate 47 + </div> 38 48 <div className="text-xs text-neutral-400">Bans and hidden posts.</div> 39 49 </Link> 40 50 <button 41 51 onClick={onDelete} 42 52 className="text-left bg-neutral-900 border border-neutral-800 rounded px-4 py-3 hover:border-red-900" 43 53 > 44 - <div className="flex items-center gap-2 text-neutral-400 mb-1"><Trash2 size={14} /> Delete</div> 54 + <div className="flex items-center gap-2 text-neutral-400 mb-1"> 55 + <Trash2 size={14} /> Delete 56 + </div> 45 57 <div className="text-xs text-neutral-400">Remove your BBS.</div> 46 58 </button> 47 59 </div>
+10 -3
web/src/components/DialBBS.tsx
··· 93 93 } 94 94 aria-label="Dial a BBS by handle" 95 95 /> 96 - <Button type="submit"><ArrowRight size={16} /></Button> 97 - <Button type="button" onClick={onRandom}><Dices size={16} /></Button> 96 + <Button type="submit"> 97 + <ArrowRight size={16} /> 98 + </Button> 99 + <Button type="button" onClick={onRandom}> 100 + <Dices size={16} /> 101 + </Button> 98 102 </form> 99 103 {dropdownOpen && ( 100 104 <div className="relative"> 101 - <div role="listbox" className="absolute left-0 right-0 mt-1 bg-neutral-900 border border-neutral-800 rounded shadow-lg z-10"> 105 + <div 106 + role="listbox" 107 + className="absolute left-0 right-0 mt-1 bg-neutral-900 border border-neutral-800 rounded shadow-lg z-10" 108 + > 102 109 {visibleSuggestions.map((entry, index) => ( 103 110 <Link 104 111 key={entry.to}
+1 -5
web/src/components/PinnedList.tsx
··· 13 13 const [shown, setShown] = useState(PAGE_SIZE); 14 14 15 15 if (pins.length === 0) 16 - return ( 17 - <p className="text-neutral-400"> 18 - No pinned BBSes yet. 19 - </p> 20 - ); 16 + return <p className="text-neutral-400">No pinned BBSes yet.</p>; 21 17 22 18 return ( 23 19 <div className="space-y-1">
+24 -15
web/src/components/form/ComposeForm.tsx
··· 45 45 }: ComposeFormProps) { 46 46 function addFiles(fileList: FileList | null) { 47 47 if (!fileList) return; 48 - const combined = [...files, ...Array.from(fileList)].slice(0, MAX_ATTACHMENTS); 48 + const combined = [...files, ...Array.from(fileList)].slice( 49 + 0, 50 + MAX_ATTACHMENTS, 51 + ); 49 52 onFilesChange(combined); 50 53 } 51 54 ··· 98 101 maxLength={bodyMaxLength} 99 102 /> 100 103 101 - {files.length > 0 && ( 102 - <FileChips files={files} onRemove={removeFile} /> 103 - )} 104 + {files.length > 0 && <FileChips files={files} onRemove={removeFile} />} 104 105 105 106 <div className="flex items-center gap-3"> 106 107 <Button type="submit" disabled={posting}> 107 - {posting ? "posting..." : <><Send size={14} className="inline -mt-0.5" /> {submitLabel}</>} 108 + {posting ? ( 109 + "posting..." 110 + ) : ( 111 + <> 112 + <Send size={14} className="inline -mt-0.5" /> {submitLabel} 113 + </> 114 + )} 108 115 </Button> 109 116 {!attachmentsAtLimit && ( 110 - <label className="text-neutral-200 cursor-pointer bg-neutral-800 hover:bg-neutral-700 px-4 py-2 rounded inline-block"> 111 - <span className="inline-flex items-center gap-1.5"><Paperclip size={14} /> attach</span> 112 - <input 113 - name="attachments" 114 - type="file" 115 - multiple 116 - onChange={(e) => addFiles(e.target.files)} 117 - className="hidden" 118 - /> 119 - </label> 117 + <label className="text-neutral-200 cursor-pointer bg-neutral-800 hover:bg-neutral-700 px-4 py-2 rounded inline-block"> 118 + <span className="inline-flex items-center gap-1.5"> 119 + <Paperclip size={14} /> attach 120 + </span> 121 + <input 122 + name="attachments" 123 + type="file" 124 + multiple 125 + onChange={(e) => addFiles(e.target.files)} 126 + className="hidden" 127 + /> 128 + </label> 120 129 )} 121 130 </div> 122 131 </form>
+7 -2
web/src/components/layout/Footer.tsx
··· 10 10 <footer className="border-t border-neutral-800 mt-auto"> 11 11 <div className="max-w-2xl mx-auto px-4 py-4 flex items-center justify-between text-xs text-neutral-400"> 12 12 <span> 13 - made by <a href="https://aly.codes" className={linkStyle}>aly.codes</a> 13 + made by{" "} 14 + <a href="https://aly.codes" className={linkStyle}> 15 + aly.codes 16 + </a> 14 17 </span> 15 18 <div className="flex items-center gap-4"> 16 19 {links.map(({ href, label }) => ( 17 - <a key={label} href={href} className={linkStyle}>{label}</a> 20 + <a key={label} href={href} className={linkStyle}> 21 + {label} 22 + </a> 18 23 ))} 19 24 </div> 20 25 </div>
+12 -3
web/src/components/layout/Header.tsx
··· 28 28 <div className="hidden md:flex items-center gap-3 shrink-0 ml-4"> 29 29 {user ? ( 30 30 <> 31 - <Link to={`/profile/${encodeURIComponent(user.handle)}`} className={linkStyle}>{user.handle}</Link> 32 - <button type="button" onClick={onLogout} className={linkStyle}>log out</button> 31 + <Link 32 + to={`/profile/${encodeURIComponent(user.handle)}`} 33 + className={linkStyle} 34 + > 35 + {user.handle} 36 + </Link> 37 + <button type="button" onClick={onLogout} className={linkStyle}> 38 + log out 39 + </button> 33 40 </> 34 41 ) : ( 35 - <Link to="/login" className={linkStyle}>log in</Link> 42 + <Link to="/login" className={linkStyle}> 43 + log in 44 + </Link> 36 45 )} 37 46 </div> 38 47 <MobileMenu user={user} onLogout={onLogout} />
+1 -4
web/src/components/layout/HeaderBreadcrumbs.tsx
··· 20 20 {crumb.label} 21 21 </Link> 22 22 ) : ( 23 - <span 24 - key={`crumb-${index}`} 25 - className="text-neutral-400 truncate" 26 - > 23 + <span key={`crumb-${index}`} className="text-neutral-400 truncate"> 27 24 {crumb.label} 28 25 </span> 29 26 );
+18 -3
web/src/components/layout/MobileMenu.tsx
··· 37 37 <div className={panelStyle}> 38 38 {user ? ( 39 39 <> 40 - <Link to={`/profile/${encodeURIComponent(user.handle)}`} onClick={close} className="text-neutral-300 hover:text-neutral-200"> 40 + <Link 41 + to={`/profile/${encodeURIComponent(user.handle)}`} 42 + onClick={close} 43 + className="text-neutral-300 hover:text-neutral-200" 44 + > 41 45 {user.handle} 42 46 </Link> 43 - <button type="button" onClick={() => { close(); onLogout(); }} className="text-neutral-400 hover:text-neutral-300"> 47 + <button 48 + type="button" 49 + onClick={() => { 50 + close(); 51 + onLogout(); 52 + }} 53 + className="text-neutral-400 hover:text-neutral-300" 54 + > 44 55 log out 45 56 </button> 46 57 </> 47 58 ) : ( 48 - <Link to="/login" onClick={close} className="text-neutral-300 hover:text-neutral-200"> 59 + <Link 60 + to="/login" 61 + onClick={close} 62 + className="text-neutral-300 hover:text-neutral-200" 63 + > 49 64 log in 50 65 </Link> 51 66 )}
+6 -1
web/src/components/nav/ActionButton.tsx
··· 32 32 ); 33 33 } 34 34 35 - export function ActionLink({ to, children, icon: Icon, className }: ActionLinkProps) { 35 + export function ActionLink({ 36 + to, 37 + children, 38 + icon: Icon, 39 + className, 40 + }: ActionLinkProps) { 36 41 return ( 37 42 <Link to={to} className={`${actionStyle} ${className ?? ""}`}> 38 43 {Icon && <Icon size={14} />}
+3 -1
web/src/components/profile/EditProfile.tsx
··· 63 63 /> 64 64 </div> 65 65 <div className="flex gap-2"> 66 - <Button onClick={handleSubmit}><Check size={14} className="inline -mt-0.5" /> save</Button> 66 + <Button onClick={handleSubmit}> 67 + <Check size={14} className="inline -mt-0.5" /> save 68 + </Button> 67 69 <button 68 70 onClick={onCancel} 69 71 className="inline-flex items-center gap-1.5 text-neutral-400 hover:text-neutral-300 text-xs"
+7 -2
web/src/components/profile/ViewProfile.tsx
··· 24 24 {profile?.name ?? handle} 25 25 </h1> 26 26 {isOwner && ( 27 - <ActionButton onClick={onEdit} icon={Pencil}>edit</ActionButton> 27 + <ActionButton onClick={onEdit} icon={Pencil}> 28 + edit 29 + </ActionButton> 28 30 )} 29 31 </div> 30 32 <p className="text-neutral-400"> ··· 58 60 </div> 59 61 )} 60 62 </div> 61 - <ChevronRight size={18} className="text-neutral-400 group-hover:text-neutral-300 ml-4" /> 63 + <ChevronRight 64 + size={18} 65 + className="text-neutral-400 group-hover:text-neutral-300 ml-4" 66 + /> 62 67 </Link> 63 68 </div> 64 69 )}
+6 -7
web/src/hooks/useDropdown.ts
··· 2 2 3 3 import { useRef, useState } from "react"; 4 4 5 - export function useDropdown(optionCount: number, onSelect: (index: number) => void) { 5 + export function useDropdown( 6 + optionCount: number, 7 + onSelect: (index: number) => void, 8 + ) { 6 9 const [focused, setFocused] = useState(false); 7 10 const [activeIndex, setActiveIndex] = useState(-1); 8 11 const blurTimeout = useRef<ReturnType<typeof setTimeout>>(undefined); ··· 26 29 27 30 if (event.key === "ArrowDown") { 28 31 event.preventDefault(); 29 - setActiveIndex((prev) => 30 - prev < optionCount - 1 ? prev + 1 : 0, 31 - ); 32 + setActiveIndex((prev) => (prev < optionCount - 1 ? prev + 1 : 0)); 32 33 } else if (event.key === "ArrowUp") { 33 34 event.preventDefault(); 34 - setActiveIndex((prev) => 35 - prev > 0 ? prev - 1 : optionCount - 1, 36 - ); 35 + setActiveIndex((prev) => (prev > 0 ? prev - 1 : optionCount - 1)); 37 36 } else if (event.key === "Enter" && activeIndex >= 0) { 38 37 event.preventDefault(); 39 38 onSelectRef.current(activeIndex);
+3 -1
web/src/hooks/useThreadReplies.ts
··· 145 145 // Drop moderated content. 146 146 const visible = records.filter((r) => { 147 147 const { did } = parseAtUri(r.uri); 148 - return !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri); 148 + return ( 149 + !bbs.site.bannedDids.has(did) && !bbs.site.hiddenPosts.has(r.uri) 150 + ); 149 151 }); 150 152 151 153 // Resolve author handles and build Reply objects.
+1 -5
web/src/lib/pins.ts
··· 1 1 /** Fetch and resolve the user's pinned BBSes. */ 2 2 3 - import { 4 - listRecords, 5 - getRecord, 6 - resolveIdentitiesBatch, 7 - } from "./atproto"; 3 + import { listRecords, getRecord, resolveIdentitiesBatch } from "./atproto"; 8 4 import { PIN, SITE } from "./lexicon"; 9 5 import { is } from "@atcute/lexicons/validations"; 10 6 import { mainSchema as pinSchema } from "../lexicons/types/xyz/atboards/pin";
+2 -1
web/src/lib/profile.ts
··· 42 42 profileResult.status === "fulfilled" && 43 43 is(profileSchema, profileResult.value.value) 44 44 ) { 45 - const value = profileResult.value.value as unknown as XyzAtboardsProfile.Main; 45 + const value = profileResult.value 46 + .value as unknown as XyzAtboardsProfile.Main; 46 47 profile.name = value.name; 47 48 profile.pronouns = value.pronouns; 48 49 profile.bio = value.bio;
+12 -2
web/src/lib/writes.ts
··· 1 1 /** Authenticated PDS write helpers using an atcute Client from useAuth().agent. */ 2 2 3 3 import type { Client } from "@atcute/client"; 4 - import { SITE, BOARD, NEWS, THREAD, REPLY, BAN, HIDE, PIN, PROFILE } from "./lexicon"; 4 + import { 5 + SITE, 6 + BOARD, 7 + NEWS, 8 + THREAD, 9 + REPLY, 10 + BAN, 11 + HIDE, 12 + PIN, 13 + PROFILE, 14 + } from "./lexicon"; 5 15 import { invalidateBBSCache } from "./bbs"; 6 16 import { nowIso } from "./util"; 7 17 import { getCurrentUser } from "./auth"; ··· 281 291 createdAt: nowIso() as ProfileValue["createdAt"], 282 292 }; 283 293 return putRecord(rpc, PROFILE, "self", value); 284 - } 294 + }
+19 -3
web/src/pages/BBS.tsx
··· 13 13 import ActionBar from "../components/nav/ActionBar"; 14 14 import { ActionLink } from "../components/nav/ActionButton"; 15 15 import PinButton from "../components/PinButton"; 16 - import { User, Pencil, Shield, LayoutGrid, Newspaper, Megaphone, ChevronDown } from "lucide-react"; 16 + import { 17 + User, 18 + Pencil, 19 + Shield, 20 + LayoutGrid, 21 + Newspaper, 22 + Megaphone, 23 + ChevronDown, 24 + } from "lucide-react"; 17 25 import type { News } from "../lib/bbs"; 18 26 import type { BBSLoaderData } from "../router/loaders"; 19 27 import PostBody from "../components/post/PostBody"; ··· 87 95 <ActionLink to={`/profile/${encodeURIComponent(handle)}`} icon={User}> 88 96 owner 89 97 </ActionLink> 90 - {isSysop && <ActionLink to="/account/edit" icon={Pencil}>edit</ActionLink>} 91 - {isSysop && <ActionLink to="/account/moderate" icon={Shield}>moderate</ActionLink>} 98 + {isSysop && ( 99 + <ActionLink to="/account/edit" icon={Pencil}> 100 + edit 101 + </ActionLink> 102 + )} 103 + {isSysop && ( 104 + <ActionLink to="/account/moderate" icon={Shield}> 105 + moderate 106 + </ActionLink> 107 + )} 92 108 </ActionBar> 93 109 </div> 94 110
+9 -8
web/src/pages/Dashboard.tsx
··· 70 70 await deleteBBS(agent, user.did, user.pdsUrl); 71 71 revalidator.revalidate(); 72 72 } catch (error) { 73 - alert( 74 - error instanceof Error ? error.message : "Could not delete BBS.", 75 - ); 73 + alert(error instanceof Error ? error.message : "Could not delete BBS."); 76 74 } 77 75 } 78 76 ··· 91 89 <DialBBS discovered={discovered} suggestions={suggestions} /> 92 90 </div> 93 91 94 - <div role="tablist" className="flex gap-4 border-b border-neutral-800 mb-6 overflow-x-auto"> 92 + <div 93 + role="tablist" 94 + className="flex gap-4 border-b border-neutral-800 mb-6 overflow-x-auto" 95 + > 95 96 {tabs.map((entry) => ( 96 97 <button 97 98 key={entry.key} 98 99 role="tab" 99 100 aria-selected={tab === entry.key} 100 101 onClick={() => setTab(entry.key)} 101 - className={tab === entry.key ? TAB_STYLE_ACTIVE : TAB_STYLE_INACTIVE} 102 + className={ 103 + tab === entry.key ? TAB_STYLE_ACTIVE : TAB_STYLE_INACTIVE 104 + } 102 105 > 103 106 {entry.label} 104 107 </button> ··· 148 151 149 152 {tab === "bbs" && ( 150 153 <> 151 - <p className="text-neutral-400 text-xs mb-4"> 152 - Manage your BBS. 153 - </p> 154 + <p className="text-neutral-400 text-xs mb-4">Manage your BBS.</p> 154 155 <BBSPanel 155 156 hasBBS={hasBBS} 156 157 userHandle={user.handle}
+5 -2
web/src/pages/LoggedOutHome.tsx
··· 69 69 </div> 70 70 71 71 <div className="border-t border-neutral-800 py-4"> 72 - <h2 className="text-neutral-300 mb-4 flex items-center gap-2"><Phone size={16} /> Dial a BBS</h2> 72 + <h2 className="text-neutral-300 mb-4 flex items-center gap-2"> 73 + <Phone size={16} /> Dial a BBS 74 + </h2> 73 75 <div className="mb-6"> 74 76 <DialBBS discovered={discovered} suggestions={suggestions} /> 75 77 </div> ··· 94 96 {installCommands[tab].split("\n").map((line, i) => ( 95 97 <span key={`${tab}-${i}`}> 96 98 {i > 0 && "\n"} 97 - <span className="select-none">$ </span>{line} 99 + <span className="select-none">$ </span> 100 + {line} 98 101 </span> 99 102 ))} 100 103 </pre>
+1 -3
web/src/pages/Profile.tsx
··· 49 49 <p className="text-xs text-neutral-400 uppercase tracking-wide mb-3 inline-flex items-center gap-1.5"> 50 50 <MessageSquare size={12} /> Recent Threads 51 51 </p> 52 - <Suspense 53 - fallback={<p className="text-neutral-400">loading...</p>} 54 - > 52 + <Suspense fallback={<p className="text-neutral-400">loading...</p>}> 55 53 <Await resolve={threads}> 56 54 {(resolved: MyThread[]) => ( 57 55 <MyThreadList threads={resolved.slice(0, 5)} />
+19 -5
web/src/pages/SysopCreate.tsx
··· 6 6 import * as limits from "../lib/limits"; 7 7 import { usePageTitle } from "../hooks/usePageTitle"; 8 8 import { Input, Textarea, Button } from "../components/form/Form"; 9 - import BoardRowEditor, { type BoardRow } from "../components/form/BoardRowEditor"; 9 + import BoardRowEditor, { 10 + type BoardRow, 11 + } from "../components/form/BoardRowEditor"; 10 12 import type { AuthUser } from "../lib/auth"; 11 13 12 14 export default function SysopCreate() { ··· 45 47 const now = nowIso(); 46 48 try { 47 49 for (const board of cleanBoards) { 48 - await putBoard(agent, board.slug, board.name || board.slug, board.desc, now); 50 + await putBoard( 51 + agent, 52 + board.slug, 53 + board.name || board.slug, 54 + board.desc, 55 + now, 56 + ); 49 57 } 50 58 await putSite(agent, { 51 59 name: name.trim(), ··· 69 77 {error && <p className="text-red-500 mb-4">{error}</p>} 70 78 <form onSubmit={onSubmit} className="space-y-6"> 71 79 <div> 72 - <label className="text-xs text-neutral-400 uppercase tracking-wide">BBS Name</label> 80 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 81 + BBS Name 82 + </label> 73 83 <Input 74 84 name="name" 75 85 required ··· 80 90 /> 81 91 </div> 82 92 <div> 83 - <label className="text-xs text-neutral-400 uppercase tracking-wide">Description</label> 93 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 94 + Description 95 + </label> 84 96 <Input 85 97 name="description" 86 98 value={description} ··· 90 102 /> 91 103 </div> 92 104 <div> 93 - <label className="text-xs text-neutral-400 uppercase tracking-wide">Welcome Message</label> 105 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 106 + Welcome Message 107 + </label> 94 108 <Textarea 95 109 name="intro" 96 110 rows={6}
+19 -5
web/src/pages/SysopEdit.tsx
··· 6 6 import * as limits from "../lib/limits"; 7 7 import { usePageTitle } from "../hooks/usePageTitle"; 8 8 import { Input, Textarea, Button } from "../components/form/Form"; 9 - import BoardRowEditor, { type BoardRow } from "../components/form/BoardRowEditor"; 9 + import BoardRowEditor, { 10 + type BoardRow, 11 + } from "../components/form/BoardRowEditor"; 10 12 import type { BBS } from "../lib/bbs"; 11 13 import type { AuthUser } from "../lib/auth"; 12 14 ··· 47 49 const now = nowIso(); 48 50 try { 49 51 for (const board of cleanBoards) { 50 - await putBoard(agent, board.slug, board.name || board.slug, board.desc, now); 52 + await putBoard( 53 + agent, 54 + board.slug, 55 + board.name || board.slug, 56 + board.desc, 57 + now, 58 + ); 51 59 } 52 60 await putSite(agent, { 53 61 name: name.trim(), ··· 70 78 {error && <p className="text-red-500 mb-4">{error}</p>} 71 79 <form onSubmit={onSubmit} className="space-y-6"> 72 80 <div> 73 - <label className="text-xs text-neutral-400 uppercase tracking-wide">BBS Name</label> 81 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 82 + BBS Name 83 + </label> 74 84 <Input 75 85 name="name" 76 86 required ··· 80 90 /> 81 91 </div> 82 92 <div> 83 - <label className="text-xs text-neutral-400 uppercase tracking-wide">Description</label> 93 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 94 + Description 95 + </label> 84 96 <Input 85 97 name="description" 86 98 value={description} ··· 89 101 /> 90 102 </div> 91 103 <div> 92 - <label className="text-xs text-neutral-400 uppercase tracking-wide">Welcome Message</label> 104 + <label className="text-xs text-neutral-400 uppercase tracking-wide"> 105 + Welcome Message 106 + </label> 93 107 <Textarea 94 108 name="intro" 95 109 rows={6}
+3 -1
web/src/pages/Thread.tsx
··· 217 217 thread: ThreadObj, 218 218 handle: string, 219 219 ) { 220 - const board = bbs.site.boards.find((board) => board.slug === thread.boardSlug); 220 + const board = bbs.site.boards.find( 221 + (board) => board.slug === thread.boardSlug, 222 + ); 221 223 return [ 222 224 { label: bbs.site.name, to: `/bbs/${handle}` }, 223 225 ...(board
+5 -1
web/src/router/loaders/bbs.ts
··· 17 17 return { handle, bbs, pinRkey }; 18 18 } 19 19 20 - export type BBSLoaderData = { handle: string; bbs: BBS; pinRkey: string | null }; 20 + export type BBSLoaderData = { 21 + handle: string; 22 + bbs: BBS; 23 + pinRkey: string | null; 24 + };