From a809b3c403b5deebce2dabea976ee1b0f37c3a9d Mon Sep 17 00:00:00 2001 From: Dominic Ferrando Date: Fri, 18 Apr 2025 11:21:47 -0400 Subject: [PATCH] Make threads openable from catalog --- .gitignore | 1 + Makefile | 2 +- internal/database/handlers.go | 27 ++++++-- main.go => web/main.go | 116 ++++++++++++++++++++++++---------- web/views/post.templ | 7 -- web/views/posts.templ | 19 ------ web/views/thread.templ | 12 ++++ web/views/threads.templ | 18 ++++++ 8 files changed, 134 insertions(+), 68 deletions(-) rename main.go => web/main.go (53%) delete mode 100644 web/views/post.templ delete mode 100644 web/views/posts.templ create mode 100644 web/views/thread.templ create mode 100644 web/views/threads.templ diff --git a/.gitignore b/.gitignore index 0d485db..83a6adb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ internal/database/comfychan.db **/*_templ.go +tmp diff --git a/Makefile b/Makefile index 17c7c79..fa7952f 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ live/templ: - templ generate --watch --proxy="http://localhost:8080" --cmd="go run ." --open-browser=false -v + templ generate --watch --proxy="http://localhost:8080" --cmd="go run ./web" --open-browser=false -v live/sync_assets: go run github.com/air-verse/air@v1.61.7 \ diff --git a/internal/database/handlers.go b/internal/database/handlers.go index 0e6b5da..c35cfde 100644 --- a/internal/database/handlers.go +++ b/internal/database/handlers.go @@ -28,12 +28,15 @@ func GetBoard(db *sql.DB, slug string) (Board, error) { row := db.QueryRow(`SELECT id, name, slug, tag FROM boards WHERE slug = ?`, slug) var result Board - row.Scan(&result.Id, &result.Name, &result.Slug, &result.Tag) + err := row.Scan(&result.Id, &result.Name, &result.Slug, &result.Tag) + if err != nil { + return Board{}, err + } return result, row.Err() } -func GetBoardThreads(db *sql.DB, slug string) ([]Thread, error) { +func GetThreads(db *sql.DB, slug string) ([]Thread, error) { rows, err := db.Query( `SELECT id, board_slug, subject, created_at, bumped_at FROM threads WHERE board_slug = ?`, slug) if err != nil { @@ -54,8 +57,20 @@ func GetBoardThreads(db *sql.DB, slug string) ([]Thread, error) { return result, rows.Err() } -func GetThreadPosts(db *sql.DB, thread_id int) ([]Post, error) { - rows, err := db.Query(`SELECT id, thread_id, author, body, created_at FROM posts where thread_id = ?`, thread_id) +func GetThread(db *sql.DB, threadId int) (Thread, error) { + row := db.QueryRow(`SELECT id, board_slug, subject, created_at, bumped_at FROM threads WHERE id = ?`, threadId) + + var thread Thread + err := row.Scan(&thread.Id, &thread.BoardSlug, &thread.Subject, &thread.CreatedAt, &thread.BumpedAt) + if err != nil { + return Thread{}, err + } + + return thread, row.Err() +} + +func GetThreadPosts(db *sql.DB, threadId int) ([]Post, error) { + rows, err := db.Query(`SELECT id, thread_id, author, body, created_at FROM posts where thread_id = ?`, threadId) if err != nil { return nil, err } @@ -74,14 +89,14 @@ func GetThreadPosts(db *sql.DB, thread_id int) ([]Post, error) { return result, rows.Err() } -func PutBoardThread(db *sql.DB, board_slug string, subject string, body string) error { +func PutBoardThread(db *sql.DB, boardSlug string, subject string, body string) error { tx, err := db.Begin() if err != nil { return err } defer tx.Rollback() - res, err := tx.Exec(`INSERT INTO threads (board_slug, subject) VALUES (?, ?) RETURNING id`, board_slug, subject) + res, err := tx.Exec(`INSERT INTO threads (board_slug, subject) VALUES (?, ?) RETURNING id`, boardSlug, subject) if err != nil { return err } diff --git a/main.go b/web/main.go similarity index 53% rename from main.go rename to web/main.go index 2c7c8bc..c6bb851 100644 --- a/main.go +++ b/web/main.go @@ -3,13 +3,15 @@ package main import ( "database/sql" "fmt" + "log" + "net/http" + "strconv" + "github.com/dominicf2001/comfychan/internal/database" "github.com/dominicf2001/comfychan/web/views" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" _ "github.com/mattn/go-sqlite3" - "log" - "net/http" ) var dev = true @@ -25,6 +27,10 @@ func disableCacheInDevMode(next http.Handler) http.Handler { } func main() { + // ----------------- + // SETUP + // ----------------- + db, err := sql.Open("sqlite3", "internal/database/comfychan.db") if err != nil { log.Fatal(err) @@ -40,6 +46,12 @@ func main() { http.StripPrefix("/static", http.FileServer(http.Dir("web/static"))))) + // ----------------- + + // ----------------- + // MAIN ROUTES + // ----------------- + r.Get("/", func(w http.ResponseWriter, r *http.Request) { views.Index().Render(r.Context(), w) }) @@ -57,46 +69,38 @@ func main() { views.Board(board).Render(r.Context(), w) }) - // r.Get("/{slug}/{threadId}/posts", func(w http.ResponseWriter, r *http.Request) { - // slug := chi.URLParam(r, "slug") - // threadIdStr := chi.URLParam(r, "postId") - // threadId, err := strconv.Atoi(threadIdStr) - // if err != nil { - // http.Error(w, "Invalid thread id", http.StatusBadRequest) - // return - // } - // - // posts, err := database.GetThreadPosts(db, threadId) - // if err != nil { - // http.Error(w, "Failed to get thread posts", http.StatusInternalServerError) - // } - // }) - - r.Get("/hx/{slug}/catalog", func(w http.ResponseWriter, r *http.Request) { + r.Get("/{slug}/threads/{threadId}", func(w http.ResponseWriter, r *http.Request) { slug := chi.URLParam(r, "slug") - threads, err := database.GetBoardThreads(db, slug) + // TODO: use relative thread_nums (see issue #1) + threadIdStr := chi.URLParam(r, "threadId") + threadId, err := strconv.Atoi(threadIdStr) if err != nil { - http.Error(w, "Failed to get board threads", http.StatusInternalServerError) - log.Printf("GetBoardThreads: %v", err) + http.Error(w, "Invalid thread id", http.StatusBadRequest) return } - vms := make([]views.ThreadGridBoxViewModel, 0, len(threads)) - for _, thread := range threads { - posts, err := database.GetThreadPosts(db, thread.Id) - if err != nil { - http.Error(w, "Error getting thread posts", http.StatusBadRequest) - log.Printf("GetThreadPosts: %v", err) - return - } - - vms = append(vms, views.ThreadGridBoxViewModel{ - Thread: thread, - Posts: posts, - }) + board, err := database.GetBoard(db, slug) + if err != nil { + http.NotFound(w, r) + log.Printf("Board %q not found: %v", slug, err) + return } - views.PostsCatalog(vms).Render(r.Context(), w) + thread, err := database.GetThread(db, threadId) + if err != nil { + http.Error(w, "Failed to get thread", http.StatusInternalServerError) + log.Printf("Failed to get thread %q: %v", threadIdStr, err) + return + } + + posts, err := database.GetThreadPosts(db, threadId) + if err != nil { + http.Error(w, "Failed to get thread posts", http.StatusInternalServerError) + log.Printf("Failed to get thread %q posts: %v", threadIdStr, err) + return + } + + views.Thread(board, thread, posts).Render(r.Context(), w) }) r.Post("/{slug}/threads", func(w http.ResponseWriter, r *http.Request) { @@ -113,6 +117,48 @@ func main() { database.PutBoardThread(db, slug, subject, body) }) + // ----------------- + + // ----------------- + // PARTIALS + // ----------------- + + r.Get("/hx/{slug}/catalog", func(w http.ResponseWriter, r *http.Request) { + slug := chi.URLParam(r, "slug") + threads, err := database.GetThreads(db, slug) + if err != nil { + http.Error(w, "Failed to get board threads", http.StatusInternalServerError) + log.Printf("GetThreads: %v", err) + return + } + + previews := make([]views.CatalogThreadPreview, 0, len(threads)) + for _, thread := range threads { + posts, err := database.GetThreadPosts(db, thread.Id) + if err != nil { + http.Error(w, "Error getting thread posts", http.StatusBadRequest) + log.Printf("GetThreadPosts: %v", err) + return + } + + if len(posts) == 0 { + http.Error(w, "Malformed thread", http.StatusInternalServerError) + log.Printf("GetThreadPosts: %v", err) + return + } + + previews = append(previews, views.CatalogThreadPreview{ + Subject: thread.Subject, + Body: posts[0].Body, + ThreadURL: fmt.Sprintf("/%s/threads/%d", slug, thread.Id), + }) + } + + views.ThreadsCatalog(previews).Render(r.Context(), w) + }) + + // ----------------- + fmt.Println("Listening on :8080") http.ListenAndServe(":8080", r) } diff --git a/web/views/post.templ b/web/views/post.templ deleted file mode 100644 index 4c5909a..0000000 --- a/web/views/post.templ +++ /dev/null @@ -1,7 +0,0 @@ -package views - -import "github.com/dominicf2001/comfychan/internal/database" - -templ Post(board database.Board) { -BoardHeader(board) -} diff --git a/web/views/posts.templ b/web/views/posts.templ deleted file mode 100644 index 4f3b124..0000000 --- a/web/views/posts.templ +++ /dev/null @@ -1,19 +0,0 @@ -package views - -import "github.com/dominicf2001/comfychan/internal/database" - -type ThreadGridBoxViewModel struct { -Thread database.Thread -Posts []database.Post -} - -templ PostsCatalog(vms []ThreadGridBoxViewModel) { -
- for _, vm := range vms { -
-

{ vm.Thread.Subject }

-

{ vm.Posts[0].Body }

-
- } -
-} diff --git a/web/views/thread.templ b/web/views/thread.templ new file mode 100644 index 0000000..35ec88c --- /dev/null +++ b/web/views/thread.templ @@ -0,0 +1,12 @@ +package views + +import ( +"github.com/dominicf2001/comfychan/internal/database" +"github.com/dominicf2001/comfychan/web/views/shared" +) + +templ Thread(board database.Board, thread database.Thread, posts []database.Post) { +@shared.Layout() { +@BoardHeader(board) +} +} diff --git a/web/views/threads.templ b/web/views/threads.templ new file mode 100644 index 0000000..9465194 --- /dev/null +++ b/web/views/threads.templ @@ -0,0 +1,18 @@ +package views + +type CatalogThreadPreview struct { +Subject string +Body string +ThreadURL string +} + +templ ThreadsCatalog(previews []CatalogThreadPreview) { +
+ for _, preview := range previews { +
+

{ preview.Subject }

+

{ preview.Body }

+
+ } +
+}