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)
|
data := BuildTemplateData(db, w, r)
|
||||||
context := helpers.TemplateContext(w, r, data)
|
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)
|
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,24 +1,40 @@
|
|||||||
package helpers
|
package helpers
|
||||||
|
|
||||||
// ToDo should be a handler?
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"synlotto-website/models"
|
"synlotto-website/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
func RenderError(w http.ResponseWriter, r *http.Request, statusCode int) {
|
||||||
|
log.Printf("⚙️ RenderError called with status: %d", statusCode)
|
||||||
|
|
||||||
context := TemplateContext(w, r, models.TemplateData{})
|
context := TemplateContext(w, r, models.TemplateData{})
|
||||||
|
|
||||||
page := fmt.Sprintf("templates/error/%d.html", statusCode)
|
pagePath := fmt.Sprintf("templates/error/%d.html", statusCode)
|
||||||
tmpl := LoadTemplateFiles(fmt.Sprintf("%d.html", statusCode), page)
|
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)
|
w.WriteHeader(statusCode)
|
||||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||||
if err != nil {
|
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)
|
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 (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -59,6 +60,12 @@ func TemplateFuncs() template.FuncMap {
|
|||||||
"inSlice": InSlice,
|
"inSlice": InSlice,
|
||||||
"lower": lower,
|
"lower": lower,
|
||||||
"rangeClass": rangeClass,
|
"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...)
|
all := append(shared, files...)
|
||||||
|
|
||||||
|
log.Printf("📄 Loading templates: %v", all)
|
||||||
return template.Must(template.New(name).Funcs(TemplateFuncs()).ParseFiles(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("/signup", middleware.Auth(false)(handlers.Signup))
|
||||||
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
|
mux.HandleFunc("/account/tickets/add_ticket", handlers.AddTicket(db))
|
||||||
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(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)))
|
mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"synlotto-website/helpers"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Recover(next http.Handler) http.Handler {
|
func Recover(next http.Handler) http.Handler {
|
||||||
@@ -12,7 +13,8 @@ func Recover(next http.Handler) http.Handler {
|
|||||||
if rec := recover(); rec != nil {
|
if rec := recover(); rec != nil {
|
||||||
log.Printf("🔥 Recovered from panic: %v\n%s", rec, debug.Stack())
|
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)
|
next.ServeHTTP(w, r)
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ type Notification struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
ID int
|
ID int
|
||||||
Sender string
|
SenderId int
|
||||||
Subject string
|
RecipientId int
|
||||||
Message string
|
Subject string
|
||||||
IsRead bool
|
Message string
|
||||||
CreatedAt time.Time
|
IsRead bool
|
||||||
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
|||||||
@@ -2,24 +2,25 @@ package storage
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"synlotto-website/models"
|
"synlotto-website/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetMessageCount(db *sql.DB, userID int) (int, error) {
|
func GetMessageCount(db *sql.DB, userID int) (int, error) {
|
||||||
var count int
|
var count int
|
||||||
err := db.QueryRow(`
|
err := db.QueryRow(`
|
||||||
SELECT COUNT(*) FROM users_messages
|
SELECT COUNT(*) FROM user_messages
|
||||||
WHERE user_id = ? AND is_read = FALSE
|
WHERE recipient_id = ? AND is_read = FALSE
|
||||||
`, userID).Scan(&count)
|
`, userID).Scan(&count)
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
SELECT id, title, message, is_read
|
SELECT id, sender_id, recipient_id, subject, body, is_read, created_at
|
||||||
FROM users_messages
|
FROM user_messages
|
||||||
WHERE user_id = ?
|
WHERE recipient_id = ?
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT ?
|
LIMIT ?
|
||||||
`, userID, limit)
|
`, userID, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30,9 +31,53 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
|||||||
var messages []models.Message
|
var messages []models.Message
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var m models.Message
|
var m models.Message
|
||||||
rows.Scan(&m.ID, &m.Subject, &m.Message, &m.IsRead)
|
err := rows.Scan(
|
||||||
messages = append(messages, m)
|
&m.ID,
|
||||||
|
&m.SenderId,
|
||||||
|
&m.RecipientId,
|
||||||
|
&m.Subject,
|
||||||
|
&m.Message,
|
||||||
|
&m.IsRead,
|
||||||
|
&m.CreatedAt,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
messages = append(messages, m)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages
|
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 = `
|
const SchemaUsersMessages = `
|
||||||
CREATE TABLE IF NOT EXISTS users_messages (
|
CREATE TABLE IF NOT EXISTS users_messages (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
user_id INTEGER NOT NULL REFERENCES users(id),
|
senderId INTEGER NOT NULL REFERENCES users(id),
|
||||||
title TEXT NOT NULL,
|
recipientId int,
|
||||||
|
subject TEXT NOT NULL,
|
||||||
message TEXT,
|
message TEXT,
|
||||||
is_read BOOLEAN DEFAULT FALSE,
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
type VARCHAR(50),
|
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">
|
<div class="container py-4">
|
||||||
<h2 class="mb-4">Notifications</h2>
|
<h2 class="mb-4">Your Notifications</h2>
|
||||||
|
|
||||||
{{ if .Notifications }}
|
{{ if .Notifications }}
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{{ range .Notifications }}
|
{{ range .Notifications }}
|
||||||
<li class="list-group-item d-flex justify-content-between align-items-start {{ if not .IsRead }}fw-bold{{ end }}">
|
<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="ms-2 me-auto">
|
||||||
<div class="fw-semibold">{{ .Title }}</div>
|
<div class="fw-bold">
|
||||||
<small class="text-muted">{{ .Message }}</small>
|
<a href="/account/notifications/read?id={{ .ID }}" class="{{ if not .IsRead }}text-primary fw-bold{{ end }}">
|
||||||
</div>
|
{{ .Subject }}
|
||||||
{{ if not .IsRead }}
|
</a>
|
||||||
<a href="/account/notifications/read?id={{ .ID }}" class="badge bg-primary text-decoration-none">Mark as read</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 }}
|
{{ end }}
|
||||||
</li>
|
</ul>
|
||||||
{{ end }}
|
|
||||||
</ul>
|
|
||||||
{{ else }}
|
{{ 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 }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|||||||
@@ -82,33 +82,35 @@
|
|||||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
|
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
|
||||||
aria-expanded="false">
|
aria-expanded="false">
|
||||||
<i class="bi bi-envelope fs-5 position-relative">
|
<i class="bi bi-envelope fs-5 position-relative">
|
||||||
<!-- Unread badge (example: 2 messages) -->
|
{{ if gt .MessageCount 0 }}
|
||||||
<span
|
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
||||||
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
|
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
</i>
|
</i>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
|
||||||
aria-labelledby="messageDropdown">
|
aria-labelledby="messageDropdown">
|
||||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||||
<li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
<hr class="dropdown-divider">
|
|
||||||
</li>
|
{{ if .Messages }}
|
||||||
|
{{ range $i, $m := .Messages }}
|
||||||
<!-- Example message item -->
|
<li class="px-3 py-2">
|
||||||
<li class="px-3 py-2">
|
<div class="d-flex align-items-start">
|
||||||
<div class="d-flex align-items-start">
|
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
<div>
|
||||||
<div>
|
<div class="fw-semibold">{{ $m.Subject }}</div>
|
||||||
<div class="fw-semibold">Admin</div>
|
<small class="text-muted">{{ truncate $m.Body 40 }}</small>
|
||||||
<small class="text-muted">Welcome to SynLotto!</small>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</li>
|
||||||
</li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{{ end }}
|
||||||
<li>
|
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||||
<hr class="dropdown-divider">
|
{{ else }}
|
||||||
</li>
|
<li class="text-center text-muted py-2">No messages</li>
|
||||||
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user