Compare commits
2 Commits
2498b33a9c
...
b630296b8c
| Author | SHA1 | Date | |
|---|---|---|---|
| b630296b8c | |||
| ab1d9abc72 |
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)
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ func NotificationsHandler(db *sql.DB) http.HandlerFunc {
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("notifications.html", "templates/account/notifications.html")
|
||||
tmpl := helpers.LoadTemplateFiles("index.html", "templates/account/notifications/index.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
package helpers
|
||||
|
||||
// ToDo should be a handler?
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"os"
|
||||
"synlotto-website/models"
|
||||
)
|
||||
|
||||
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
||||
log.Printf("⚙️ RenderError called with status: %d", statusCode)
|
||||
|
||||
context := TemplateContext(w, r, models.TemplateData{})
|
||||
|
||||
page := fmt.Sprintf("templates/error/%d.html", statusCode)
|
||||
tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), page)
|
||||
pagePath := fmt.Sprintf("templates/error/%d.html", statusCode)
|
||||
log.Printf("📄 Checking for template file: %s", pagePath)
|
||||
|
||||
if _, err := os.Stat(pagePath); err != nil {
|
||||
log.Printf("🚫 Template file missing: %s", err)
|
||||
http.Error(w, http.StatusText(statusCode), statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("✅ Template file found, loading...")
|
||||
|
||||
tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), pagePath)
|
||||
|
||||
w.WriteHeader(statusCode)
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
log.Printf("❌ Failed to render error page for %d: %v", statusCode, err)
|
||||
log.Printf("❌ Failed to render error page layout: %v", err)
|
||||
http.Error(w, http.StatusText(statusCode), statusCode)
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("✅ Successfully rendered 500 page")
|
||||
}
|
||||
|
||||
//ToDo Pages.go /template.go to be merged?
|
||||
|
||||
@@ -2,6 +2,7 @@ package helpers
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -59,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] + "..."
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +76,7 @@ func LoadTemplateFiles(name string, files ...string) *template.Template {
|
||||
}
|
||||
all := append(shared, files...)
|
||||
|
||||
log.Printf("📄 Loading templates: %v", all)
|
||||
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(all...))
|
||||
}
|
||||
|
||||
|
||||
3
main.go
3
main.go
@@ -68,6 +68,9 @@ 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)))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"synlotto-website/helpers"
|
||||
)
|
||||
|
||||
func Recover(next http.Handler) http.Handler {
|
||||
@@ -12,7 +13,8 @@ func Recover(next http.Handler) http.Handler {
|
||||
if rec := recover(); rec != nil {
|
||||
log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack())
|
||||
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
// ✅ Call your custom template-based fallback
|
||||
helpers.RenderError(w, r, http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
@@ -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 }}
|
||||
@@ -1,23 +1,29 @@
|
||||
{{ define "notifications" }}
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2 class="mb-4">Notifications</h2>
|
||||
<h2 class="mb-4">Your Notifications</h2>
|
||||
|
||||
{{ if .Notifications }}
|
||||
<ul class="list-group">
|
||||
{{ range .Notifications }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start {{ if not .IsRead }}fw-bold{{ end }}">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-semibold">{{ .Title }}</div>
|
||||
<small class="text-muted">{{ .Message }}</small>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<a href="/account/notifications/read?id={{ .ID }}" class="badge bg-primary text-decoration-none">Mark as read</a>
|
||||
<ul class="list-group">
|
||||
{{ range .Notifications }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start {{ if not .IsRead }}bg-light{{ end }}">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">
|
||||
<a href="/account/notifications/read?id={{ .ID }}" class="{{ if not .IsRead }}text-primary fw-bold{{ end }}">
|
||||
{{ .Subject }}
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">{{ .CreatedAt.Format "Jan 2, 2006 15:04" }}</small>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<span class="badge bg-warning text-dark">New</span>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="alert alert-info">You have no notifications.</div>
|
||||
<div class="alert alert-info text-center">
|
||||
You don’t have any notifications.
|
||||
</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