From b630296b8c4ff5fe4fbe740ba6b228d3795455c0 Mon Sep 17 00:00:00 2001 From: H3ALY Date: Wed, 2 Apr 2025 11:56:11 +0100 Subject: [PATCH] Add message system (inbox, read view, dropdown) and truncate helper Implemented message retrieval and read logic in storage layer Added handlers for inbox and individual message view Integrated messages into topbar dropdown with unread badge Added truncate helper to template functions Created new templates: messages/index.html and messages/read.html Fixed missing template function error in topbar rendering --- handlers/messages.go | 54 +++++++++++++++++++++++ helpers/template.go | 6 +++ main.go | 2 + models/user.go | 13 +++--- storage/messages.go | 63 +++++++++++++++++++++++---- storage/schema.go | 5 ++- templates/account/messages/index.html | 22 ++++++++++ templates/account/messages/read.html | 15 +++++++ templates/topbar.html | 44 ++++++++++--------- 9 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 handlers/messages.go create mode 100644 templates/account/messages/index.html create mode 100644 templates/account/messages/read.html diff --git a/handlers/messages.go b/handlers/messages.go new file mode 100644 index 0000000..378fd49 --- /dev/null +++ b/handlers/messages.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "database/sql" + "log" + "net/http" + + "synlotto-website/helpers" + "synlotto-website/storage" +) + +func MessagesInboxHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + + tmpl := helpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html") + + err := tmpl.ExecuteTemplate(w, "layout", context) + if err != nil { + helpers.RenderError(w, r, 500) + } + } +} + +func ReadMessageHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + idStr := r.URL.Query().Get("id") + messageID := helpers.Atoi(idStr) + + session, _ := helpers.GetSession(w, r) + userID, ok := session.Values["user_id"].(int) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + message, err := storage.GetMessageByID(db, userID, messageID) + if err != nil { + log.Printf("❌ Message not found: %v", err) + message = nil + } else if !message.IsRead { + _ = storage.MarkMessageAsRead(db, messageID, userID) + } + + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Message"] = message + + tmpl := helpers.LoadTemplateFiles("read-message.html", "templates/account/messages/read.html") + + tmpl.ExecuteTemplate(w, "layout", context) + } +} diff --git a/helpers/template.go b/helpers/template.go index ec8025c..ffff078 100644 --- a/helpers/template.go +++ b/helpers/template.go @@ -60,6 +60,12 @@ func TemplateFuncs() template.FuncMap { "inSlice": InSlice, "lower": lower, "rangeClass": rangeClass, + "truncate": func(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max] + "..." + }, } } diff --git a/main.go b/main.go index 479ac6b..eaf3915 100644 --- a/main.go +++ b/main.go @@ -68,6 +68,8 @@ func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/signup", middleware.Auth(false)(handlers.Signup)) mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db)) mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db)) + mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db))) + mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db))) mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db))) mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db))) } diff --git a/models/user.go b/models/user.go index daff8c9..2ffaf9d 100644 --- a/models/user.go +++ b/models/user.go @@ -23,12 +23,13 @@ type Notification struct { } type Message struct { - ID int - Sender string - Subject string - Message string - IsRead bool - CreatedAt time.Time + ID int + SenderId int + RecipientId int + Subject string + Message string + IsRead bool + CreatedAt time.Time } var db *sql.DB diff --git a/storage/messages.go b/storage/messages.go index 1854953..5168065 100644 --- a/storage/messages.go +++ b/storage/messages.go @@ -2,24 +2,25 @@ package storage import ( "database/sql" + "fmt" "synlotto-website/models" ) func GetMessageCount(db *sql.DB, userID int) (int, error) { var count int err := db.QueryRow(` - SELECT COUNT(*) FROM users_messages - WHERE user_id = ? AND is_read = FALSE + SELECT COUNT(*) FROM user_messages + WHERE recipient_id = ? AND is_read = FALSE `, userID).Scan(&count) return count, err } func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message { rows, err := db.Query(` - SELECT id, title, message, is_read - FROM users_messages - WHERE user_id = ? - ORDER BY created_at DESC + SELECT id, sender_id, recipient_id, subject, body, is_read, created_at + FROM user_messages + WHERE recipient_id = ? + ORDER BY created_at DESC LIMIT ? `, userID, limit) if err != nil { @@ -30,9 +31,53 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message { var messages []models.Message for rows.Next() { var m models.Message - rows.Scan(&m.ID, &m.Subject, &m.Message, &m.IsRead) - messages = append(messages, m) + err := rows.Scan( + &m.ID, + &m.SenderId, + &m.RecipientId, + &m.Subject, + &m.Message, + &m.IsRead, + &m.CreatedAt, + ) + if err == nil { + messages = append(messages, m) + } } - return messages } + +func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error) { + row := db.QueryRow(` + SELECT id, sender_id, recipient_id, subject, body, is_read, created_at + FROM user_messages + WHERE id = ? AND recipient_id = ? + `, messageID, userID) + + var m models.Message + err := row.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Message, &m.IsRead, &m.CreatedAt) + if err != nil { + return nil, err + } + return &m, nil +} + +func MarkMessageAsRead(db *sql.DB, messageID, userID int) error { + result, err := db.Exec(` + UPDATE user_messages + SET is_read = TRUE + WHERE id = ? AND recipient_id = ? + `, messageID, userID) + if err != nil { + return err + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return err + } + if rowsAffected == 0 { + return fmt.Errorf("no matching message found for user_id=%d and message_id=%d", userID, messageID) + } + return nil +} diff --git a/storage/schema.go b/storage/schema.go index efdb29b..993f137 100644 --- a/storage/schema.go +++ b/storage/schema.go @@ -114,8 +114,9 @@ CREATE TABLE IF NOT EXISTS my_tickets ( const SchemaUsersMessages = ` CREATE TABLE IF NOT EXISTS users_messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL REFERENCES users(id), - title TEXT NOT NULL, + senderId INTEGER NOT NULL REFERENCES users(id), + recipientId int, + subject TEXT NOT NULL, message TEXT, is_read BOOLEAN DEFAULT FALSE, type VARCHAR(50), diff --git a/templates/account/messages/index.html b/templates/account/messages/index.html new file mode 100644 index 0000000..ff21f25 --- /dev/null +++ b/templates/account/messages/index.html @@ -0,0 +1,22 @@ +{{ define "content" }} +
+

Your Messages

+ +
+{{ end }} diff --git a/templates/account/messages/read.html b/templates/account/messages/read.html new file mode 100644 index 0000000..8d92980 --- /dev/null +++ b/templates/account/messages/read.html @@ -0,0 +1,15 @@ +{{ define "content" }} +
+ {{ if .Message }} +

{{ .Message.Subject }}

+

Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}

+
+

{{ .Message.Body }}

+ Back to Inbox + {{ else }} +
+ Message not found or access denied. +
+ {{ end }} +
+{{ end }} diff --git a/templates/topbar.html b/templates/topbar.html index 3ed641e..5921a9c 100644 --- a/templates/topbar.html +++ b/templates/topbar.html @@ -82,33 +82,35 @@