more changes but unsure of state had to trash last set fof changes and try repair.

This commit is contained in:
2025-04-01 15:50:15 +01:00
parent 5aaddf16f1
commit 0e20cc023c
13 changed files with 525 additions and 313 deletions

2
go.mod
View File

@@ -6,8 +6,8 @@ require (
github.com/gorilla/csrf v1.7.2 github.com/gorilla/csrf v1.7.2
github.com/gorilla/sessions v1.4.0 github.com/gorilla/sessions v1.4.0
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
modernc.org/sqlite v1.36.1
golang.org/x/time v0.11.0 golang.org/x/time v0.11.0
modernc.org/sqlite v1.36.1
) )
require ( require (

64
handlers/home.go Normal file
View File

@@ -0,0 +1,64 @@
package handlers
import (
"database/sql"
"html/template"
"log"
"net/http"
"sort"
"synlotto-website/helpers"
"synlotto-website/models"
)
// Home shows latest Thunderball results
func Home(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query(`
SELECT id, draw_date, machine, ballset, ball1, ball2, ball3, ball4, ball5, thunderball
FROM results_thunderball
ORDER BY id DESC
`)
if err != nil {
log.Println("❌ DB error:", err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
defer rows.Close()
var results []models.ThunderballResult
for rows.Next() {
var res models.ThunderballResult
err := rows.Scan(
&res.Id, &res.DrawDate, &res.Machine, &res.BallSet,
&res.Ball1, &res.Ball2, &res.Ball3, &res.Ball4, &res.Ball5, &res.Thunderball,
)
if err != nil {
log.Println("❌ Row scan error:", err)
continue
}
res.SortedBalls = []int{
res.Ball1, res.Ball2, res.Ball3, res.Ball4, res.Ball5,
}
sort.Ints(res.SortedBalls)
results = append(results, res)
}
context := BuildTemplateContext(db, w, r)
context["Data"] = results
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
"templates/layout.html",
"templates/topbar.html",
"templates/index.html",
))
err = tmpl.ExecuteTemplate(w, "layout", context)
if err != nil {
log.Println("❌ Template error:", err)
http.Error(w, "Error rendering homepage", http.StatusInternalServerError)
}
}
}

View File

@@ -0,0 +1,60 @@
package handlers
import (
"database/sql"
"net/http"
"synlotto-website/helpers"
"synlotto-website/models"
"synlotto-website/storage"
"github.com/gorilla/csrf"
)
type TemplateData map[string]interface{}
func BuildTemplateContext(db *sql.DB, w http.ResponseWriter, r *http.Request) TemplateData {
session, _ := helpers.GetSession(w, r)
var flash string
if f, ok := session.Values["flash"].(string); ok {
flash = f
delete(session.Values, "flash")
session.Save(r, w)
}
var currentUser *models.User
var isAdmin bool
notificationCount := 0
notifications := []models.Notification{}
messageCount := 0
messages := []models.Message{}
switch v := session.Values["user_id"].(type) {
case int:
currentUser = models.GetUserByID(v)
case int64:
currentUser = models.GetUserByID(int(v))
}
if currentUser != nil {
isAdmin = currentUser.IsAdmin
notificationCount = storage.GetNotificationCount(db, currentUser.Id)
notifications = storage.GetRecentNotifications(db, currentUser.Id, 15)
messageCount, _ = storage.GetMessageCount(db, currentUser.Id)
messages = storage.GetRecentMessages(db, currentUser.Id, 15)
}
return TemplateData{
"CSRFField": csrf.TemplateField(r),
"Flash": flash,
"User": currentUser,
"IsAdmin": isAdmin,
"NotificationCount": notificationCount,
"Notifications": notifications,
"MessageCount": messageCount,
"Messages": messages,
}
}

View File

@@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"synlotto-website/models" "synlotto-website/models"
"synlotto-website/storage"
"github.com/gorilla/csrf" "github.com/gorilla/csrf"
) )
@@ -50,6 +51,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
var currentUser *models.User var currentUser *models.User
var isAdmin bool var isAdmin bool
var notificationCount int
var notifications []models.Notification
switch v := session.Values["user_id"].(type) { switch v := session.Values["user_id"].(type) {
case int: case int:
@@ -60,6 +63,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
if currentUser != nil { if currentUser != nil {
isAdmin = currentUser.IsAdmin isAdmin = currentUser.IsAdmin
notificationCount = storage.GetNotificationCount(currentUser.Id)
notifications = storage.GetRecentNotifications(currentUser.Id, 15)
} }
return map[string]interface{}{ return map[string]interface{}{
@@ -67,6 +72,8 @@ func TemplateContext(w http.ResponseWriter, r *http.Request) map[string]interfac
"Flash": flash, "Flash": flash,
"User": currentUser, "User": currentUser,
"IsAdmin": isAdmin, "IsAdmin": isAdmin,
"NotificationCount": notificationCount,
"Notifications": notifications,
} }
} }
@@ -100,3 +107,9 @@ func rangeClass(n int) string {
return "50-plus" return "50-plus"
} }
} }
func SetFlash(w http.ResponseWriter, r *http.Request, message string) {
session, _ := GetSession(w, r)
session.Values["flash"] = message
session.Save(r, w)
}

View File

@@ -51,7 +51,9 @@ func setupAdminRoutes(mux *http.ServeMux, db *sql.DB) {
mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db))) mux.HandleFunc("/admin/triggers", middleware.AdminOnly(db, admin.AdminTriggersHandler(db)))
// Draw management // Draw management
mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.NewDrawHandler(db))) mux.HandleFunc("/admin/draws", middleware.AdminOnly(db, admin.ListDrawsHandler(db)))
mux.HandleFunc("/admin/draws/new", middleware.AdminOnly(db, admin.RenderNewDrawForm(db)))
mux.HandleFunc("/admin/draws/submit", middleware.AdminOnly(db, admin.CreateDrawHandler(db)))
mux.HandleFunc("/admin/draws/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db))) mux.HandleFunc("/admin/draws/modify", middleware.AdminOnly(db, admin.ModifyDrawHandler(db)))
mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db))) mux.HandleFunc("/admin/draws/delete", middleware.AdminOnly(db, admin.DeleteDrawHandler(db)))

View File

@@ -25,6 +25,7 @@ type Message struct {
ID int ID int
Sender string Sender string
Subject string Subject string
Message string
IsRead bool IsRead bool
CreatedAt time.Time CreatedAt time.Time
} }

View File

@@ -1,11 +0,0 @@
package storage
// "database/sql"
// // Get all for count
// var count int
// db.Get(&count, `SELECT COUNT(*) FROM user_notifications WHERE user_id = ? AND is_read = FALSE`, userID)
// // Then get the top 15 for display
// var notifications []Notification
// db.Select(&notifications, `SELECT * FROM user_notifications WHERE user_id = ? AND is_read = FALSE ORDER BY created_at DESC LIMIT 15`, userID)

View File

@@ -13,222 +13,24 @@ func InitDB(filepath string) *sql.DB {
log.Fatal("❌ Failed to open DB:", err) log.Fatal("❌ Failed to open DB:", err)
} }
createThunderballResultsTable := ` schemas := []string{
CREATE TABLE IF NOT EXISTS results_thunderball ( SchemaUsers,
id INTEGER PRIMARY KEY AUTOINCREMENT, SchemaThunderballResults,
draw_date TEXT NOT NULL UNIQUE, SchemaThunderballPrizes,
machine TEXT, SchemaLottoResults,
ballset TEXT, SchemaMyTickets,
ball1 INTEGER, SchemaUsersMessages,
ball2 INTEGER, SchemaUsersNotifications,
ball3 INTEGER, SchemaAuditLog,
ball4 INTEGER, SchemaLogTicketMatching,
ball5 INTEGER, SchemaAdminAccessLog,
thunderball INTEGER SchemaNewAuditLog,
);`
if _, err := db.Exec(createThunderballResultsTable); err != nil {
log.Fatal("❌ Failed to create Thunderball table:", err)
} }
createThunderballPrizeTable := ` for _, stmt := range schemas {
CREATE TABLE IF NOT EXISTS prizes_thunderball ( if _, err := db.Exec(stmt); err != nil {
id INTEGER PRIMARY KEY AUTOINCREMENT, log.Fatalf("❌ Failed to apply schema: %v", err)
draw_id INTEGER NOT NULL,
draw_date TEXT,
prize1 TEXT,
prize1_winners INTEGER,
prize1_per_winner INTEGER,
prize1_fund INTEGER,
prize2 TEXT,
prize2_winners INTEGER,
prize2_per_winner INTEGER,
prize2_fund INTEGER,
prize3 TEXT,
prize3_winners INTEGER,
prize3_per_winner INTEGER,
prize3_fund INTEGER,
prize4 TEXT,
prize4_winners INTEGER,
prize4_per_winner INTEGER,
prize4_fund INTEGER,
prize5 TEXT,
prize5_winners INTEGER,
prize5_per_winner INTEGER,
prize5_fund INTEGER,
prize6 TEXT,
prize6_winners INTEGER,
prize6_per_winner INTEGER,
prize6_fund INTEGER,
prize7 TEXT,
prize7_winners INTEGER,
prize7_per_winner INTEGER,
prize7_fund INTEGER,
prize8 TEXT,
prize8_winners INTEGER,
prize8_per_winner INTEGER,
prize8_fund INTEGER,
prize9 TEXT,
prize9_winners INTEGER,
prize9_per_winner INTEGER,
prize9_fund INTEGER,
total_winners INTEGER,
total_prize_fund INTEGER,
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
);`
_, err = db.Exec(createThunderballPrizeTable)
if err != nil {
log.Fatal("❌ Failed to create Thunderball prize table:", err)
} }
createLottoResultsTable := `
CREATE TABLE IF NOT EXISTS results_lotto (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE,
machine TEXT,
ballset TEXT,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonusball INTEGER
);`
if _, err := db.Exec(createLottoResultsTable); err != nil {
log.Fatal("❌ Failed to create Thunderball table:", err)
}
createMyTicketsTable := `
CREATE TABLE IF NOT EXISTS my_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
game_type TEXT NOT NULL,
draw_date TEXT NOT NULL,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonus1 INTEGER,
bonus2 INTEGER,
duplicate BOOLEAN DEFAULT 0,
purchase_date TEXT,
purchase_method TEXT,
image_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
matched_main INTEGER,
matched_bonus INTEGER,
prize_tier TEXT,
is_winner BOOLEAN,
prize_amount INTEGER,
prize_label TEXT,
FOREIGN KEY (userId) REFERENCES users(id)
);`
if _, err := db.Exec(createMyTicketsTable); err != nil {
log.Fatal("❌ Failed to create MyTickets table:", err)
}
createUsersTable := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_admin BOOLEAN
);`
if _, err := db.Exec(createUsersTable); err != nil {
log.Fatal("❌ Failed to create Users table:", err)
}
createUsersMessageTable := `
CREATE TABLE IF NOT EXISTS users_messages (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
message TEXT,
is_read BOOLEAN DEFAULT FALSE,
type VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
if _, err := db.Exec(createUsersMessageTable); err != nil {
log.Fatal("❌ Failed to create Users messages table:", err)
}
createUsersNotificationTable := `
CREATE TABLE IF NOT EXISTS users_notification (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
sender_name VARCHAR(100),
subject TEXT,
body TEXT,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
if _, err := db.Exec(createUsersNotificationTable); err != nil {
log.Fatal("❌ Failed to create Users notification table:", err)
}
createAuditLogTable := `
CREATE TABLE IF NOT EXISTS auditlog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
success INTEGER,
timestamp TEXT
);`
if _, err := db.Exec(createAuditLogTable); err != nil {
log.Fatal("❌ Failed to create Users table:", err)
}
createLogTicketMatchingTable := `
CREATE TABLE IF NOT EXISTS log_ticket_matching (
id INTEGER PRIMARY KEY AUTOINCREMENT,
triggered_by TEXT,
run_at DATETIME DEFAULT CURRENT_TIMESTAMP,
tickets_matched INTEGER,
winners_found INTEGER,
notes TEXT
);`
if _, err := db.Exec(createLogTicketMatchingTable); err != nil {
log.Fatal("❌ Failed to create ticket matching log table:", err)
}
createAdminAccessLogTable := `
CREATE TABLE IF NOT EXISTS admin_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
path TEXT,
ip TEXT,
user_agent TEXT
);`
if _, err := db.Exec(createAdminAccessLogTable); err != nil {
log.Fatal("❌ Failed to create admin access log table:", err)
}
createNewAuditLogTable := `
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username TEXT,
action TEXT,
path TEXT,
ip TEXT,
user_agent TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
if _, err := db.Exec(createNewAuditLogTable); err != nil {
log.Fatal("❌ Failed to create admin access log table:", err)
} }
return db return db

38
storage/messages.go Normal file
View File

@@ -0,0 +1,38 @@
package storage
import (
"database/sql"
"synlotto-website/models"
)
func GetMessageCount(db *sql.DB, userID int) (int, error) {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM users_messages
WHERE user_id = ? AND is_read = FALSE
`, userID).Scan(&count)
return count, err
}
func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
rows, err := db.Query(`
SELECT id, title, message, is_read
FROM users_messages
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?
`, userID, limit)
if err != nil {
return nil
}
defer rows.Close()
var messages []models.Message
for rows.Next() {
var m models.Message
rows.Scan(&m.ID, &m.Subject, &m.Message, &m.IsRead)
messages = append(messages, m)
}
return messages
}

47
storage/notifications.go Normal file
View File

@@ -0,0 +1,47 @@
package storage
import (
"database/sql"
"log"
"synlotto-website/models"
)
func GetNotificationCount(db *sql.DB, userID int) int {
var count int
err := db.QueryRow(`
SELECT COUNT(*) FROM users_notification
WHERE user_id = ? AND is_read = FALSE`, userID).Scan(&count)
if err != nil {
log.Println("⚠️ Failed to count notifications:", err)
return 0
}
return count
}
func GetRecentNotifications(db *sql.DB, userID int, limit int) []models.Notification {
rows, err := db.Query(`
SELECT id, subject, body, is_read, created_at
FROM users_notification
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT ?`, userID, limit)
if err != nil {
log.Println("⚠️ Failed to get notifications:", err)
return nil
}
defer rows.Close()
var notifications []models.Notification
for rows.Next() {
var n models.Notification
if err := rows.Scan(&n.ID, &n.Title, &n.Message, &n.IsRead, &n.CreatedAt); err == nil {
notifications = append(notifications, n)
}
}
return notifications
}

174
storage/schema.go Normal file
View File

@@ -0,0 +1,174 @@
package storage
const SchemaUsers = `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
is_admin BOOLEAN
);`
const SchemaThunderballResults = `
CREATE TABLE IF NOT EXISTS results_thunderball (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE,
machine TEXT,
ballset TEXT,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
thunderball INTEGER
);`
const SchemaThunderballPrizes = `
CREATE TABLE IF NOT EXISTS prizes_thunderball (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_id INTEGER NOT NULL,
draw_date TEXT,
prize1 TEXT,
prize1_winners INTEGER,
prize1_per_winner INTEGER,
prize1_fund INTEGER,
prize2 TEXT,
prize2_winners INTEGER,
prize2_per_winner INTEGER,
prize2_fund INTEGER,
prize3 TEXT,
prize3_winners INTEGER,
prize3_per_winner INTEGER,
prize3_fund INTEGER,
prize4 TEXT,
prize4_winners INTEGER,
prize4_per_winner INTEGER,
prize4_fund INTEGER,
prize5 TEXT,
prize5_winners INTEGER,
prize5_per_winner INTEGER,
prize5_fund INTEGER,
prize6 TEXT,
prize6_winners INTEGER,
prize6_per_winner INTEGER,
prize6_fund INTEGER,
prize7 TEXT,
prize7_winners INTEGER,
prize7_per_winner INTEGER,
prize7_fund INTEGER,
prize8 TEXT,
prize8_winners INTEGER,
prize8_per_winner INTEGER,
prize8_fund INTEGER,
prize9 TEXT,
prize9_winners INTEGER,
prize9_per_winner INTEGER,
prize9_fund INTEGER,
total_winners INTEGER,
total_prize_fund INTEGER,
FOREIGN KEY (draw_date) REFERENCES results_thunderball(draw_date)
);`
const SchemaLottoResults = `
CREATE TABLE IF NOT EXISTS results_lotto (
id INTEGER PRIMARY KEY AUTOINCREMENT,
draw_date TEXT NOT NULL UNIQUE,
machine TEXT,
ballset TEXT,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonusball INTEGER
);`
const SchemaMyTickets = `
CREATE TABLE IF NOT EXISTS my_tickets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL,
game_type TEXT NOT NULL,
draw_date TEXT NOT NULL,
ball1 INTEGER,
ball2 INTEGER,
ball3 INTEGER,
ball4 INTEGER,
ball5 INTEGER,
ball6 INTEGER,
bonus1 INTEGER,
bonus2 INTEGER,
duplicate BOOLEAN DEFAULT 0,
purchase_date TEXT,
purchase_method TEXT,
image_path TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
matched_main INTEGER,
matched_bonus INTEGER,
prize_tier TEXT,
is_winner BOOLEAN,
prize_amount INTEGER,
prize_label TEXT,
FOREIGN KEY (userId) REFERENCES users(id)
);`
const SchemaUsersMessages = `
CREATE TABLE IF NOT EXISTS users_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
title TEXT NOT NULL,
message TEXT,
is_read BOOLEAN DEFAULT FALSE,
type VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
const SchemaUsersNotifications = `
CREATE TABLE IF NOT EXISTS users_notification (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id),
sender_name VARCHAR(100),
subject TEXT,
body TEXT,
is_read BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
const SchemaAuditLog = `
CREATE TABLE IF NOT EXISTS auditlog (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
success INTEGER,
timestamp TEXT
);`
const SchemaLogTicketMatching = `
CREATE TABLE IF NOT EXISTS log_ticket_matching (
id INTEGER PRIMARY KEY AUTOINCREMENT,
triggered_by TEXT,
run_at DATETIME DEFAULT CURRENT_TIMESTAMP,
tickets_matched INTEGER,
winners_found INTEGER,
notes TEXT
);`
const SchemaAdminAccessLog = `
CREATE TABLE IF NOT EXISTS admin_access_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
accessed_at DATETIME DEFAULT CURRENT_TIMESTAMP,
path TEXT,
ip TEXT,
user_agent TEXT
);`
const SchemaNewAuditLog = `
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
username TEXT,
action TEXT,
path TEXT,
ip TEXT,
user_agent TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`

View File

@@ -1,6 +1,6 @@
{{ define "new_draw" }} {{ define "new_draw" }}
<h2 class="text-xl font-semibold mb-4">Add New Draw</h2> <h2 class="text-xl font-semibold mb-4">Add New Draw</h2>
<form method="POST" action="/admin/draws/new"> <form method="POST" action="/admin/draws/submit">
{{ .CSRFField }} {{ .CSRFField }}
<label class="block">Game Type: <input name="game_type" class="input"></label> <label class="block">Game Type: <input name="game_type" class="input"></label>
<label class="block">Draw Date: <input name="draw_date" type="date" class="input"></label> <label class="block">Draw Date: <input name="draw_date" type="date" class="input"></label>

View File

@@ -9,33 +9,44 @@
{{ if .IsAdmin }} {{ if .IsAdmin }}
<!-- Admin Dropdown --> <!-- Admin Dropdown -->
<div class="dropdown"> <div class="dropdown">
<a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link text-dark" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-shield-lock fs-5 position-relative"></i> <i class="bi bi-shield-lock fs-5 position-relative"></i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow" aria-labelledby="adminDropdown"> <ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm dropdown-with-arrow"
aria-labelledby="adminDropdown">
<li class="dropdown-header text-center fw-bold">Admin Menu</li> <li class="dropdown-header text-center fw-bold">Admin Menu</li>
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Tools</a></li> <li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Tools</a></li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li> <li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Audit Logs</a></li>
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li> <li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
</ul> </ul>
</div> </div>
{{ end }} {{ end }}
<!-- Notification Dropdown <!-- Notification Dropdown -->
<div class="dropdown"> <div class="dropdown">
<a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link text-dark" href="#" id="notificationDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-bell fs-5 position-relative"> <i class="bi bi-bell fs-5 position-relative">
{{ if gt .NotificationCount 0 }} {{ if gt (intVal .NotificationCount) 0 }}
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small"> <span
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }} class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
{{ if gt (intVal .NotificationCount) 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
</span> </span>
{{ end }} {{ end }}
</i> </i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow" aria-labelledby="notificationDropdown"> <ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm dropdown-with-arrow"
aria-labelledby="notificationDropdown">
<li class="dropdown-header text-center fw-bold">Notifications</li> <li class="dropdown-header text-center fw-bold">Notifications</li>
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
{{ $total := len .Notifications }} {{ $total := len .Notifications }}
{{ range $i, $n := .Notifications }} {{ range $i, $n := .Notifications }}
@@ -49,28 +60,37 @@
</div> </div>
</li> </li>
{{ if lt (add $i 1) $total }} {{ if lt (add $i 1) $total }}
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
{{ end }} {{ end }}
{{ else }} {{ else }}
<li class="text-center text-muted py-2">No notifications</li> <li class="text-center text-muted py-2">No notifications</li>
{{ end }} {{ end }}
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li> <li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
</ul> </ul>
</div> --> </div>
<!-- Message Dropdown --> <!-- Message Dropdown -->
<div class="dropdown"> <div class="dropdown">
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="bi bi-envelope fs-5 position-relative"> <i class="bi bi-envelope fs-5 position-relative">
<!-- Unread badge (example: 2 messages) --> <!-- Unread badge (example: 2 messages) -->
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span> <span
class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
</i> </i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow" aria-labelledby="messageDropdown"> <ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow"
aria-labelledby="messageDropdown">
<li class="dropdown-header text-center fw-bold">Messages</li> <li class="dropdown-header text-center fw-bold">Messages</li>
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
<!-- Example message item --> <!-- Example message item -->
<li class="px-3 py-2"> <li class="px-3 py-2">
@@ -83,7 +103,9 @@
</div> </div>
</li> </li>
<li><hr class="dropdown-divider"></li> <li>
<hr class="dropdown-divider">
</li>
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li> <li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
</ul> </ul>
</div> </div>