282 lines
6.9 KiB
Go

package routes
import (
"dk/internal/components"
"dk/internal/database"
"dk/internal/helpers/markdown"
"dk/internal/models/forum"
"dk/internal/models/users"
"fmt"
"strings"
sushi "git.sharkk.net/Sharkk/Sushi"
"git.sharkk.net/Sharkk/Sushi/auth"
)
// ThreadInfo combines forum thread with author and last reply info
type ThreadInfo struct {
Thread *forum.Forum
Author *users.User
AuthorClass string
LastReplyBy *users.User
}
// PostInfo combines a forum post/reply with its author
type PostInfo struct {
Post *forum.Forum
Author *users.User
AuthorClass string
Content string // Pre-processed markdown content
}
func RegisterForumRoutes(app *sushi.App) {
authed := app.Group("/forum")
authed.Use(auth.RequireAuth())
authed.Get("/", index)
authed.Get("/new", showNew)
authed.Post("/new", new)
authed.Get("/:id", showThread)
authed.Get("/:id/reply", showReply)
authed.Post("/:id/reply", reply)
}
func index(ctx sushi.Ctx) {
page := int(ctx.QueryArgs().GetUintOrZero("page"))
if page < 1 {
page = 1
}
perPage := 30
offset := (page - 1) * perPage
// Get threads with pagination
var threads []*forum.Forum
err := database.Select(&threads, "SELECT * FROM forum WHERE parent = 0 ORDER BY last_post DESC, id DESC LIMIT %d OFFSET %d", perPage, offset)
if err != nil {
threads = make([]*forum.Forum, 0)
}
// Get total count for pagination
var totalCount int
database.Get(&totalCount, "SELECT COUNT(*) FROM forum WHERE parent = 0")
// Build thread info with authors and last reply info
var threadInfos []ThreadInfo
for _, thread := range threads {
author, _ := users.Find(thread.Author)
if author == nil {
author = &users.User{Username: "[Deleted]", ClassID: 1}
}
authorClass := author.Class().Name
// Get last reply author if there are replies
var lastReplyBy *users.User
if thread.Replies > 0 {
var lastReply forum.Forum
err := database.Get(&lastReply, "SELECT * FROM forum WHERE parent = %d ORDER BY posted DESC LIMIT 1", thread.ID)
if err == nil {
lastReplyBy, _ = users.Find(lastReply.Author)
}
}
threadInfos = append(threadInfos, ThreadInfo{
Thread: thread,
Author: author,
AuthorClass: authorClass,
LastReplyBy: lastReplyBy,
})
}
totalPages := (totalCount + perPage - 1) / perPage
components.RenderPage(ctx, "Forum", "forum/index.html", map[string]any{
"threadInfos": threadInfos,
"currentPage": page,
"totalPages": totalPages,
"hasNext": page < totalPages,
"hasPrev": page > 1,
})
}
func showNew(ctx sushi.Ctx) {
components.RenderPage(ctx, "New Forum Thread", "forum/new.html", map[string]any{})
}
func new(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
title := strings.TrimSpace(ctx.Form("title").String())
content := strings.TrimSpace(ctx.Form("content").String())
if title == "" {
sess.SetFlash("error", "Thread title cannot be empty")
ctx.Redirect("/forum/new")
return
}
if content == "" {
sess.SetFlash("error", "Thread content cannot be empty")
ctx.Redirect("/forum/new")
return
}
user := ctx.GetCurrentUser().(*users.User)
thread := forum.New()
thread.Author = user.ID
thread.Title = title
thread.Content = content
database.Transaction(func() error {
return thread.Insert()
})
ctx.Redirect(fmt.Sprintf("/forum/%d", thread.ID))
}
func showThread(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
id := ctx.Param("id").Int()
page := int(ctx.QueryArgs().GetUintOrZero("page"))
if page < 1 {
page = 1
}
thread, err := forum.Find(id)
if err != nil {
sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id))
ctx.Redirect("/forum")
return
}
if thread.Parent != 0 {
sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id))
ctx.Redirect("/forum")
return
}
// Get thread author
threadAuthor, _ := users.Find(thread.Author)
if threadAuthor == nil {
threadAuthor = &users.User{Username: "[Deleted]", Level: 1, ClassID: 1}
}
threadAuthorClass := threadAuthor.Class().Name
threadContent := markdown.MarkdownToHTML(thread.Content)
// Get replies with pagination
perPage := 30
offset := (page - 1) * perPage
var replies []*forum.Forum
err = database.Select(&replies, "SELECT * FROM forum WHERE parent = %d ORDER BY posted ASC LIMIT %d OFFSET %d", id, perPage, offset)
if err != nil {
replies = make([]*forum.Forum, 0)
}
// Build reply info with authors and processed content
var replyInfos []PostInfo
for _, reply := range replies {
author, _ := users.Find(reply.Author)
if author == nil {
author = &users.User{Username: "[Deleted]", Level: 1, ClassID: 1}
}
authorClass := author.Class().Name
content := markdown.MarkdownToHTML(reply.Content)
replyInfos = append(replyInfos, PostInfo{
Post: reply,
Author: author,
AuthorClass: authorClass,
Content: content,
})
}
totalPages := (thread.Replies + perPage - 1) / perPage
if totalPages < 1 {
totalPages = 1
}
components.RenderPage(ctx, thread.Title, "forum/thread.html", map[string]any{
"thread": thread,
"threadContent": threadContent,
"threadAuthor": threadAuthor,
"threadAuthorClass": threadAuthorClass,
"replyInfos": replyInfos,
"currentPage": page,
"totalPages": totalPages,
"hasNext": page < totalPages,
"hasPrev": page > 1,
})
}
func showReply(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
id := ctx.Param("id").Int()
thread, err := forum.Find(id)
if err != nil {
sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id))
ctx.Redirect("/forum")
return
}
if thread.Parent != 0 {
sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id))
ctx.Redirect("/forum")
return
}
components.RenderPage(ctx, "Reply to "+thread.Title, "forum/reply.html", map[string]any{
"thread": thread,
})
}
func reply(ctx sushi.Ctx) {
sess := ctx.GetCurrentSession()
id := ctx.Param("id").Int()
thread, err := forum.Find(id)
if err != nil {
sess.SetFlash("error", fmt.Sprintf("Forum thread %d not found", id))
ctx.Redirect("/forum")
return
}
if thread.Parent != 0 {
sess.SetFlash("error", fmt.Sprintf("Forum post %d is not a thread", id))
ctx.Redirect("/forum")
return
}
content := strings.TrimSpace(ctx.Form("content").String())
if content == "" {
sess.SetFlash("error", "Reply content cannot be empty")
ctx.Redirect(fmt.Sprintf("/forum/%d/reply", id))
return
}
user := ctx.GetCurrentUser().(*users.User)
reply := forum.New()
reply.Author = user.ID
reply.Parent = thread.ID
reply.Title = "Re: " + thread.Title
reply.Content = content
database.Transaction(func() error {
if err := reply.Insert(); err != nil {
return err
}
// Update thread reply count and last post time
thread.IncrementReplies()
thread.UpdateLastPost()
return database.Exec("UPDATE forum SET replies = %d, last_post = %d WHERE id = %d",
thread.Replies, thread.LastPost, thread.ID)
})
ctx.Redirect(fmt.Sprintf("/forum/%d", thread.ID))
}