diff --git a/handlers/board.go b/handlers/board.go
index 2fb8ce3..374cf5d 100644
--- a/handlers/board.go
+++ b/handlers/board.go
@@ -54,9 +54,8 @@ func BoardHandler(app *App) http.HandlerFunc {
action := r.URL.Query().Get("action")
if action == "create_thread" {
title := r.FormValue("title")
- threadType := r.FormValue("type")
- if title == "" || (threadType != "classic" && threadType != "chat" && threadType != "question") {
- http.Error(w, "Invalid input", http.StatusBadRequest)
+ if title == "" {
+ http.Error(w, "Thread title is required", http.StatusBadRequest)
return
}
if board.Private {
@@ -74,7 +73,6 @@ func BoardHandler(app *App) http.HandlerFunc {
thread := models.Thread{
BoardID: boardID,
Title: title,
- Type: threadType,
CreatedByUserID: userID,
}
err = models.CreateThread(app.DB, thread)
diff --git a/handlers/boards.go b/handlers/boards.go
index f6aaf34..54141c4 100644
--- a/handlers/boards.go
+++ b/handlers/boards.go
@@ -3,16 +3,52 @@ package handlers
import (
"log"
"net/http"
- "github.com/gorilla/sessions"
+ "strconv"
"threadr/models"
+ "github.com/gorilla/sessions"
)
func BoardsHandler(app *App) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
session := r.Context().Value("session").(*sessions.Session)
loggedIn := session.Values["user_id"] != nil
- userID, _ := session.Values["user_id"].(int)
cookie, _ := r.Cookie("threadr_cookie_banner")
+ userID, _ := session.Values["user_id"].(int)
+ isAdmin := false
+
+ if loggedIn {
+ user, err := models.GetUserByID(app.DB, userID)
+ if err != nil {
+ log.Printf("Error fetching user: %v", err)
+ } else if user != nil {
+ isAdmin = models.HasGlobalPermission(user, models.PermCreateBoard)
+ }
+ }
+
+ if r.Method == http.MethodPost && loggedIn && isAdmin {
+ name := r.FormValue("name")
+ description := r.FormValue("description")
+ if name == "" {
+ http.Error(w, "Board name is required", http.StatusBadRequest)
+ return
+ }
+ board := models.Board{
+ Name: name,
+ Description: description,
+ Private: false,
+ PublicVisible: true,
+ }
+ query := "INSERT INTO boards (name, description, private, public_visible) VALUES (?, ?, ?, ?)"
+ result, err := app.DB.Exec(query, board.Name, board.Description, board.Private, board.PublicVisible)
+ if err != nil {
+ log.Printf("Error creating board: %v", err)
+ http.Error(w, "Failed to create board", http.StatusInternalServerError)
+ return
+ }
+ boardID, _ := result.LastInsertId()
+ http.Redirect(w, r, app.Config.ThreadrDir+"/board/?id="+strconv.FormatInt(boardID, 10), http.StatusFound)
+ return
+ }
publicBoards, err := models.GetAllBoards(app.DB, false)
if err != nil {
@@ -46,6 +82,7 @@ func BoardsHandler(app *App) http.HandlerFunc {
PageData
PublicBoards []models.Board
PrivateBoards []models.Board
+ IsAdmin bool
}{
PageData: PageData{
Title: "ThreadR - Boards",
@@ -58,6 +95,7 @@ func BoardsHandler(app *App) http.HandlerFunc {
},
PublicBoards: publicBoards,
PrivateBoards: privateBoards,
+ IsAdmin: isAdmin,
}
if err := app.Tmpl.ExecuteTemplate(w, "boards", data); err != nil {
log.Printf("Error executing template in BoardsHandler: %v", err)
diff --git a/handlers/thread.go b/handlers/thread.go
index 8cfa1fa..df5c12c 100644
--- a/handlers/thread.go
+++ b/handlers/thread.go
@@ -61,7 +61,7 @@ func ThreadHandler(app *App) http.HandlerFunc {
if action == "submit" {
content := r.FormValue("content")
replyToStr := r.URL.Query().Get("to")
- replyTo := 0
+ replyTo := -1
if replyToStr != "" {
replyTo, err = strconv.Atoi(replyToStr)
if err != nil {
@@ -109,12 +109,6 @@ func ThreadHandler(app *App) http.HandlerFunc {
return
}
- if thread.Type == "chat" {
- for i, j := 0, len(posts)-1; i < j; i, j = i+1, j-1 {
- posts[i], posts[j] = posts[j], posts[i]
- }
- }
-
data := struct {
PageData
Thread models.Thread
diff --git a/main.go b/main.go
index 0061744..9038b4c 100644
--- a/main.go
+++ b/main.go
@@ -64,13 +64,12 @@ func createTablesIfNotExist(db *sql.DB) error {
return fmt.Errorf("error creating users table: %v", err)
}
- // Create threads table
+ // Create threads table (without type field)
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS threads (
id INT AUTO_INCREMENT PRIMARY KEY,
board_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
- type VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by_user_id INT NOT NULL,
diff --git a/models/board.go b/models/board.go
index ea2892b..fedbab5 100644
--- a/models/board.go
+++ b/models/board.go
@@ -20,20 +20,38 @@ func GetBoardByID(db *sql.DB, id int) (*Board, error) {
query := "SELECT id, name, description, private, public_visible, pinned_threads, custom_landing_page, color_scheme FROM boards WHERE id = ?"
row := db.QueryRow(query, id)
board := &Board{}
- var pinnedThreadsJSON string
- err := row.Scan(&board.ID, &board.Name, &board.Description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &board.CustomLandingPage, &board.ColorScheme)
+ var pinnedThreadsJSON sql.NullString
+ var customLandingPage sql.NullString
+ var colorScheme sql.NullString
+ var description sql.NullString
+ err := row.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
- if pinnedThreadsJSON != "" {
- err = json.Unmarshal([]byte(pinnedThreadsJSON), &board.PinnedThreads)
+ if description.Valid {
+ board.Description = description.String
+ } else {
+ board.Description = ""
+ }
+ if pinnedThreadsJSON.Valid && pinnedThreadsJSON.String != "" {
+ err = json.Unmarshal([]byte(pinnedThreadsJSON.String), &board.PinnedThreads)
if err != nil {
return nil, err
}
}
+ if customLandingPage.Valid {
+ board.CustomLandingPage = customLandingPage.String
+ } else {
+ board.CustomLandingPage = ""
+ }
+ if colorScheme.Valid {
+ board.ColorScheme = colorScheme.String
+ } else {
+ board.ColorScheme = ""
+ }
return board, nil
}
@@ -48,17 +66,35 @@ func GetAllBoards(db *sql.DB, private bool) ([]Board, error) {
var boards []Board
for rows.Next() {
board := Board{}
- var pinnedThreadsJSON string
- err := rows.Scan(&board.ID, &board.Name, &board.Description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &board.CustomLandingPage, &board.ColorScheme)
+ var pinnedThreadsJSON sql.NullString
+ var customLandingPage sql.NullString
+ var colorScheme sql.NullString
+ var description sql.NullString
+ err := rows.Scan(&board.ID, &board.Name, &description, &board.Private, &board.PublicVisible, &pinnedThreadsJSON, &customLandingPage, &colorScheme)
if err != nil {
return nil, err
}
- if pinnedThreadsJSON != "" {
- err = json.Unmarshal([]byte(pinnedThreadsJSON), &board.PinnedThreads)
+ if description.Valid {
+ board.Description = description.String
+ } else {
+ board.Description = ""
+ }
+ if pinnedThreadsJSON.Valid && pinnedThreadsJSON.String != "" {
+ err = json.Unmarshal([]byte(pinnedThreadsJSON.String), &board.PinnedThreads)
if err != nil {
return nil, err
}
}
+ if customLandingPage.Valid {
+ board.CustomLandingPage = customLandingPage.String
+ } else {
+ board.CustomLandingPage = ""
+ }
+ if colorScheme.Valid {
+ board.ColorScheme = colorScheme.String
+ } else {
+ board.ColorScheme = ""
+ }
boards = append(boards, board)
}
return boards, nil
diff --git a/models/post.go b/models/post.go
index fea607f..d806710 100644
--- a/models/post.go
+++ b/models/post.go
@@ -29,10 +29,26 @@ func GetPostsByThreadID(db *sql.DB, threadID int) ([]Post, error) {
var posts []Post
for rows.Next() {
post := Post{}
- err := rows.Scan(&post.ID, &post.ThreadID, &post.UserID, &post.PostTime, &post.EditTime, &post.Content, &post.AttachmentHash, &post.AttachmentName, &post.Title, &post.ReplyTo)
+ var postTimeStr string
+ var editTimeStr sql.NullString
+ err := rows.Scan(&post.ID, &post.ThreadID, &post.UserID, &postTimeStr, &editTimeStr, &post.Content, &post.AttachmentHash, &post.AttachmentName, &post.Title, &post.ReplyTo)
if err != nil {
return nil, err
}
+ post.PostTime, err = time.Parse("2006-01-02 15:04:05", postTimeStr)
+ if err != nil {
+ post.PostTime = time.Time{}
+ }
+ if editTimeStr.Valid {
+ editTime, err := time.Parse("2006-01-02 15:04:05", editTimeStr.String)
+ if err != nil {
+ post.EditTime = nil
+ } else {
+ post.EditTime = &editTime
+ }
+ } else {
+ post.EditTime = nil
+ }
posts = append(posts, post)
}
return posts, nil
diff --git a/models/thread.go b/models/thread.go
index a357c41..891b329 100644
--- a/models/thread.go
+++ b/models/thread.go
@@ -9,7 +9,6 @@ type Thread struct {
ID int
BoardID int
Title string
- Type string // "classic", "chat", "question"
CreatedAt time.Time
UpdatedAt time.Time
CreatedByUserID int
@@ -17,21 +16,31 @@ type Thread struct {
}
func GetThreadByID(db *sql.DB, id int) (*Thread, error) {
- query := "SELECT id, board_id, title, type, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE id = ?"
+ query := "SELECT id, board_id, title, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE id = ?"
row := db.QueryRow(query, id)
thread := &Thread{}
- err := row.Scan(&thread.ID, &thread.BoardID, &thread.Title, &thread.Type, &thread.CreatedAt, &thread.UpdatedAt, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID)
+ var createdAtStr string
+ var updatedAtStr string
+ err := row.Scan(&thread.ID, &thread.BoardID, &thread.Title, &createdAtStr, &updatedAtStr, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
+ thread.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtStr)
+ if err != nil {
+ thread.CreatedAt = time.Time{}
+ }
+ thread.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtStr)
+ if err != nil {
+ thread.UpdatedAt = time.Time{}
+ }
return thread, nil
}
func GetThreadsByBoardID(db *sql.DB, boardID int) ([]Thread, error) {
- query := "SELECT id, board_id, title, type, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE board_id = ? ORDER BY updated_at DESC"
+ query := "SELECT id, board_id, title, created_at, updated_at, created_by_user_id, accepted_answer_post_id FROM threads WHERE board_id = ? ORDER BY updated_at DESC"
rows, err := db.Query(query, boardID)
if err != nil {
return nil, err
@@ -41,17 +50,27 @@ func GetThreadsByBoardID(db *sql.DB, boardID int) ([]Thread, error) {
var threads []Thread
for rows.Next() {
thread := Thread{}
- err := rows.Scan(&thread.ID, &thread.BoardID, &thread.Title, &thread.Type, &thread.CreatedAt, &thread.UpdatedAt, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID)
+ var createdAtStr string
+ var updatedAtStr string
+ err := rows.Scan(&thread.ID, &thread.BoardID, &thread.Title, &createdAtStr, &updatedAtStr, &thread.CreatedByUserID, &thread.AcceptedAnswerPostID)
if err != nil {
return nil, err
}
+ thread.CreatedAt, err = time.Parse("2006-01-02 15:04:05", createdAtStr)
+ if err != nil {
+ thread.CreatedAt = time.Time{}
+ }
+ thread.UpdatedAt, err = time.Parse("2006-01-02 15:04:05", updatedAtStr)
+ if err != nil {
+ thread.UpdatedAt = time.Time{}
+ }
threads = append(threads, thread)
}
return threads, nil
}
func CreateThread(db *sql.DB, thread Thread) error {
- query := "INSERT INTO threads (board_id, title, type, created_by_user_id, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())"
- _, err := db.Exec(query, thread.BoardID, thread.Title, thread.Type, thread.CreatedByUserID)
+ query := "INSERT INTO threads (board_id, title, created_by_user_id, created_at, updated_at, type) VALUES (?, ?, ?, NOW(), NOW(), 'classic')"
+ _, err := db.Exec(query, thread.BoardID, thread.Title, thread.CreatedByUserID)
return err
}
\ No newline at end of file
diff --git a/static/style.css b/static/style.css
index d5d8e4d..e2a9f56 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,5 +1,5 @@
body {
- font-family: Arial, sans-serif;
+ font-family: monospace;
margin: 0;
padding: 0;
background-color: #fef6e4; /* beige */
@@ -10,7 +10,7 @@ main {
display: flex;
flex-direction: column;
align-items: center;
- padding: 20px;
+ padding: 25px;
}
main > header {
@@ -20,20 +20,33 @@ main > header {
main > section {
margin: 1em;
- padding: 1em;
+ padding: 14px 20px;
border: 1px solid #001858;
border-radius: 5px;
background-color: #f3d2c1; /* orange */
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
+ width: 80%;
+ max-width: 800px;
+}
+
+main > div {
+ width: 80%;
+ max-width: 800px;
}
main > div > article {
border: 1px solid #001858;
- padding: 1em;
+ padding: 14px 20px;
margin-bottom: 1em;
background-color: #fef6e4;
border-radius: 5px;
box-shadow: inset 0px 8px 16px 0px rgba(0,0,0,0.2);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+main > div > article:hover {
+ transform: translateY(-2px);
+ box-shadow: 0px 4px 12px 0px rgba(0,0,0,0.15);
}
article > header {
@@ -41,7 +54,9 @@ article > header {
background-color: #001858;
color: #fef6e4;
padding: 0.5em;
- margin: -1em -1em 1em -1em;
+ margin: -14px -20px 1em -20px;
+ border-radius: 5px 5px 0 0;
+ box-shadow: 0px 4px 8px 0px rgba(0,0,0,0.2);
}
ul.topnav {
@@ -52,6 +67,7 @@ ul.topnav {
background-color: #001858;
position: fixed;
top: 0;
+ left: 0;
width: 100%;
box-shadow: 0 0.7em 1.2em 0 rgba(0,0,0,0.2);
}
@@ -64,11 +80,13 @@ ul.topnav li a {
display: block;
color: #fef6e4;
text-align: center;
- padding: 14px 16px;
+ padding: 1.2em 1.3em;
text-decoration: none;
+ font-family: monospace;
+ font-size: 1em;
}
-ul.topnav li a:hover {
+ul.topnav li a:hover:not(.active) {
background-color: #8bd3dd; /* cyan */
}
@@ -83,6 +101,7 @@ div.topnav {
div.banner {
position: fixed;
bottom: 0;
+ left: 0;
width: 100%;
background-color: #001858;
padding: 10px;
@@ -104,17 +123,21 @@ form {
}
input, textarea, select {
- margin: 0.5em 0;
- padding: 0.5em;
+ margin: 8px 0;
+ padding: 14px 20px;
border: 1px solid #001858;
border-radius: 4px;
background-color: #fef6e4;
color: #001858;
+ font-family: monospace;
+ box-sizing: border-box;
+ box-shadow: inset 0px 8px 16px 0px rgba(0,0,0,0.2);
}
input[type="submit"] {
background-color: #001858;
color: #fef6e4;
+ border: none;
cursor: pointer;
}
@@ -123,13 +146,15 @@ input[type="submit"]:hover {
}
button {
- margin: 0.5em 0;
- padding: 0.5em;
+ margin: 8px 0;
+ padding: 14px 20px;
border: none;
border-radius: 4px;
background-color: #001858;
color: #fef6e4;
cursor: pointer;
+ font-family: monospace;
+ width: 100%;
}
button:hover {
@@ -138,6 +163,125 @@ button:hover {
img {
max-width: 100%;
+ object-fit: contain;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: monospace;
+ color: #001858;
+}
+
+p, a, li {
+ font-family: monospace;
+ color: #001858;
+}
+
+/* Enhanced styles for boards */
+ul.board-list {
+ list-style-type: none;
+ padding: 0;
+ margin: 0;
+}
+
+li.board-item {
+ margin-bottom: 1em;
+ padding: 1em;
+ background-color: #fef6e4;
+ border: 1px solid #001858;
+ border-radius: 8px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+li.board-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0px 6px 14px 0px rgba(0,0,0,0.15);
+}
+
+li.board-item a {
+ color: #001858;
+ font-weight: bold;
+ text-decoration: none;
+ font-size: 1.2em;
+}
+
+li.board-item a:hover {
+ color: #f582ae;
+ text-decoration: underline;
+}
+
+p.board-desc {
+ margin: 0.5em 0 0 0;
+ color: #001858;
+ font-size: 0.9em;
+}
+
+/* Enhanced styles for thread posts */
+.thread-posts {
+ width: 80%;
+ max-width: 800px;
+}
+
+.post-item {
+ background-color: #fef6e4;
+ border: 1px solid #001858;
+ border-radius: 8px;
+ margin-bottom: 1.5em;
+ padding: 1em;
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.post-item:hover {
+ transform: translateY(-3px);
+ box-shadow: 0px 6px 14px 0px rgba(0,0,0,0.15);
+}
+
+.post-item header {
+ background-color: #001858;
+ color: #fef6e4;
+ padding: 0.5em;
+ margin: -1em -1em 1em -1em;
+ border-radius: 6px 6px 0 0;
+ border-bottom: 1px solid #001858;
+}
+
+.post-item header h3 {
+ margin: 0;
+ font-size: 1.1em;
+}
+
+.post-item header p {
+ margin: 0.3em 0 0 0;
+ font-size: 0.85em;
+ opacity: 0.9;
+}
+
+.post-content {
+ margin: 0;
+ padding: 0.5em;
+ line-height: 1.5;
+ font-size: 0.95em;
+}
+
+.post-actions {
+ margin-top: 0.8em;
+ display: flex;
+ gap: 0.5em;
+ align-items: center;
+}
+
+.post-actions a {
+ color: #001858;
+ text-decoration: none;
+ font-size: 0.9em;
+ padding: 0.3em 0.6em;
+ border: 1px solid #001858;
+ border-radius: 4px;
+ transition: background-color 0.2s ease;
+}
+
+.post-actions a:hover {
+ background-color: #8bd3dd;
+ color: #fef6e4;
}
@media (prefers-color-scheme: dark) {
@@ -169,6 +313,40 @@ img {
input[type="submit"]:hover, button:hover {
background-color: #8bd3dd;
}
+ li.board-item {
+ background-color: #444;
+ border-color: #fef6e4;
+ }
+ li.board-item a {
+ color: #fef6e4;
+ }
+ li.board-item a:hover {
+ color: #f582ae;
+ }
+ p.board-desc {
+ color: #fef6e4;
+ }
+ .post-item {
+ background-color: #444;
+ border-color: #fef6e4;
+ }
+ .post-content {
+ color: #fef6e4;
+ }
+ .post-actions a {
+ color: #fef6e4;
+ border-color: #fef6e4;
+ }
+ .post-actions a:hover {
+ background-color: #8bd3dd;
+ color: #001858;
+ }
+ h1, h2, h3, h4, h5, h6 {
+ color: #fef6e4;
+ }
+ p, a, li {
+ color: #fef6e4;
+ }
}
@media (max-width: 600px) {
@@ -182,5 +360,12 @@ img {
main > section {
margin: 0.5em;
padding: 0.5em;
+ width: 95%;
+ }
+ main > div {
+ width: 95%;
+ }
+ .thread-posts {
+ width: 95%;
}
}
\ No newline at end of file
diff --git a/templates/pages/board.html b/templates/pages/board.html
index 3d25e5d..c285327 100644
--- a/templates/pages/board.html
+++ b/templates/pages/board.html
@@ -13,11 +13,16 @@
{{.Board.Description}}
+ Threads
+ {{if .Threads}}
{{range .Threads}}
- - {{.Title}} ({{.Type}})
+ - {{.Title}} - Updated on {{.UpdatedAt.Format "02/01/2006 - 15:04"}}
{{end}}
+ {{else}}
+ No threads available in this board yet.
+ {{end}}
{{if .LoggedIn}}
diff --git a/templates/pages/boards.html b/templates/pages/boards.html
index 9960c04..d4350e6 100644
--- a/templates/pages/boards.html
+++ b/templates/pages/boards.html
@@ -13,20 +13,46 @@
{{if .LoggedIn}}
+ {{end}}
+ {{if .IsAdmin}}
+
+ Create New Public Board
+
{{end}}
diff --git a/templates/pages/thread.html b/templates/pages/thread.html
index 4ae367b..e94d328 100644
--- a/templates/pages/thread.html
+++ b/templates/pages/thread.html
@@ -10,35 +10,32 @@
-
+
{{range .Posts}}
-
+
- {{.Title}}
- Posted by User {{.UserID}} on {{.PostTime}}
+ {{if .Title}}{{.Title}}{{else}}Post #{{.ID}}{{end}}
+ Posted on {{.PostTime.Format "02/01/2006 - 15:04"}}
{{if gt .ReplyTo 0}}
- Reply to post {{.ReplyTo}}
+ Reply to post {{.ReplyTo}}
{{end}}
- {{.Content}}
+ {{.Content}}
{{if $.LoggedIn}}
-
-
- Reply
+
+
+
+
Reply
+
{{end}}
{{end}}