Compare commits

...

7 Commits

Author SHA1 Message Date
8529116ad2 Messages now sending/loading and populating on message dropdown 2025-10-31 12:08:38 +00:00
776ea53a66 Formatting 2025-10-31 12:00:43 +00:00
5880d1ca43 Fix reading of messages. 2025-10-31 12:00:08 +00:00
da365aa9ef Remove unused functions. 2025-10-31 11:57:39 +00:00
5177194895 Add sender 2025-10-31 09:45:20 +00:00
a7a5169c67 Fix model issues. 2025-10-30 22:19:48 +00:00
262536135d Still working through messages and notifications. 2025-10-30 17:22:52 +00:00
27 changed files with 347 additions and 249 deletions

View File

@@ -7,7 +7,8 @@ import (
type Message = models.Message type Message = models.Message
type CreateMessageInput struct { type CreateMessageInput struct {
RecipientID int64 `form:"to" binding:"required,username"` SenderID int64
RecipientID int64 `form:"recipientId" binding:"required,numeric"`
Subject string `form:"subject" binding:"required,max=200"` Subject string `form:"subject" binding:"required,max=200"`
Body string `form:"body" binding:"required"` Body string `form:"body" binding:"required"`
} }

View File

@@ -7,10 +7,10 @@ package accountMessageHandler
import ( import (
"net/http" "net/http"
templateHandlers "synlotto-website/internal/handlers/template"
templateHelpers "synlotto-website/internal/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/internal/logging" "synlotto-website/internal/logging"
"synlotto-website/internal/models"
"synlotto-website/internal/platform/bootstrap" "synlotto-website/internal/platform/bootstrap"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -31,7 +31,9 @@ func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
return return
} }
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
if f := sm.PopString(c.Request.Context(), "flash"); f != "" { if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
ctx["Flash"] = f ctx["Flash"] = f
} }
@@ -39,10 +41,7 @@ func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
ctx["Title"] = "Archived Messages" ctx["Title"] = "Archived Messages"
ctx["Messages"] = msgs ctx["Messages"] = msgs
tmpl := templateHelpers.LoadTemplateFiles( tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/archived.html")
"layout.html",
"web/templates/account/messages/archived.html",
)
c.Status(http.StatusOK) c.Status(http.StatusOK)
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {

View File

@@ -10,7 +10,6 @@ import (
) )
func mustUserID(c *gin.Context) int64 { func mustUserID(c *gin.Context) int64 {
// Pull from your auth middleware/session. Panic-unsafe alternative:
if v, ok := c.Get("userID"); ok { if v, ok := c.Get("userID"); ok {
if id, ok2 := v.(int64); ok2 { if id, ok2 := v.(int64); ok2 {
return id return id
@@ -19,26 +18,3 @@ func mustUserID(c *gin.Context) int64 {
// Fallback for stubs: // Fallback for stubs:
return 1 return 1
} }
func parseIDParam(c *gin.Context, name string) (int64, error) {
// typical atoi wrapper
// (implement: strconv.ParseInt(c.Param(name), 10, 64))
return atoi64(c.Param(name))
}
func atoi64(s string) (int64, error) {
// small helper to keep imports focused
// replace with strconv.ParseInt in real code
var n int64
for _, ch := range []byte(s) {
if ch < '0' || ch > '9' {
return 0, &strconvNumErr{}
}
n = n*10 + int64(ch-'0')
}
return n, nil
}
type strconvNumErr struct{}
func (e *strconvNumErr) Error() string { return "invalid number" }

View File

@@ -1,16 +1,21 @@
// Package accountMessageHandler // Package accountMessageHandler
// Path: /internal/handlers/account/messages // Path: /internal/handlers/account/messages
// File: read.go // File: read.go
// ToDo: Remove SQL
// add LIMIT/OFFSET in service
package accountMessageHandler package accountMessageHandler
import ( import (
"bytes"
"net/http" "net/http"
"strconv"
templateHandlers "synlotto-website/internal/handlers/template"
templateHelpers "synlotto-website/internal/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
errors "synlotto-website/internal/http/error"
"synlotto-website/internal/logging" "synlotto-website/internal/logging"
"synlotto-website/internal/models"
"synlotto-website/internal/platform/bootstrap" "synlotto-website/internal/platform/bootstrap"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -25,70 +30,105 @@ func (h *AccountMessageHandlers) List(c *gin.Context) {
userID := mustUserID(c) userID := mustUserID(c)
// Pull messages (via service) // 1) Parse page param (default 1)
msgs, err := h.Svc.ListInbox(userID) page := 1
if ps := c.Query("page"); ps != "" {
if n, err := strconv.Atoi(ps); err == nil && n > 0 {
page = n
}
}
pageSize := 20
// 2) Count total for this user (so TotalPages is accurate)
totalPages, totalCount := templateHelpers.GetTotalPages(
app.DB,
"user_messages",
"WHERE recipientId = ? AND is_archived = FALSE",
[]interface{}{userID},
pageSize,
)
if page > totalPages {
page = totalPages
}
// 3) Fetch (existing service returns all inbox items)
msgsAll, err := h.Svc.ListInbox(userID)
if err != nil { if err != nil {
logging.Info("❌ list inbox error: %v", err) logging.Info("❌ list inbox error: %v", err)
c.String(http.StatusInternalServerError, "Failed to load messages") c.String(http.StatusInternalServerError, "Failed to load messages")
return return
} }
// Build template context just like LoginGet // 4) Slice in-memory for now (until you add LIMIT/OFFSET in service)
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) start := (page - 1) * pageSize
if start > len(msgsAll) {
start = len(msgsAll)
}
end := start + pageSize
if end > len(msgsAll) {
end = len(msgsAll)
}
msgs := msgsAll[start:end]
// 5) Build context with paging + CSRF + session-driven user meta
data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
if f := sm.PopString(c.Request.Context(), "flash"); f != "" { if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
ctx["Flash"] = f ctx["Flash"] = f
} }
ctx["CSRFToken"] = nosurf.Token(c.Request) ctx["CSRFToken"] = nosurf.Token(c.Request)
ctx["Title"] = "Messages" ctx["Title"] = "Messages"
ctx["Messages"] = msgs ctx["Messages"] = msgs
ctx["CurrentPage"] = page
ctx["TotalPages"] = totalPages
ctx["TotalCount"] = totalCount
ctx["PageRange"] = templateHelpers.MakePageRange(1, totalPages)
// Use the same loader + layout pattern // 6) Render (Buffer to avoid “headers already written” on error
tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/index.html") tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/index.html")
c.Status(http.StatusOK) var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(&buf, "layout", ctx); err != nil {
logging.Info("❌ Template render error: %v", err) logging.Info("❌ Template render error: %v", err)
c.String(http.StatusInternalServerError, "Error rendering messages page") c.String(http.StatusInternalServerError, "Error rendering messages page")
return
} }
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
} }
// GET /account/messages/:id
// Renders: web/templates/account/messages/read.html // Renders: web/templates/account/messages/read.html
func (h *AccountMessageHandlers) ReadGet(c *gin.Context) { func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
app := c.MustGet("app").(*bootstrap.App) app := c.MustGet("app").(*bootstrap.App)
sm := app.SessionManager sm := app.SessionManager
userID := mustUserID(c) userID := mustUserID(c)
id, err := parseIDParam(c, "id")
if err != nil { idStr := c.Query("id")
c.AbortWithStatus(http.StatusNotFound) id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
errors.RenderStatus(c, sm, http.StatusNotFound)
return return
} }
msg, err := h.Svc.GetByID(userID, id) msg, err := h.Svc.GetByID(userID, id)
if err != nil || msg == nil { if err != nil || msg == nil {
c.AbortWithStatus(http.StatusNotFound) errors.RenderStatus(c, sm, http.StatusNotFound)
return return
} }
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
if f := sm.PopString(c.Request.Context(), "flash"); f != "" { ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
ctx["Flash"] = f
}
ctx["CSRFToken"] = nosurf.Token(c.Request) ctx["CSRFToken"] = nosurf.Token(c.Request)
ctx["Title"] = msg.Subject ctx["Title"] = msg.Subject
ctx["Message"] = msg ctx["Message"] = msg
tmpl := templateHelpers.LoadTemplateFiles( tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/read.html")
"layout.html",
"web/templates/account/messages/read.html",
)
c.Status(http.StatusOK) var buf bytes.Buffer
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(&buf, "layout", ctx); err != nil {
logging.Info("❌ Template render error: %v", err) logging.Info("❌ Template render error: %v", err)
c.String(http.StatusInternalServerError, "Error rendering message") c.String(http.StatusInternalServerError, "Error rendering message")
return
} }
c.Data(http.StatusOK, "text/html; charset=utf-8", buf.Bytes())
} }

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
domain "synlotto-website/internal/domain/messages" domain "synlotto-website/internal/domain/messages"
templateHandlers "synlotto-website/internal/handlers/template"
templateHelpers "synlotto-website/internal/helpers/template" templateHelpers "synlotto-website/internal/helpers/template"
"synlotto-website/internal/logging" "synlotto-website/internal/logging"
@@ -18,23 +19,22 @@ import (
"github.com/justinas/nosurf" "github.com/justinas/nosurf"
) )
// GET /account/messages/add // GET /account/messages/send
// Renders: web/templates/account/messages/send.html // Renders: web/templates/account/messages/send.html
func (h *AccountMessageHandlers) AddGet(c *gin.Context) { func (h *AccountMessageHandlers) SendGet(c *gin.Context) {
app := c.MustGet("app").(*bootstrap.App) app := c.MustGet("app").(*bootstrap.App)
sm := app.SessionManager sm := app.SessionManager
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
if f := sm.PopString(c.Request.Context(), "flash"); f != "" { if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
ctx["Flash"] = f ctx["Flash"] = f
} }
ctx["CSRFToken"] = nosurf.Token(c.Request) ctx["CSRFToken"] = nosurf.Token(c.Request)
ctx["Title"] = "Send Message" ctx["Title"] = "Send Message"
tmpl := templateHelpers.LoadTemplateFiles( tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/send.html")
"layout.html",
"web/templates/account/messages/send.html",
)
c.Status(http.StatusOK) c.Status(http.StatusOK)
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
@@ -43,8 +43,8 @@ func (h *AccountMessageHandlers) AddGet(c *gin.Context) {
} }
} }
// POST /account/messages/add // POST /account/messages/send
func (h *AccountMessageHandlers) AddPost(c *gin.Context) { func (h *AccountMessageHandlers) SendPost(c *gin.Context) {
app := c.MustGet("app").(*bootstrap.App) app := c.MustGet("app").(*bootstrap.App)
sm := app.SessionManager sm := app.SessionManager
@@ -53,7 +53,8 @@ func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
var in domain.CreateMessageInput var in domain.CreateMessageInput
if err := c.ShouldBind(&in); err != nil { if err := c.ShouldBind(&in); err != nil {
// Re-render form with validation errors // Re-render form with validation errors
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, models.TemplateData{}) data := templateHandlers.BuildTemplateData(app, c.Writer, c.Request)
ctx := templateHelpers.TemplateContext(c.Writer, c.Request, data)
if f := sm.PopString(c.Request.Context(), "flash"); f != "" { if f := sm.PopString(c.Request.Context(), "flash"); f != "" {
ctx["Flash"] = f ctx["Flash"] = f
} }
@@ -85,21 +86,17 @@ func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
ctx["Error"] = "Could not send message." ctx["Error"] = "Could not send message."
ctx["Form"] = in ctx["Form"] = in
tmpl := templateHelpers.LoadTemplateFiles( tmpl := templateHelpers.LoadTemplateFiles("layout.html", "web/templates/account/messages/send.html")
"layout.html",
"web/templates/account/messages/send.html",
)
c.Status(http.StatusInternalServerError) c.Status(http.StatusInternalServerError)
if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil { if err := tmpl.ExecuteTemplate(c.Writer, "layout", ctx); err != nil {
logging.Info("❌ Template render error: %v", err) logging.Info("❌ Template render error: %v", err)
c.String(http.StatusInternalServerError, "Error rendering send message page") c.String(http.StatusInternalServerError, "Error rendering send message page")
} }
return return
} }
// Optional: set a flash message for success (since you already PopString elsewhere)
// If you're using scs/v2, Put is available:
sm.Put(c.Request.Context(), "flash", "Message sent!") sm.Put(c.Request.Context(), "flash", "Message sent!")
// Redirect back to inbox // Redirect back to inbox

View File

@@ -38,8 +38,8 @@ func List(c *gin.Context) {
rows, err := app.DB.QueryContext(c.Request.Context(), ` rows, err := app.DB.QueryContext(c.Request.Context(), `
SELECT id, numbers, game, price, purchased_at, created_at SELECT id, numbers, game, price, purchased_at, created_at
FROM tickets FROM my_tickets
WHERE user_id = ? WHERE userId = ?
ORDER BY purchased_at DESC, id DESC ORDER BY purchased_at DESC, id DESC
`, userID) `, userID)
if err != nil { if err != nil {

View File

@@ -18,14 +18,9 @@ import (
// using ONLY session data (no DB) so 404/500 pages don't crash and still // using ONLY session data (no DB) so 404/500 pages don't crash and still
// look "logged in" when a session exists. // look "logged in" when a session exists.
func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) { func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) {
// Synthesize minimal TemplateData from session only r := c.Request
var data models.TemplateData uid := int64(0)
if v := sessions.Get(r.Context(), sessionkeys.UserID); v != nil {
ctx := c.Request.Context()
// Read minimal user snapshot from session
var uid int64
if v := sessions.Get(ctx, sessionkeys.UserID); v != nil {
switch t := v.(type) { switch t := v.(type) {
case int64: case int64:
uid = t uid = t
@@ -33,22 +28,22 @@ func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) {
uid = int64(t) uid = int64(t)
} }
} }
// --- build minimal template data from session
var data models.TemplateData
if uid > 0 { if uid > 0 {
// username and is_admin are optional but make navbar correct uname := ""
var uname string if v := sessions.Get(r.Context(), sessionkeys.Username); v != nil {
if v := sessions.Get(ctx, sessionkeys.Username); v != nil {
if s, ok := v.(string); ok { if s, ok := v.(string); ok {
uname = s uname = s
} }
} }
var isAdmin bool isAdmin := false
if v := sessions.Get(ctx, sessionkeys.IsAdmin); v != nil { if v := sessions.Get(r.Context(), sessionkeys.IsAdmin); v != nil {
if b, ok := v.(bool); ok { if b, ok := v.(bool); ok {
isAdmin = b isAdmin = b
} }
} }
// Build a lightweight user; avoids DB lookups in error paths
data.User = &models.User{ data.User = &models.User{
Id: uid, Id: uid,
Username: uname, Username: uname,
@@ -57,15 +52,11 @@ func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) {
data.IsAdmin = isAdmin data.IsAdmin = isAdmin
} }
// Turn into the template context map (adds site meta, funcs, etc.) ctxMap := templateHelpers.TemplateContext(c.Writer, r, data)
ctxMap := templateHelpers.TemplateContext(c.Writer, c.Request, data) if f := sessions.PopString(r.Context(), sessionkeys.Flash); f != "" {
// Flash (SCS)
if f := sessions.PopString(ctx, sessionkeys.Flash); f != "" {
ctxMap["Flash"] = f ctxMap["Flash"] = f
} }
// Template paths (layout-first)
pagePath := fmt.Sprintf("web/templates/error/%d.html", status) pagePath := fmt.Sprintf("web/templates/error/%d.html", status)
if _, err := os.Stat(pagePath); err != nil { if _, err := os.Stat(pagePath); err != nil {
c.String(status, http.StatusText(status)) c.String(status, http.StatusText(status))
@@ -86,11 +77,13 @@ func RenderStatus(c *gin.Context, sessions *scs.SessionManager, status int) {
func NoRoute(sessions *scs.SessionManager) gin.HandlerFunc { func NoRoute(sessions *scs.SessionManager) gin.HandlerFunc {
return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusNotFound) } return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusNotFound) }
} }
func NoMethod(sessions *scs.SessionManager) gin.HandlerFunc { func NoMethod(sessions *scs.SessionManager) gin.HandlerFunc {
return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusMethodNotAllowed) } return func(c *gin.Context) { RenderStatus(c, sessions, http.StatusMethodNotAllowed) }
} }
func Recovery(sessions *scs.SessionManager) gin.RecoveryFunc { func Recovery(sessions *scs.SessionManager) gin.RecoveryFunc {
return func(c *gin.Context, _ interface{}) { return func(c *gin.Context, rec interface{}) {
RenderStatus(c, sessions, http.StatusInternalServerError) RenderStatus(c, sessions, http.StatusInternalServerError)
} }
} }

View File

@@ -0,0 +1,26 @@
// internal/http/middleware/errorlog.go
package middleware
import (
"time"
"synlotto-website/internal/logging"
"github.com/gin-gonic/gin"
)
func ErrorLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
if len(c.Errors) == 0 {
return
}
for _, e := range c.Errors {
logging.Info("❌ %s %s -> %d in %v: %v",
c.Request.Method, c.FullPath(), c.Writer.Status(),
time.Since(start), e.Err)
}
}
}

View File

@@ -66,10 +66,11 @@ func RegisterAccountRoutes(app *bootstrap.App) {
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth()) messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
{ {
messages.GET("/", msgH.List) messages.GET("/", msgH.List)
messages.GET("/add", msgH.AddGet) messages.GET("/send", msgH.SendGet)
messages.POST("/add", msgH.AddPost) messages.POST("/send", msgH.SendPost)
messages.GET("/archived", msgH.ArchivedList) // renders archived.html messages.GET("/archived", msgH.ArchivedList) // renders archived.html
messages.GET("/:id", msgH.ReadGet) // renders read.html messages.GET("/read", msgH.ReadGet)
} }
// Notifications (auth-required) // Notifications (auth-required)

View File

@@ -0,0 +1,15 @@
package models
import "time"
type Message struct {
ID int
SenderId int
RecipientId int
Subject string
Body string
IsRead bool
IsArchived bool
CreatedAt time.Time
ArchivedAt *time.Time
}

View File

@@ -0,0 +1,12 @@
package models
import "time"
type Notification struct {
ID int
UserId int
Title string
Body string
IsRead bool
CreatedAt time.Time
}

View File

@@ -13,26 +13,3 @@ type User struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
} }
// ToDo: should be in a notification model?
type Notification struct {
ID int
UserId int
Title string
Body string
IsRead bool
CreatedAt time.Time
}
// ToDo: should be in a message model?
type Message struct {
ID int
SenderId int
RecipientId int
Subject string
Body string
IsRead bool
IsArchived bool
CreatedAt time.Time
ArchivedAt *time.Time
}

View File

@@ -151,7 +151,7 @@ func Load(configPath string) (*App, error) {
srv := &http.Server{ srv := &http.Server{
Addr: addr, Addr: addr,
Handler: handler, Handler: handler,
ReadHeaderTimeout: 10 * time.Second, // ToDo: consider moving to config ReadHeaderTimeout: cfg.HttpServer.ReadHeaderTimeout,
} }
app.Handler = handler app.Handler = handler

View File

@@ -30,6 +30,8 @@
package config package config
import "time"
// Config represents all runtime configuration for the application. // Config represents all runtime configuration for the application.
// Loaded from JSON and passed into bootstrap for wiring platform components. // Loaded from JSON and passed into bootstrap for wiring platform components.
type Config struct { type Config struct {
@@ -55,6 +57,7 @@ type Config struct {
Port int `json:"port"` Port int `json:"port"`
Address string `json:"address"` Address string `json:"address"`
ProductionMode bool `json:"productionMode"` // controls Secure cookie flag ProductionMode bool `json:"productionMode"` // controls Secure cookie flag
ReadHeaderTimeout time.Duration `json:"readHeaderTimeout"` // config in nanoseconds
} `json:"httpServer"` } `json:"httpServer"`
// Remote licensing API service configuration // Remote licensing API service configuration

View File

@@ -8,9 +8,14 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"fmt"
"time" "time"
"synlotto-website/internal/logging"
domain "synlotto-website/internal/domain/messages" domain "synlotto-website/internal/domain/messages"
"github.com/go-sql-driver/mysql"
) )
// Service implements domain.Service. // Service implements domain.Service.
@@ -37,14 +42,15 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
// Ensure *Service satisfies the domain interface. // Ensure *Service satisfies the domain interface.
var _ domain.MessageService = (*Service)(nil) var _ domain.MessageService = (*Service)(nil)
// ToDo: Needs a userId on table or rename the recipiant id.. but then again dont want to expose userids to users for sending.
func (s *Service) ListInbox(userID int64) ([]domain.Message, error) { func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel() defer cancel()
q := ` q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
FROM users_messages FROM user_messages
WHERE user_id = ? AND is_archived = FALSE WHERE recipientId = ? AND is_archived = FALSE
ORDER BY created_at DESC` ORDER BY created_at DESC`
q = s.bind(q) q = s.bind(q)
@@ -57,7 +63,7 @@ func (s *Service) ListInbox(userID int64) ([]domain.Message, error) {
var out []domain.Message var out []domain.Message
for rows.Next() { for rows.Next() {
var m domain.Message var m domain.Message
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
return nil, err return nil, err
} }
out = append(out, m) out = append(out, m)
@@ -71,9 +77,9 @@ func (s *Service) ListArchived(userID int64) ([]domain.Message, error) {
defer cancel() defer cancel()
q := ` q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
FROM users_messages FROM user_messages
WHERE user_id = ? AND is_archived = TRUE WHERE recipientId = ? AND is_archived = TRUE
ORDER BY created_at DESC` ORDER BY created_at DESC`
q = s.bind(q) q = s.bind(q)
@@ -86,7 +92,7 @@ func (s *Service) ListArchived(userID int64) ([]domain.Message, error) {
var out []domain.Message var out []domain.Message
for rows.Next() { for rows.Next() {
var m domain.Message var m domain.Message
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil { if err := rows.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
return nil, err return nil, err
} }
out = append(out, m) out = append(out, m)
@@ -99,14 +105,14 @@ func (s *Service) GetByID(userID, id int64) (*domain.Message, error) {
defer cancel() defer cancel()
q := ` q := `
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at SELECT id, senderId, recipientId, subject, body, is_read, is_archived, created_at
FROM users_messages FROM user_messages
WHERE user_id = ? AND id = ?` WHERE recipientId = ? AND id = ?`
q = s.bind(q) q = s.bind(q)
var m domain.Message var m domain.Message
err := s.DB.QueryRowContext(ctx, q, userID, id). err := s.DB.QueryRowContext(ctx, q, userID, id).
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt) Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, nil return nil, nil
} }
@@ -116,34 +122,66 @@ func (s *Service) GetByID(userID, id int64) (*domain.Message, error) {
return &m, nil return &m, nil
} }
func (s *Service) Create(userID int64, in domain.CreateMessageInput) (int64, error) { func (s *Service) Create(senderID int64, in domain.CreateMessageInput) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel() defer cancel()
switch s.Dialect { // ✅ make sure this matches your current table/column names
case "postgres":
const q = ` const q = `
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at) INSERT INTO user_messages
VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, NOW()) (senderId, recipientId, subject, body, is_read, is_archived, created_at)
RETURNING id` VALUES
var id int64 (?, ?, ?, ?, 0, 0, CURRENT_TIMESTAMP)
if err := s.DB.QueryRowContext(ctx, q, userID, "", in.To, in.Subject, in.Body).Scan(&id); err != nil { `
return 0, err
} // 👀 Log the SQL and arguments (truncate body in logs if you prefer)
return id, nil logging.Info("🧪 SQL Exec: %s | args: senderId=%d recipientId=%d subject=%q body_len=%d", compactSQL(q), senderID, in.RecipientID, in.Subject, len(in.Body))
default: // mysql/sqlite
const q = ` res, err := s.DB.ExecContext(ctx, q, senderID, in.RecipientID, in.Subject, in.Body)
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
VALUES (?, ?, ?, ?, ?, FALSE, FALSE, CURRENT_TIMESTAMP)`
res, err := s.DB.ExecContext(ctx, q, userID, "", in.To, in.Subject, in.Body)
if err != nil { if err != nil {
return 0, err // Surface MySQL code/message (very helpful for FK #1452 etc.)
} var me *mysql.MySQLError
return res.LastInsertId() if errors.As(err, &me) {
wrapped := fmt.Errorf("insert user_messages: mysql #%d %s | args(senderId=%d, recipientId=%d, subject=%q, body_len=%d)",
me.Number, me.Message, senderID, in.RecipientID, in.Subject, len(in.Body))
logging.Info("❌ %v", wrapped)
return 0, wrapped
} }
wrapped := fmt.Errorf("insert user_messages: %w | args(senderId=%d, recipientId=%d, subject=%q, body_len=%d)",
err, senderID, in.RecipientID, in.Subject, len(in.Body))
logging.Info("❌ %v", wrapped)
return 0, wrapped
}
id, err := res.LastInsertId()
if err != nil {
wrapped := fmt.Errorf("lastInsertId user_messages: %w", err)
logging.Info("❌ %v", wrapped)
return 0, wrapped
}
logging.Info("✅ Inserted message id=%d", id)
return id, nil
}
// compactSQL removes newlines/extra spaces for cleaner logs
func compactSQL(s string) string {
out := make([]rune, 0, len(s))
space := false
for _, r := range s {
if r == '\n' || r == '\t' || r == '\r' || r == ' ' {
if !space {
out = append(out, ' ')
space = true
}
continue
}
space = false
out = append(out, r)
}
return string(out)
} }
// bind replaces '?' with '$1..' only for Postgres. For MySQL/SQLite it returns q unchanged.
func (s *Service) bind(q string) string { func (s *Service) bind(q string) string {
if s.Dialect != "postgres" { if s.Dialect != "postgres" {
return q return q

View File

@@ -35,14 +35,15 @@ func New(db *sql.DB, opts ...func(*Service)) *Service {
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } } func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
// List returns newest-first notifications for a user. // List returns newest-first notifications for a user.
// ToDo:table is users_notification, where as messages is plural, this table seems oto use user_id reather than userId need to unify. Do i want to prefix with users/user
func (s *Service) List(userID int64) ([]domain.Notification, error) { func (s *Service) List(userID int64) ([]domain.Notification, error) {
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout) ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
defer cancel() defer cancel()
const q = ` const q = `
SELECT id, title, body, is_read, created_at SELECT id, title, body, is_read, created_at
FROM notifications FROM users_notification
WHERE user_id = ? WHERE user_Id = ?
ORDER BY created_at DESC` ORDER BY created_at DESC`
rows, err := s.DB.QueryContext(ctx, q, userID) rows, err := s.DB.QueryContext(ctx, q, userID)
@@ -69,7 +70,7 @@ func (s *Service) GetByID(userID, id int64) (*domain.Notification, error) {
const q = ` const q = `
SELECT id, title, body, is_read, created_at SELECT id, title, body, is_read, created_at
FROM notifications FROM notifications
WHERE user_id = ? AND id = ?` WHERE userId = ? AND id = ?`
var n domain.Notification var n domain.Notification
err := s.DB.QueryRowContext(ctx, q, userID, id). err := s.DB.QueryRowContext(ctx, q, userID, id).

View File

@@ -4,10 +4,10 @@ import (
"database/sql" "database/sql"
) )
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error { func SendMessage(db *sql.DB, senderID, recipientID int, subject, body string) error {
_, err := db.Exec(` _, err := db.Exec(`
INSERT INTO users_messages (senderId, recipientId, subject, message) INSERT INTO user_messages (senderId, recipientId, subject, body)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?, ?)
`, senderID, recipientID, subject, message) `, senderID, recipientID, subject, body)
return err return err
} }

View File

@@ -9,7 +9,7 @@ import (
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 recipientId = ? AND is_read = FALSE AND is_archived = FALSE WHERE recipientId = ? AND is_read = FALSE AND is_archived = FALSE
`, userID).Scan(&count) `, userID).Scan(&count)
return count, err return count, err
@@ -17,8 +17,8 @@ func GetMessageCount(db *sql.DB, userID int) (int, error) {
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, senderId, recipientId, subject, message, is_read, created_at SELECT id, senderId, recipientId, subject, body, is_read, created_at
FROM users_messages FROM user_messages
WHERE recipientId = ? AND is_archived = FALSE WHERE recipientId = ? AND is_archived = FALSE
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ? LIMIT ?
@@ -36,7 +36,7 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
&m.SenderId, &m.SenderId,
&m.RecipientId, &m.RecipientId,
&m.Subject, &m.Subject,
&m.Message, &m.Body,
&m.IsRead, &m.IsRead,
&m.CreatedAt, &m.CreatedAt,
) )
@@ -49,13 +49,13 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error) { func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error) {
row := db.QueryRow(` row := db.QueryRow(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at SELECT id, senderId, recipientId, subject, body, is_read, created_at
FROM users_messages FROM user_messages
WHERE id = ? AND recipientId = ? WHERE id = ? AND recipientId = ?
`, messageID, userID) `, messageID, userID)
var m models.Message var m models.Message
err := row.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Message, &m.IsRead, &m.CreatedAt) err := row.Scan(&m.ID, &m.SenderId, &m.RecipientId, &m.Subject, &m.Body, &m.IsRead, &m.CreatedAt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -65,8 +65,8 @@ func GetMessageByID(db *sql.DB, userID, messageID int) (*models.Message, error)
func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message { func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage offset := (page - 1) * perPage
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at, archived_at SELECT id, senderId, recipientId, subject, body, is_read, created_at, archived_at
FROM users_messages FROM user_messages
WHERE recipientId = ? AND is_archived = TRUE WHERE recipientId = ? AND is_archived = TRUE
ORDER BY archived_at DESC ORDER BY archived_at DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
@@ -81,7 +81,7 @@ func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Mes
var m models.Message var m models.Message
err := rows.Scan( err := rows.Scan(
&m.ID, &m.SenderId, &m.RecipientId, &m.ID, &m.SenderId, &m.RecipientId,
&m.Subject, &m.Message, &m.IsRead, &m.Subject, &m.Body, &m.IsRead,
&m.CreatedAt, &m.ArchivedAt, &m.CreatedAt, &m.ArchivedAt,
) )
if err == nil { if err == nil {
@@ -94,8 +94,8 @@ func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Mes
func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Message { func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
offset := (page - 1) * perPage offset := (page - 1) * perPage
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, senderId, recipientId, subject, message, is_read, created_at SELECT id, senderId, recipientId, subject, body, is_read, created_at
FROM users_messages FROM user_messages
WHERE recipientId = ? AND is_archived = FALSE WHERE recipientId = ? AND is_archived = FALSE
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
@@ -110,7 +110,7 @@ func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Messag
var m models.Message var m models.Message
err := rows.Scan( err := rows.Scan(
&m.ID, &m.SenderId, &m.RecipientId, &m.ID, &m.SenderId, &m.RecipientId,
&m.Subject, &m.Message, &m.IsRead, &m.CreatedAt, &m.Subject, &m.Body, &m.IsRead, &m.CreatedAt,
) )
if err == nil { if err == nil {
messages = append(messages, m) messages = append(messages, m)
@@ -122,7 +122,7 @@ func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Messag
func GetInboxMessageCount(db *sql.DB, userID int) int { func GetInboxMessageCount(db *sql.DB, userID int) int {
var count int var count int
err := db.QueryRow(` err := db.QueryRow(`
SELECT COUNT(*) FROM users_messages SELECT COUNT(*) FROM user_messages
WHERE recipientId = ? AND is_archived = FALSE WHERE recipientId = ? AND is_archived = FALSE
`, userID).Scan(&count) `, userID).Scan(&count)
if err != nil { if err != nil {

View File

@@ -7,7 +7,7 @@ import (
func ArchiveMessage(db *sql.DB, userID, messageID int) error { func ArchiveMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(` _, err := db.Exec(`
UPDATE users_messages UPDATE user_messages
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
WHERE id = ? AND recipientId = ? WHERE id = ? AND recipientId = ?
`, messageID, userID) `, messageID, userID)
@@ -16,7 +16,7 @@ func ArchiveMessage(db *sql.DB, userID, messageID int) error {
func MarkMessageAsRead(db *sql.DB, messageID, userID int) error { func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
result, err := db.Exec(` result, err := db.Exec(`
UPDATE users_messages UPDATE user_messages
SET is_read = TRUE SET is_read = TRUE
WHERE id = ? AND recipientId = ? WHERE id = ? AND recipientId = ?
`, messageID, userID) `, messageID, userID)
@@ -36,7 +36,7 @@ func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
func RestoreMessage(db *sql.DB, userID, messageID int) error { func RestoreMessage(db *sql.DB, userID, messageID int) error {
_, err := db.Exec(` _, err := db.Exec(`
UPDATE users_messages UPDATE user_messages
SET is_archived = FALSE, archived_at = NULL SET is_archived = FALSE, archived_at = NULL
WHERE id = ? AND recipientId = ? WHERE id = ? AND recipientId = ?
`, messageID, userID) `, messageID, userID)

View File

@@ -140,20 +140,20 @@ CREATE TABLE IF NOT EXISTS my_tickets (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- USERS MESSAGES -- USERS MESSAGES
CREATE TABLE IF NOT EXISTS users_messages ( CREATE TABLE IF NOT EXISTS user_messages (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
senderId BIGINT UNSIGNED NOT NULL, senderId BIGINT UNSIGNED NOT NULL,
recipientId BIGINT UNSIGNED NOT NULL, recipientId BIGINT UNSIGNED NOT NULL,
subject VARCHAR(255) NOT NULL, subject VARCHAR(255) NOT NULL,
message MEDIUMTEXT, body MEDIUMTEXT,
is_read TINYINT(1) NOT NULL DEFAULT 0, is_read TINYINT(1) NOT NULL DEFAULT 0,
is_archived TINYINT(1) NOT NULL DEFAULT 0, is_archived TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
archived_at DATETIME NULL, archived_at DATETIME NULL,
CONSTRAINT fk_users_messages_sender CONSTRAINT fk_user_messages_sender
FOREIGN KEY (senderId) REFERENCES users(id) FOREIGN KEY (senderId) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE, ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT fk_users_messages_recipient CONSTRAINT fk_user_messages_recipient
FOREIGN KEY (recipientId) REFERENCES users(id) FOREIGN KEY (recipientId) REFERENCES users(id)
ON UPDATE CASCADE ON DELETE CASCADE ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

View File

@@ -16,7 +16,7 @@ func GetNotificationByID(db *sql.DB, userID, notificationID int) (*models.Notifi
`, notificationID, userID) `, notificationID, userID)
var n models.Notification var n models.Notification
err := row.Scan(&n.ID, &n.UserId, &n.Subject, &n.Body, &n.IsRead) err := row.Scan(&n.ID, &n.UserId, &n.Title, &n.Body, &n.IsRead)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -27,7 +27,7 @@ func GetNotificationCount(db *sql.DB, userID int) int {
var count int var count int
err := db.QueryRow(` err := db.QueryRow(`
SELECT COUNT(*) FROM users_notification SELECT COUNT(*) FROM users_notification
WHERE user_id = ? AND is_read = FALSE`, userID).Scan(&count) WHERE user_Id = ? AND is_read = FALSE`, userID).Scan(&count)
if err != nil { if err != nil {
log.Println("⚠️ Failed to count notifications:", err) log.Println("⚠️ Failed to count notifications:", err)
@@ -41,7 +41,7 @@ func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notifica
rows, err := db.Query(` rows, err := db.Query(`
SELECT id, subject, body, is_read, created_at SELECT id, subject, body, is_read, created_at
FROM users_notification FROM users_notification
WHERE user_id = ? WHERE user_Id = ?
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT ?`, userID, limit) LIMIT ?`, userID, limit)
if err != nil { if err != nil {
@@ -54,7 +54,7 @@ func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notifica
for rows.Next() { for rows.Next() {
var n models.Notification var n models.Notification
if err := rows.Scan(&n.ID, &n.Subject, &n.Body, &n.IsRead, &n.CreatedAt); err == nil { if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err == nil {
notifications = append(notifications, n) notifications = append(notifications, n)
} }
} }

View File

@@ -7,20 +7,27 @@
<div class="card mb-3"> <div class="card mb-3">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">{{ .Subject }}</h5> <h5 class="card-title">{{ .Subject }}</h5>
<p class="card-text">{{ .Message }}</p> <p class="card-text">{{ .Body }}</p>
<p class="card-text"> <p class="card-text">
<small class="text-muted">Archived: {{ .ArchivedAt.Format "02 Jan 2006 15:04" }}</small> <small class="text-muted">
Archived:
{{ if .ArchivedAt.Valid }}
{{ .ArchivedAt.Time.Format "02 Jan 2006 15:04" }}
{{ else }}
{{ end }}
</small>
</p> </p>
</div>
</div>
<form method="POST" action="/account/messages/restore" class="m-0"> <form method="POST" action="/account/messages/restore" class="m-0">
{{ $.CSRFField }} <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<input type="hidden" name="id" value="{{ .ID }}"> <input type="hidden" name="id" value="{{ .ID }}">
<button type="submit" class="btn btn-sm btn-outline-success">Restore</button> <button type="submit" class="btn btn-sm btn-outline-success">Restore</button>
</form> </form>
</div>
</div>
{{ end }} {{ end }}
<!-- Pagination Controls --> <!-- Pagination Controls (keep if your funcs exist) -->
<nav> <nav>
<ul class="pagination"> <ul class="pagination">
{{ if gt .Page 1 }} {{ if gt .Page 1 }}

View File

@@ -1,25 +1,24 @@
{{ define "content" }} {{ define "content" }}
<!-- Todo lists messages but doesn't show which ones have been read and unread-->
<div class="container py-5"> <div class="container py-5">
<h2>Your Inbox</h2> <h2>Your Inbox</h2>
{{ if .Messages }} {{ if .Messages }}
<ul class="list-group mb-4"> <ul class="list-group mb-4">
{{ range .Messages }} {{ range .Messages }}
<li class="list-group-item d-flex justify-content-between align-items-center"> <li class="list-group-item d-flex justify-content-between align-items-center">
<div> <div>
<a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a> <a href="/account/messages/read?={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a><br>
<br>
<small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small> <small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
</div> </div>
<form method="POST" action="/account/messages/archive?id={{ .ID }}" class="m-0"> <form method="POST" action="/account/messages/archive" class="m-0">
{{ $.CSRFField }} <input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<input type="hidden" name="id" value="{{ .ID }}"> <input type="hidden" name="id" value="{{ .ID }}">
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button> <button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
</form> </form>
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
<!-- Pagination -->
<nav> <nav>
<ul class="pagination"> <ul class="pagination">
{{ if gt .CurrentPage 1 }} {{ if gt .CurrentPage 1 }}
@@ -41,10 +40,8 @@
{{ end }} {{ end }}
</ul> </ul>
</nav> </nav>
{{ else }} {{ else }}
<div class="alert alert-info">No messages found.</div> <div class="alert alert-info text-center">No messages found.</div>
{{ end }} {{ end }}
<div class="mt-3"> <div class="mt-3">

View File

@@ -4,8 +4,15 @@
<h2>{{ .Message.Subject }}</h2> <h2>{{ .Message.Subject }}</h2>
<p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p> <p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p>
<hr> <hr>
<p>{{ .Message.Message }}</p> <p>{{ .Message.Body }}</p>
<a href="/account/messages" class="btn btn-secondary mt-4">Back to Inbox</a> <a href="/account/messages/archive?id={{ .Message.ID }}" class="btn btn-outline-danger mt-3">Archive</a>
<form method="POST" action="/account/messages/archive" class="d-inline">
<input type="hidden" name="csrf_token" value="{{ $.CSRFToken }}">
<input type="hidden" name="id" value="{{ .Message.ID }}">
<button type="submit" class="btn btn-outline-danger mt-3">Archive</button>
</form>
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
{{ else }} {{ else }}
<div class="alert alert-danger text-center"> <div class="alert alert-danger text-center">
Message not found or access denied. Message not found or access denied.

View File

@@ -1,24 +1,32 @@
{{ define "content" }} {{ define "content" }}
<div class="container py-5"> <div class="container py-5">
<h2>Send a Message</h2> <h2>Send a Message</h2>
{{ if .Flash }} {{ if .Flash }}
<div class="alert alert-info">{{ .Flash }}</div> <div class="alert alert-info">{{ .Flash }}</div>
{{ end }} {{ end }}
{{ if .Error }}
<div class="alert alert-danger">{{ .Error }}</div>
{{ end }}
<form method="POST" action="/account/messages/send"> <form method="POST" action="/account/messages/send">
{{ .CSRFField }} <input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="mb-3"> <div class="mb-3">
<label for="recipient_id" class="form-label">Recipient User ID</label> <label for="recipientId" class="form-label">Recipient User ID</label>
<input type="number" class="form-control" name="recipient_id" required> <input type="number" class="form-control" name="recipientId" value="{{ with .Form }}{{ .RecipientID }}{{ end }}" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="subject" class="form-label">Subject</label> <label for="subject" class="form-label">Subject</label>
<input type="text" class="form-control" name="subject" required> <input type="text" class="form-control" name="subject" value="{{ with .Form }}{{ .Subject }}{{ end }}" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="message" class="form-label">Message</label> <label for="body" class="form-label">Message</label>
<textarea class="form-control" name="message" rows="5" required></textarea> <textarea class="form-control" name="body" rows="5" required>{{ with .Form }}{{ .Body }}{{ end }}</textarea>
</div> </div>
<button type="submit" class="btn btn-primary">Send</button> <button type="submit" class="btn btn-primary">Send</button>
</form> </form>

View File

@@ -13,7 +13,7 @@
<a href="/legal/terms">Terms & Conditions</a> | <a href="/legal/terms">Terms & Conditions</a> |
<a href="/contact">Contact Us</a> <a href="/contact">Contact Us</a>
<br> <br>
The content and operations of this website have not been approved or endorsed by {{ $lotteryOperator }} or the {{ $commisionName }}.
</small> </small>
</footer> </footer>
{{ end }} {{ end }}

View File

@@ -94,7 +94,7 @@
<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">{{ $m.Subject }}</div>
<small class="text-muted">{{ truncate $m.Message 40 }}</small> <small class="text-muted">{{ truncate $m.Body 40 }}</small>
</div> </div>
</div> </div>
</a> </a>