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
This commit is contained in:
54
handlers/messages.go
Normal file
54
handlers/messages.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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] + "..."
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
main.go
2
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)))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,23 +2,24 @@ 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 = ?
|
||||
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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
22
templates/account/messages/index.html
Normal file
22
templates/account/messages/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Your Messages</h2>
|
||||
<ul class="list-group mt-3">
|
||||
{{ range .Messages }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="{{ if not .IsRead }}fw-bold{{ end }}">
|
||||
{{ .Subject }}
|
||||
</a>
|
||||
<div class="small text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</div>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<span class="badge bg-primary">Unread</span>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ else }}
|
||||
<li class="list-group-item text-muted text-center">No messages</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
15
templates/account/messages/read.html
Normal file
15
templates/account/messages/read.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
{{ if .Message }}
|
||||
<h2>{{ .Message.Subject }}</h2>
|
||||
<p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p>
|
||||
<hr>
|
||||
<p>{{ .Message.Body }}</p>
|
||||
<a href="/account/messages" class="btn btn-secondary mt-4">Back to Inbox</a>
|
||||
{{ else }}
|
||||
<div class="alert alert-danger text-center">
|
||||
Message not found or access denied.
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -82,33 +82,35 @@
|
||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<i class="bi bi-envelope fs-5 position-relative">
|
||||
<!-- Unread badge (example: 2 messages) -->
|
||||
<span
|
||||
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
|
||||
{{ if gt .MessageCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
||||
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</i>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
||||
aria-labelledby="messageDropdown">
|
||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
|
||||
<!-- Example message item -->
|
||||
<li class="px-3 py-2">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">Admin</div>
|
||||
<small class="text-muted">Welcome to SynLotto!</small>
|
||||
{{ if .Messages }}
|
||||
{{ range $i, $m := .Messages }}
|
||||
<li class="px-3 py-2">
|
||||
<div class="d-flex align-items-start">
|
||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||
<div>
|
||||
<div class="fw-semibold">{{ $m.Subject }}</div>
|
||||
<small class="text-muted">{{ truncate $m.Body 40 }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<hr class="dropdown-divider">
|
||||
</li>
|
||||
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{{ end }}
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No messages</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user