Compare commits
5 Commits
f458250d3a
...
b41e92629b
| Author | SHA1 | Date | |
|---|---|---|---|
| b41e92629b | |||
| 0b2883a494 | |||
| 5520685504 | |||
| e2b30c0234 | |||
| 07f7a50b77 |
26
internal/handlers/account/messages/archive.go
Normal file
26
internal/handlers/account/messages/archive.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: archive.go
|
||||||
|
|
||||||
|
package accountMessageHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /account/messages/archived
|
||||||
|
// Renders: web/templates/account/messages/archived.html
|
||||||
|
func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
|
||||||
|
userID := mustUserID(c)
|
||||||
|
msgs, err := h.Svc.ListArchived(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load archived messages"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "account/messages/archived.html", gin.H{
|
||||||
|
"title": "Archived Messages",
|
||||||
|
"messages": msgs,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,98 +1,14 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: list.go
|
||||||
|
// ToDo: helpers for reading getting messages shouldn't really be here. ---
|
||||||
|
|
||||||
package accountMessageHandler
|
package accountMessageHandler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET /account/messages
|
|
||||||
// Renders: web/templates/account/messages/index.html
|
|
||||||
func (h *AccountMessageHandlers) List(c *gin.Context) {
|
|
||||||
userID := mustUserID(c) // replace with your auth/user extraction
|
|
||||||
msgs, err := h.Svc.ListInbox(userID)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load messages"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusOK, "account/messages/index.html", gin.H{
|
|
||||||
"title": "Messages",
|
|
||||||
"messages": msgs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /account/messages/add
|
|
||||||
// Renders: web/templates/account/messages/send.html
|
|
||||||
func (h *AccountMessageHandlers) AddGet(c *gin.Context) {
|
|
||||||
c.HTML(http.StatusOK, "account/messages/send.html", gin.H{
|
|
||||||
"title": "Send Message",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /account/messages/add
|
|
||||||
func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
|
|
||||||
userID := mustUserID(c)
|
|
||||||
var in CreateMessageInput
|
|
||||||
if err := c.ShouldBind(&in); err != nil {
|
|
||||||
// Re-render form with validation errors
|
|
||||||
c.HTML(http.StatusBadRequest, "account/messages/send.html", gin.H{
|
|
||||||
"title": "Send Message",
|
|
||||||
"error": "Please correct the errors below.",
|
|
||||||
"form": in,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err := h.Svc.Create(userID, in); err != nil {
|
|
||||||
c.HTML(http.StatusInternalServerError, "account/messages/send.html", gin.H{
|
|
||||||
"title": "Send Message",
|
|
||||||
"error": "Could not send message.",
|
|
||||||
"form": in,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Redirect back to inbox
|
|
||||||
c.Redirect(http.StatusSeeOther, "/account/messages")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Optional extras since you have read.html and archived.html ---
|
|
||||||
|
|
||||||
// GET /account/messages/:id
|
|
||||||
// Renders: web/templates/account/messages/read.html
|
|
||||||
func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
|
|
||||||
userID := mustUserID(c)
|
|
||||||
id, err := parseIDParam(c, "id")
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatus(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
msg, err := h.Svc.GetByID(userID, id)
|
|
||||||
if err != nil || msg == nil {
|
|
||||||
c.AbortWithStatus(http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusOK, "account/messages/read.html", gin.H{
|
|
||||||
"title": msg.Subject,
|
|
||||||
"message": msg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET /account/messages/archived
|
|
||||||
// Renders: web/templates/account/messages/archived.html
|
|
||||||
func (h *AccountMessageHandlers) ArchivedList(c *gin.Context) {
|
|
||||||
userID := mustUserID(c)
|
|
||||||
msgs, err := h.Svc.ListArchived(userID)
|
|
||||||
if err != nil {
|
|
||||||
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load archived messages"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.HTML(http.StatusOK, "account/messages/archived.html", gin.H{
|
|
||||||
"title": "Archived Messages",
|
|
||||||
"messages": msgs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- helpers ---
|
|
||||||
|
|
||||||
func mustUserID(c *gin.Context) int64 {
|
func mustUserID(c *gin.Context) int64 {
|
||||||
// Pull from your auth middleware/session. Panic-unsafe alternative:
|
// Pull from your auth middleware/session. Panic-unsafe alternative:
|
||||||
if v, ok := c.Get("userID"); ok {
|
if v, ok := c.Get("userID"); ok {
|
||||||
|
|||||||
46
internal/handlers/account/messages/read.go
Normal file
46
internal/handlers/account/messages/read.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: read.go
|
||||||
|
|
||||||
|
package accountMessageHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /account/messages
|
||||||
|
// Renders: web/templates/account/messages/index.html
|
||||||
|
func (h *AccountMessageHandlers) List(c *gin.Context) {
|
||||||
|
userID := mustUserID(c)
|
||||||
|
msgs, err := h.Svc.ListInbox(userID)
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "failed to load messages"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "account/messages/index.html", gin.H{
|
||||||
|
"title": "Messages",
|
||||||
|
"messages": msgs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /account/messages/:id
|
||||||
|
// Renders: web/templates/account/messages/read.html
|
||||||
|
func (h *AccountMessageHandlers) ReadGet(c *gin.Context) {
|
||||||
|
userID := mustUserID(c)
|
||||||
|
id, err := parseIDParam(c, "id")
|
||||||
|
if err != nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg, err := h.Svc.GetByID(userID, id)
|
||||||
|
if err != nil || msg == nil {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.HTML(http.StatusOK, "account/messages/read.html", gin.H{
|
||||||
|
"title": msg.Subject,
|
||||||
|
"message": msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
44
internal/handlers/account/messages/send.go
Normal file
44
internal/handlers/account/messages/send.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: send.go
|
||||||
|
|
||||||
|
package accountMessageHandler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GET /account/messages/add
|
||||||
|
// Renders: web/templates/account/messages/send.html
|
||||||
|
func (h *AccountMessageHandlers) AddGet(c *gin.Context) {
|
||||||
|
c.HTML(http.StatusOK, "account/messages/send.html", gin.H{
|
||||||
|
"title": "Send Message",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /account/messages/add
|
||||||
|
func (h *AccountMessageHandlers) AddPost(c *gin.Context) {
|
||||||
|
userID := mustUserID(c)
|
||||||
|
var in CreateMessageInput
|
||||||
|
if err := c.ShouldBind(&in); err != nil {
|
||||||
|
// Re-render form with validation errors
|
||||||
|
c.HTML(http.StatusBadRequest, "account/messages/send.html", gin.H{
|
||||||
|
"title": "Send Message",
|
||||||
|
"error": "Please correct the errors below.",
|
||||||
|
"form": in,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := h.Svc.Create(userID, in); err != nil {
|
||||||
|
c.HTML(http.StatusInternalServerError, "account/messages/send.html", gin.H{
|
||||||
|
"title": "Send Message",
|
||||||
|
"error": "Could not send message.",
|
||||||
|
"form": in,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Redirect back to inbox
|
||||||
|
c.Redirect(http.StatusSeeOther, "/account/messages")
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
// Package accountMessageHandler
|
||||||
|
// Path: /internal/handlers/account/messages
|
||||||
|
// File: types.go
|
||||||
|
|
||||||
package accountMessageHandler
|
package accountMessageHandler
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ func TemplateContext(w http.ResponseWriter, r *http.Request, data models.Templat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToDo the funcs need breaking up getting large
|
||||||
func TemplateFuncs() template.FuncMap {
|
func TemplateFuncs() template.FuncMap {
|
||||||
return template.FuncMap{
|
return template.FuncMap{
|
||||||
"plus1": func(i int) int { return i + 1 },
|
"plus1": func(i int) int { return i + 1 },
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
accountHandlers "synlotto-website/internal/handlers/account"
|
accountHandler "synlotto-website/internal/handlers/account"
|
||||||
accountMessageHandlers "synlotto-website/internal/handlers/account/messages"
|
accoutMessageHandler "synlotto-website/internal/handlers/account/messages"
|
||||||
accountNotificationHandlers "synlotto-website/internal/handlers/account/notifications"
|
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||||
accountTicketHandlers "synlotto-website/internal/handlers/account/tickets"
|
accountTicketHandler "synlotto-website/internal/handlers/account/tickets"
|
||||||
|
|
||||||
"synlotto-website/internal/http/middleware"
|
"synlotto-website/internal/http/middleware"
|
||||||
"synlotto-website/internal/platform/bootstrap"
|
"synlotto-website/internal/platform/bootstrap"
|
||||||
@@ -37,45 +37,45 @@ func RegisterAccountRoutes(app *bootstrap.App) {
|
|||||||
acc := r.Group("/account")
|
acc := r.Group("/account")
|
||||||
acc.Use(middleware.PublicOnly())
|
acc.Use(middleware.PublicOnly())
|
||||||
{
|
{
|
||||||
acc.GET("/login", accountHandlers.LoginGet)
|
acc.GET("/login", accountHandler.LoginGet)
|
||||||
acc.POST("/login", accountHandlers.LoginPost)
|
acc.POST("/login", accountHandler.LoginPost)
|
||||||
acc.GET("/signup", accountHandlers.SignupGet)
|
acc.GET("/signup", accountHandler.SignupGet)
|
||||||
acc.POST("/signup", accountHandlers.SignupPost)
|
acc.POST("/signup", accountHandler.SignupPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth-required account actions
|
// Auth-required account actions
|
||||||
accAuth := r.Group("/account")
|
accAuth := r.Group("/account")
|
||||||
accAuth.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
accAuth.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
accAuth.POST("/logout", accountHandlers.Logout)
|
accAuth.POST("/logout", accountHandler.Logout)
|
||||||
accAuth.GET("/logout", accountHandlers.Logout) // optional
|
accAuth.GET("/logout", accountHandler.Logout) // optional
|
||||||
}
|
}
|
||||||
|
|
||||||
// Messages (auth-required)
|
// Messages (auth-required)
|
||||||
messages := r.Group("/account/messages")
|
messages := r.Group("/account/messages")
|
||||||
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
messages.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
messages.GET("/", accountMessageHandlers.List)
|
messages.GET("/", accoutMessageHandler.List)
|
||||||
messages.GET("/add", accountMessageHandlers.AddGet)
|
messages.GET("/add", accoutMessageHandler.AddGet)
|
||||||
messages.POST("/add", accountMessageHandlers.AddPost)
|
messages.POST("/add", accoutMessageHandler.AddPost)
|
||||||
messages.GET("/archived", accountMessageHandlers.ArchivedList) // renders archived.html
|
messages.GET("/archived", accoutMessageHandler.ArchivedList) // renders archived.html
|
||||||
messages.GET("/:id", accountMessageHandlers.ReadGet) // renders read.html
|
messages.GET("/:id", accoutMessageHandler.ReadGet) // renders read.html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications (auth-required)
|
// Notifications (auth-required)
|
||||||
notifications := r.Group("/account/notifications")
|
notifications := r.Group("/account/notifications")
|
||||||
notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
notifications.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
notifications.GET("/", accountNotificationHandlers.List)
|
notifications.GET("/", accountNotificationHandler.List)
|
||||||
notifications.GET("/:id", accountNotificationHandlers.ReadGet) // renders read.html
|
notifications.GET("/:id", accountNotificationHandler.ReadGet) // renders read.html
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tickets (auth-required)
|
// Tickets (auth-required)
|
||||||
tickets := r.Group("/account/tickets")
|
tickets := r.Group("/account/tickets")
|
||||||
tickets.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
tickets.Use(middleware.AuthMiddleware(), middleware.RequireAuth())
|
||||||
{
|
{
|
||||||
tickets.GET("/", accountTicketHandlers.List) // GET /account/tickets
|
tickets.GET("/", accountTicketHandler.List) // GET /account/tickets
|
||||||
tickets.GET("/add", accountTicketHandlers.AddGet) // GET /account/tickets/add
|
tickets.GET("/add", accountTicketHandler.AddGet) // GET /account/tickets/add
|
||||||
tickets.POST("/add", accountTicketHandlers.AddPost) // POST /account/tickets/add
|
tickets.POST("/add", accountTicketHandler.AddPost) // POST /account/tickets/add
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
177
internal/platform/services/messages/service.go
Normal file
177
internal/platform/services/messages/service.go
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
// ToDo: not currently used and need to carve out sql
|
||||||
|
package messagesvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
accountMessageHandler "synlotto-website/internal/handlers/account/messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Service implements accountMessageHandler.MessageService.
|
||||||
|
type Service struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Dialect string // "postgres", "mysql", "sqlite" (affects INSERT id retrieval)
|
||||||
|
Now func() time.Time
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||||
|
s := &Service{
|
||||||
|
DB: db,
|
||||||
|
Dialect: "mysql", // sane default for LastInsertId (works for mysql/sqlite)
|
||||||
|
Now: time.Now,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDialect sets SQL dialect hints: "postgres" uses RETURNING id.
|
||||||
|
func WithDialect(d string) func(*Service) { return func(s *Service) { s.Dialect = d } }
|
||||||
|
|
||||||
|
// WithTimeout overrides per-call context timeout.
|
||||||
|
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||||
|
|
||||||
|
func (s *Service) ListInbox(userID int64) ([]accountMessageHandler.Message, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND is_archived = FALSE
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountMessageHandler.Message
|
||||||
|
for rows.Next() {
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ListArchived(userID int64) ([]accountMessageHandler.Message, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND is_archived = TRUE
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
rows, err := s.DB.QueryContext(s.rebind(ctx), s.bind(q), userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountMessageHandler.Message
|
||||||
|
for rows.Next() {
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
if err := rows.Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(userID, id int64) (*accountMessageHandler.Message, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, from_email, to_email, subject, body, is_read, is_archived, created_at
|
||||||
|
FROM messages
|
||||||
|
WHERE user_id = ? AND id = ?`
|
||||||
|
var m accountMessageHandler.Message
|
||||||
|
err := s.DB.QueryRowContext(s.rebind(ctx), s.bind(q), userID, id).
|
||||||
|
Scan(&m.ID, &m.From, &m.To, &m.Subject, &m.Body, &m.IsRead, &m.IsArchived, &m.CreatedAt)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Create(userID int64, in accountMessageHandler.CreateMessageInput) (int64, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
switch s.Dialect {
|
||||||
|
case "postgres":
|
||||||
|
const q = `
|
||||||
|
INSERT INTO messages (user_id, from_email, to_email, subject, body, is_read, is_archived, created_at)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, FALSE, FALSE, NOW())
|
||||||
|
RETURNING id`
|
||||||
|
var id int64
|
||||||
|
if err := s.DB.QueryRowContext(ctx, q, userID, "", in.To, in.Subject, in.Body).Scan(&id); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return id, nil
|
||||||
|
default: // mysql/sqlite
|
||||||
|
const q = `
|
||||||
|
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(s.rebind(ctx), q, userID, "", in.To, in.Subject, in.Body)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return res.LastInsertId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- small helpers ---
|
||||||
|
|
||||||
|
// bind replaces ? with $1.. for Postgres if needed.
|
||||||
|
// We keep queries written with ? for brevity and adapt here.
|
||||||
|
func (s *Service) bind(q string) string {
|
||||||
|
if s.Dialect != "postgres" {
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
// cheap replacer for positional params:
|
||||||
|
n := 0
|
||||||
|
out := make([]byte, 0, len(q)+10)
|
||||||
|
for i := 0; i < len(q); i++ {
|
||||||
|
if q[i] == '?' {
|
||||||
|
n++
|
||||||
|
out = append(out, '$')
|
||||||
|
out = append(out, []byte(intToStr(n))...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, q[i])
|
||||||
|
}
|
||||||
|
return string(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) rebind(ctx context.Context) context.Context { return ctx }
|
||||||
|
|
||||||
|
// intToStr avoids fmt for tiny helper
|
||||||
|
func intToStr(n int) string {
|
||||||
|
if n == 0 {
|
||||||
|
return "0"
|
||||||
|
}
|
||||||
|
var b [12]byte
|
||||||
|
i := len(b)
|
||||||
|
for n > 0 {
|
||||||
|
i--
|
||||||
|
b[i] = byte('0' + n%10)
|
||||||
|
n /= 10
|
||||||
|
}
|
||||||
|
return string(b[i:])
|
||||||
|
}
|
||||||
80
internal/platform/services/notifications/service.go
Normal file
80
internal/platform/services/notifications/service.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// ToDo: carve out sql
|
||||||
|
package notifysvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
accountNotificationHandler "synlotto-website/internal/handlers/account/notifications"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
DB *sql.DB
|
||||||
|
Now func() time.Time
|
||||||
|
Timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(db *sql.DB, opts ...func(*Service)) *Service {
|
||||||
|
s := &Service{
|
||||||
|
DB: db,
|
||||||
|
Now: time.Now,
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(s)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithTimeout(d time.Duration) func(*Service) { return func(s *Service) { s.Timeout = d } }
|
||||||
|
|
||||||
|
// List returns newest-first notifications for a user.
|
||||||
|
func (s *Service) List(userID int64) ([]accountNotificationHandler.Notification, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, title, body, is_read, created_at
|
||||||
|
FROM notifications
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
|
||||||
|
rows, err := s.DB.QueryContext(ctx, q, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var out []accountNotificationHandler.Notification
|
||||||
|
for rows.Next() {
|
||||||
|
var n accountNotificationHandler.Notification
|
||||||
|
if err := rows.Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetByID(userID, id int64) (*accountNotificationHandler.Notification, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), s.Timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
const q = `
|
||||||
|
SELECT id, title, body, is_read, created_at
|
||||||
|
FROM notifications
|
||||||
|
WHERE user_id = ? AND id = ?`
|
||||||
|
|
||||||
|
var n accountNotificationHandler.Notification
|
||||||
|
err := s.DB.QueryRowContext(ctx, q, userID, id).
|
||||||
|
Scan(&n.ID, &n.Title, &n.Body, &n.IsRead, &n.CreatedAt)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
|
// ToDo: these aren't really "services"
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
| <a href="/legal/privacy">Privacy Policy</a> |
|
| <a href="/legal/privacy">Privacy Policy</a> |
|
||||||
<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>
|
||||||
|
The content and operations of this website have not been approved or endorsed by {{ $lotteryOperator }} or the {{ $commisionName }}.
|
||||||
</small>
|
</small>
|
||||||
</footer>
|
</footer>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -1,18 +1,39 @@
|
|||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<div class="wrap">
|
<div class="wrap">
|
||||||
<h1>Thunderball Statistics</h1>
|
<h1>Thunderball Statistics</h1>
|
||||||
|
<p>
|
||||||
|
Discover everything you need to supercharge your Thunderball picks! Explore which numbers have been drawn the most, which ones are the rarest,
|
||||||
|
and even which lucky pairs and triplets keep showing up again and again. You can also check out which numbers are long overdue for a win perfect
|
||||||
|
for anyone playing the waiting game!
|
||||||
|
</p>
|
||||||
|
|
||||||
<div class="grid">
|
<p>
|
||||||
<div class="card">
|
All statistics are based on every draw from <i><b>9th May 2010</b></i> right through to (since {{.LastDraw}}), covering the current ball pool of 1–39.
|
||||||
<h3>Top 5 (since {{.Since}})</h3>
|
</p>
|
||||||
<table>
|
|
||||||
<thead><tr><th>Number</th><th>Frequency</th></tr></thead>
|
<p>
|
||||||
<tbody>
|
Feeling curious about the earlier era of Thunderball draws? Dive into the historical Thunderball statistics to uncover data from before 9th May 2010, back when the ball pool ranged from 1–34.
|
||||||
{{range .TopSince}}
|
</p>
|
||||||
<tr><td>{{.Number}}</td><td>{{.Frequency}}</td></tr>
|
<div class="grid">
|
||||||
{{end}}
|
<div class="card">
|
||||||
</tbody>
|
<h3>Top 5 (since {{.Since}})</h3>
|
||||||
</table>
|
<table>
|
||||||
</div>
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Number</th>
|
||||||
|
<th>Frequency</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range .TopSince}}
|
||||||
|
<tr>
|
||||||
|
<td>{{.Number}}</td>
|
||||||
|
<td>{{.Frequency}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
Reference in New Issue
Block a user