This commit is contained in:
Chris Wanstrath 2025-09-08 13:48:54 -07:00
parent f20e280b19
commit 5d7f27e296
6 changed files with 189 additions and 0 deletions

View File

@ -0,0 +1 @@
../../CLAUDE.md

34
packages/nose-bbs/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@ -0,0 +1,6 @@
# nose-bbs
## Quickstart
bun install
bun dev

View File

@ -0,0 +1,20 @@
{
"name": "nose-bbs",
"module": "index.ts",
"type": "module",
"private": true,
"scripts": {
"dev": "bun subdomain:dev",
"subdomain:start": "bun run src/server.tsx",
"subdomain:dev": "bun run --hot src/server.tsx"
},
"dependencies": {
"hono": "catalog:"
},
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}

View File

@ -0,0 +1,99 @@
import { Hono } from "hono"
import { Database } from "bun:sqlite"
import { join } from "path"
export const DATA_DIR = process.env.DATA_DIR || "."
const api = new Hono()
const db = new Database(join(DATA_DIR, "bbs.sqlite"))
api.use("*", async (c, next) => {
const method = c.req.method
const url = c.req.url
let body = ""
if (method === "POST" || method === "PUT" || method === "PATCH") {
try {
const clonedRequest = c.req.raw.clone()
body = await clonedRequest.text()
console.log(`${method} ${url} - Body: ${body}`)
} catch (error) {
console.log(`${method} ${url} - Body: [unable to parse]`)
}
} else {
console.log(`${method} ${url}`)
}
await next()
})
db.run(`
CREATE TABLE IF NOT EXISTS topics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
author TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS replies (
id INTEGER PRIMARY KEY AUTOINCREMENT,
topic_id INTEGER NOT NULL,
content TEXT NOT NULL,
author TEXT NOT NULL,
created_at TEXT NOT NULL
);
`)
api.get("/topics", async c => {
const topics = db.query("SELECT * FROM topics ORDER BY id DESC").all()
return c.json(topics)
})
api.post("/topics", async c => {
const { title, content, author } = await c.req.json()
if (!title || !content || !author) {
return c.json({ error: "title, content and author are required" }, 400)
}
const topic = db.query(`
INSERT INTO
topics(title, content, author, created_at)
VALUES($title, $content, $author, $created_at)
`).run({ $title: title, $content: content, $author: author, $created_at: new Date().toISOString() })
return c.json({ id: topic.lastInsertRowid, ok: true })
})
api.get("/topics/:id", async c => {
const id = c.req.param("id")
if (!id) {
return c.json({ error: "id is required" }, 400)
}
let topic = db.query("SELECT * FROM topics WHERE id = $id").get({ $id: id })
if (!topic) {
return c.json({ error: "topic not found" }, 404)
}
const replies = db.query("SELECT * FROM replies WHERE topic_id = $id").all({ $id: id })
; (topic as any).replies = replies
return c.json(topic)
})
api.post("/topics/:id/reply", async c => {
const id = c.req.param("id")
if (!id) {
return c.json({ error: "id is required" }, 400)
}
const { content, author } = await c.req.json()
const reply = db.query(`
INSERT INTO replies (topic_id, content, author, created_at)
VALUES ($topic_id, $content, $author, $created_at)
`).run({ $topic_id: id, $content: content, $author: author, $created_at: new Date().toISOString() })
return c.json({ id: reply.lastInsertRowid, ok: true })
})
export default {
port: process.env.PORT || 3001,
fetch: api.fetch,
}

View File

@ -0,0 +1,29 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}