From 560e9e877c1f48b754e3bc1f99414c6930584faf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:44:32 +0000 Subject: [PATCH 1/4] Initial plan From 4dbe1008ea32554b66a04a529e96589693e3846a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:46:28 +0000 Subject: [PATCH 2/4] Initial exploration of repository structure Co-authored-by: duckeydev <171411578+duckeydev@users.noreply.github.com> --- bun.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/bun.lock b/bun.lock index 4a88db6..59648e2 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "opendiscuss", From 4e5d5858fad1d2be5dcc638fccafc6795d89c148 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:51:12 +0000 Subject: [PATCH 3/4] Create forum with Bun and modern UI Co-authored-by: duckeydev <171411578+duckeydev@users.noreply.github.com> --- .gitignore | 5 + README.md | 44 ++++++- database.ts | 91 ++++++++++++++ index.ts | 86 +++++++++++++ templates.ts | 337 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 database.ts create mode 100644 templates.ts diff --git a/.gitignore b/.gitignore index a14702c..b3adcef 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json # Finder (MacOS) folder config .DS_Store + +# SQLite databases +*.db +*.db-shm +*.db-wal diff --git a/README.md b/README.md index 7a5f062..4a5ea12 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,53 @@ -# opendiscuss +# OpenDiscuss 💬 -To install dependencies: +A modern, fast forum built with [Bun](https://bun.com) featuring a beautiful gradient UI. + +## Features + +- ✨ Create and read discussion posts +- 💬 Add comments to posts +- 🎨 Modern, responsive UI with gradient design +- ⚡ Lightning-fast performance with Bun +- 💾 SQLite database for data persistence +- 🚀 No external dependencies for the UI (pure CSS) + +## Installation + +Install dependencies: ```bash bun install ``` -To run: +## Running the Forum + +Start the server: ```bash bun run index.ts ``` +The forum will be available at `http://localhost:3000` + +## Usage + +1. **Home Page**: View all posts in reverse chronological order +2. **Create Post**: Click "Create New Post" to start a new discussion +3. **View Post**: Click "Read More" on any post to view the full content and comments +4. **Add Comment**: On a post detail page, fill in the comment form to join the discussion + +## Project Structure + +- `index.ts` - Main server application with routes +- `database.ts` - SQLite database functions for posts and comments +- `templates.ts` - HTML templates with embedded CSS +- `utils/sqliteKV.ts` - Key-value store utility (optional) + +## Technology Stack + +- **Runtime**: Bun +- **Database**: SQLite (built into Bun) +- **Frontend**: Server-side rendered HTML with CSS +- **Backend**: Bun's native HTTP server + This project was created using `bun init` in bun v1.2.21. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/database.ts b/database.ts new file mode 100644 index 0000000..9c80a58 --- /dev/null +++ b/database.ts @@ -0,0 +1,91 @@ +import { Database } from "bun:sqlite"; + +export interface Post { + id: number; + title: string; + content: string; + author: string; + createdAt: string; +} + +export interface Comment { + id: number; + postId: number; + content: string; + author: string; + createdAt: string; +} + +export function initDatabase(path = "forum.db") { + const db = new Database(path); + + // Create posts table + db.run(` + CREATE TABLE IF NOT EXISTS posts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + content TEXT NOT NULL, + author TEXT NOT NULL, + createdAt TEXT NOT NULL + ) + `); + + // Create comments table + db.run(` + CREATE TABLE IF NOT EXISTS comments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + postId INTEGER NOT NULL, + content TEXT NOT NULL, + author TEXT NOT NULL, + createdAt TEXT NOT NULL, + FOREIGN KEY (postId) REFERENCES posts(id) + ) + `); + + return db; +} + +export function getAllPosts(db: Database): Post[] { + const posts = db.query("SELECT * FROM posts ORDER BY createdAt DESC").all() as Post[]; + return posts; +} + +export function getPost(db: Database, id: number): Post | null { + const post = db.query("SELECT * FROM posts WHERE id = ?").get(id) as Post | null; + return post; +} + +export function createPost(db: Database, title: string, content: string, author: string): Post { + const createdAt = new Date().toISOString(); + const result = db.run( + "INSERT INTO posts (title, content, author, createdAt) VALUES (?, ?, ?, ?)", + [title, content, author, createdAt] + ); + return { + id: Number(result.lastInsertRowid), + title, + content, + author, + createdAt + }; +} + +export function getComments(db: Database, postId: number): Comment[] { + const comments = db.query("SELECT * FROM comments WHERE postId = ? ORDER BY createdAt ASC").all(postId) as Comment[]; + return comments; +} + +export function createComment(db: Database, postId: number, content: string, author: string): Comment { + const createdAt = new Date().toISOString(); + const result = db.run( + "INSERT INTO comments (postId, content, author, createdAt) VALUES (?, ?, ?, ?)", + [postId, content, author, createdAt] + ); + return { + id: Number(result.lastInsertRowid), + postId, + content, + author, + createdAt + }; +} diff --git a/index.ts b/index.ts index e69de29..47b4a7a 100644 --- a/index.ts +++ b/index.ts @@ -0,0 +1,86 @@ +import { initDatabase, getAllPosts, getPost, createPost, getComments, createComment } from "./database"; +import { baseHTML, homePageContent, newPostFormContent, postDetailContent } from "./templates"; + +const db = initDatabase(); +const PORT = 3000; + +const server = Bun.serve({ + port: PORT, + async fetch(req) { + const url = new URL(req.url); + const path = url.pathname; + + // Home page - list all posts + if (path === "/" && req.method === "GET") { + const posts = getAllPosts(db); + return new Response(baseHTML("Home", homePageContent(posts)), { + headers: { "Content-Type": "text/html" }, + }); + } + + // New post form + if (path === "/new" && req.method === "GET") { + return new Response(baseHTML("Create New Post", newPostFormContent()), { + headers: { "Content-Type": "text/html" }, + }); + } + + // Create new post + if (path === "/new" && req.method === "POST") { + const formData = await req.formData(); + const title = formData.get("title") as string; + const content = formData.get("content") as string; + const author = formData.get("author") as string; + + if (title && content && author) { + createPost(db, title, content, author); + return Response.redirect(url.origin + "/", 303); + } + + return new Response("Missing required fields", { status: 400 }); + } + + // View single post + const postMatch = path.match(/^\/post\/(\d+)$/); + if (postMatch && req.method === "GET") { + const postId = parseInt(postMatch[1] || "0"); + const post = getPost(db, postId); + + if (!post) { + return new Response(baseHTML("Not Found", "

Post not found

"), { + status: 404, + headers: { "Content-Type": "text/html" }, + }); + } + + const comments = getComments(db, postId); + return new Response(baseHTML(post.title, postDetailContent(post, comments)), { + headers: { "Content-Type": "text/html" }, + }); + } + + // Add comment to post + const commentMatch = path.match(/^\/post\/(\d+)\/comment$/); + if (commentMatch && req.method === "POST") { + const postId = parseInt(commentMatch[1] || "0"); + const formData = await req.formData(); + const content = formData.get("content") as string; + const author = formData.get("author") as string; + + if (content && author) { + createComment(db, postId, content, author); + return Response.redirect(url.origin + `/post/${postId}`, 303); + } + + return new Response("Missing required fields", { status: 400 }); + } + + // 404 for everything else + return new Response(baseHTML("404", "

Page Not Found

The page you're looking for doesn't exist.


Go Home"), { + status: 404, + headers: { "Content-Type": "text/html" }, + }); + }, +}); + +console.log(`🚀 OpenDiscuss forum is running at http://localhost:${PORT}`); diff --git a/templates.ts b/templates.ts new file mode 100644 index 0000000..435f71d --- /dev/null +++ b/templates.ts @@ -0,0 +1,337 @@ +export const baseHTML = (title: string, content: string) => ` + + + + + + ${title} - OpenDiscuss + + + +
+
+

💬 OpenDiscuss

+

A modern forum built with Bun

+
+
+ ${content} +
+
+ + +`; + +export const homePageContent = (posts: any[]) => { + const postsHTML = posts.length > 0 + ? posts.map(post => { + const preview = post.content.length > 200 + ? post.content.substring(0, 200) + '...' + : post.content; + const date = new Date(post.createdAt).toLocaleString(); + + return ` +
+

${escapeHtml(post.title)}

+
+ Posted by ${escapeHtml(post.author)} on ${date} +
+

${escapeHtml(preview)}

+ Read More → +
+ `; + }).join('') + : ` +
+
📝
+

No posts yet

+

Be the first to start a discussion!

+
+ `; + + return ` +
+ ✨ Create New Post +
+ ${postsHTML} + `; +}; + +export const newPostFormContent = () => ` +

Create New Post

+
+
+ + +
+
+ + +
+
+ + +
+
+ + Cancel +
+
+`; + +export const postDetailContent = (post: any, comments: any[]) => { + const date = new Date(post.createdAt).toLocaleString(); + + const commentsHTML = comments.length > 0 + ? comments.map(comment => { + const commentDate = new Date(comment.createdAt).toLocaleString(); + return ` +
+
${escapeHtml(comment.author)}
+
${commentDate}
+
${escapeHtml(comment.content)}
+
+ `; + }).join('') + : '

No comments yet. Be the first to comment!

'; + + return ` +
+ ← Back to Home +
+ +

${escapeHtml(post.title)}

+
+ Posted by ${escapeHtml(post.author)} on ${date} +
+ +
+ ${escapeHtml(post.content)} +
+ +

Comments (${comments.length})

+ +
+
+ + +
+
+ + +
+ +
+ + ${commentsHTML} + `; +}; + +function escapeHtml(text: string): string { + const map: Record = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, (m) => map[m] || m); +} From 4871b6b614773520b24a5bb5d295a7c36ae75f40 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 19 Jan 2026 05:53:25 +0000 Subject: [PATCH 4/4] Add input validation and improve error handling Co-authored-by: duckeydev <171411578+duckeydev@users.noreply.github.com> --- database.ts | 5 ++++- index.ts | 31 ++++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/database.ts b/database.ts index 9c80a58..30f60eb 100644 --- a/database.ts +++ b/database.ts @@ -16,7 +16,10 @@ export interface Comment { createdAt: string; } -export function initDatabase(path = "forum.db") { +export function initDatabase(path: string = "forum.db") { + if (typeof path !== "string") { + throw new TypeError("Database path must be a string"); + } const db = new Database(path); // Create posts table diff --git a/index.ts b/index.ts index 47b4a7a..4d738f1 100644 --- a/index.ts +++ b/index.ts @@ -28,11 +28,11 @@ const server = Bun.serve({ // Create new post if (path === "/new" && req.method === "POST") { const formData = await req.formData(); - const title = formData.get("title") as string; - const content = formData.get("content") as string; - const author = formData.get("author") as string; + const title = formData.get("title"); + const content = formData.get("content"); + const author = formData.get("author"); - if (title && content && author) { + if (typeof title === "string" && typeof content === "string" && typeof author === "string" && title && content && author) { createPost(db, title, content, author); return Response.redirect(url.origin + "/", 303); } @@ -43,7 +43,15 @@ const server = Bun.serve({ // View single post const postMatch = path.match(/^\/post\/(\d+)$/); if (postMatch && req.method === "GET") { - const postId = parseInt(postMatch[1] || "0"); + const postId = parseInt(postMatch[1]!); + + if (isNaN(postId)) { + return new Response(baseHTML("Invalid Post", "

Invalid post ID

"), { + status: 400, + headers: { "Content-Type": "text/html" }, + }); + } + const post = getPost(db, postId); if (!post) { @@ -62,12 +70,17 @@ const server = Bun.serve({ // Add comment to post const commentMatch = path.match(/^\/post\/(\d+)\/comment$/); if (commentMatch && req.method === "POST") { - const postId = parseInt(commentMatch[1] || "0"); + const postId = parseInt(commentMatch[1]!); + + if (isNaN(postId)) { + return new Response("Invalid post ID", { status: 400 }); + } + const formData = await req.formData(); - const content = formData.get("content") as string; - const author = formData.get("author") as string; + const content = formData.get("content"); + const author = formData.get("author"); - if (content && author) { + if (typeof content === "string" && typeof author === "string" && content && author) { createComment(db, postId, content, author); return Response.redirect(url.origin + `/post/${postId}`, 303); }