Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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: string = "forum.db") {
if (typeof path !== "string") {
throw new TypeError("Database path must be a string");
}
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
};
}
99 changes: 99 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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");
const content = formData.get("content");
const author = formData.get("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);
}

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]!);

if (isNaN(postId)) {
return new Response(baseHTML("Invalid Post", "<p>Invalid post ID</p>"), {
status: 400,
headers: { "Content-Type": "text/html" },
});
}

const post = getPost(db, postId);

if (!post) {
return new Response(baseHTML("Not Found", "<p>Post not found</p>"), {
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]!);

if (isNaN(postId)) {
return new Response("Invalid post ID", { status: 400 });
}

const formData = await req.formData();
const content = formData.get("content");
const author = formData.get("author");

if (typeof content === "string" && typeof author === "string" && 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", "<h2>Page Not Found</h2><p>The page you're looking for doesn't exist.</p><br><a href='/' class='btn'>Go Home</a>"), {
status: 404,
headers: { "Content-Type": "text/html" },
});
},
});

console.log(`🚀 OpenDiscuss forum is running at http://localhost:${PORT}`);
Loading