More layout and customisations.
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ToDo this should not be in draw for home!
|
||||||
func Home(db *sql.DB) http.HandlerFunc {
|
func Home(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
rows, err := db.Query(`
|
rows, err := db.Query(`
|
||||||
@@ -50,8 +51,9 @@ func Home(db *sql.DB) http.HandlerFunc {
|
|||||||
context := helpers.TemplateContext(w, r)
|
context := helpers.TemplateContext(w, r)
|
||||||
context["Data"] = results
|
context["Data"] = results
|
||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles(
|
tmpl := template.Must(template.New("").Funcs(helpers.TemplateFuncs()).ParseFiles(
|
||||||
"templates/layout.html",
|
"templates/layout.html",
|
||||||
|
"templates/topbar.html",
|
||||||
"templates/index.html",
|
"templates/index.html",
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -69,6 +71,7 @@ func NewDraw(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
tmpl := template.Must(template.ParseFiles(
|
tmpl := template.Must(template.ParseFiles(
|
||||||
"templates/layout.html",
|
"templates/layout.html",
|
||||||
|
"templates/topbar.html",
|
||||||
"templates/new_draw.html",
|
"templates/new_draw.html",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,22 @@ type User struct {
|
|||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Notification struct {
|
||||||
|
ID int
|
||||||
|
Title string
|
||||||
|
Message string
|
||||||
|
IsRead bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
ID int
|
||||||
|
Sender string
|
||||||
|
Subject string
|
||||||
|
IsRead bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
var db *sql.DB
|
var db *sql.DB
|
||||||
|
|
||||||
func SetDB(database *sql.DB) {
|
func SetDB(database *sql.DB) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "topbar.css";
|
||||||
|
|
||||||
body { font-family: Arial, sans-serif; margin: 0px; }
|
body { font-family: Arial, sans-serif; margin: 0px; }
|
||||||
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
|
||||||
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
|
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
|
||||||
@@ -15,23 +17,6 @@ th { background-color: #f5f5f5; }
|
|||||||
}
|
}
|
||||||
.flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; }
|
.flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; }
|
||||||
|
|
||||||
.dropdown-admin-box {
|
|
||||||
min-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-notification-box {
|
|
||||||
min-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdown-message-box {
|
|
||||||
min-width: 350px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-xs {
|
|
||||||
padding: 2px 6px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ball Stuff */
|
/* Ball Stuff */
|
||||||
.ball {
|
.ball {
|
||||||
|
|||||||
50
static/css/topbar.css
Normal file
50
static/css/topbar.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
.dropdown-admin-box {
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-notification-box {
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-message-box {
|
||||||
|
min-width: 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-with-arrow::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -8px;
|
||||||
|
right: 3.5px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 9px solid transparent;
|
||||||
|
border-right: 9px solid transparent;
|
||||||
|
border-bottom: 9px solid white;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-with-arrow::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: -9px;
|
||||||
|
right: 3.5px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 9px solid transparent;
|
||||||
|
border-right: 9px solid transparent;
|
||||||
|
border-bottom: 9px solid rgba(0, 0, 0, 0.15);
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-xs {
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-small {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 2px 5px;
|
||||||
|
line-height: 1;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
11
storage/badgecounts.go
Normal file
11
storage/badgecounts.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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(¬ifications, `SELECT * FROM user_notifications WHERE user_id = ? AND is_read = FALSE ORDER BY created_at DESC LIMIT 15`, userID)
|
||||||
@@ -135,16 +135,46 @@ func InitDB(filepath string) *sql.DB {
|
|||||||
|
|
||||||
createUsersTable := `
|
createUsersTable := `
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT NOT NULL UNIQUE,
|
username TEXT NOT NULL UNIQUE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
is_admin BOOLEAN
|
is_admin BOOLEAN
|
||||||
);`
|
);`
|
||||||
|
|
||||||
if _, err := db.Exec(createUsersTable); err != nil {
|
if _, err := db.Exec(createUsersTable); err != nil {
|
||||||
log.Fatal("❌ Failed to create Users table:", err)
|
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 := `
|
createAuditLogTable := `
|
||||||
CREATE TABLE IF NOT EXISTS auditlog (
|
CREATE TABLE IF NOT EXISTS auditlog (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
@@ -10,111 +10,8 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="d-flex flex-column min-vh-100">
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
|
||||||
<!-- Topbar -->
|
<!-- Topbar -->
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light px-3">
|
{{ template "topbar" . }}
|
||||||
<a class="navbar-brand d-flex align-items-center" href="/">
|
|
||||||
<img src="/static/img/logo.png" alt="Logo" height="30" class="me-2">
|
|
||||||
<span>SynLotto</span>
|
|
||||||
</a>
|
|
||||||
<div class="ms-auto d-flex align-items-center gap-3">
|
|
||||||
{{ if .User }}
|
|
||||||
{{ if .IsAdmin }}
|
|
||||||
<!-- Admin Dropdown -->
|
|
||||||
<div class="dropdown">
|
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-admin-box shadow-sm" aria-labelledby="adminDropdown">
|
|
||||||
<li class="dropdown-header text-center fw-bold">Admin Menu</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">Audit Logs</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
<!-- Notification Dropdown -->
|
|
||||||
<div class="dropdown">
|
|
||||||
<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">
|
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark" style="transform: translate(-40%, -50%)">
|
|
||||||
3
|
|
||||||
</span>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-notification-box shadow-sm" aria-labelledby="notificationDropdown">
|
|
||||||
<li class="dropdown-header text-center fw-bold">Notifications</li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
|
|
||||||
<!-- Example notification -->
|
|
||||||
<li class="px-3 py-2">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i class="bi bi-info-circle text-primary me-2 fs-4"></i>
|
|
||||||
<div>
|
|
||||||
<div class="fw-semibold">System Update</div>
|
|
||||||
<small class="text-muted">A new lottery draw has been posted.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
|
|
||||||
<li class="px-3 py-2">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i class="bi bi-check-circle text-success me-2 fs-4"></i>
|
|
||||||
<div>
|
|
||||||
<div class="fw-semibold">Sync Complete</div>
|
|
||||||
<small class="text-muted">All results are up-to-date.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Message Dropdown -->
|
|
||||||
<div class="dropdown">
|
|
||||||
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
<i class="bi bi-envelope fs-5 position-relative">
|
|
||||||
<!-- Unread badge (example: 2 messages) -->
|
|
||||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark" style="transform: translate(-40%, -50%)">
|
|
||||||
2
|
|
||||||
</span>
|
|
||||||
</i>
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm" aria-labelledby="messageDropdown" style="min-width: 300px;">
|
|
||||||
<li class="dropdown-header text-center fw-bold">Messages</li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
|
|
||||||
<!-- Example message item -->
|
|
||||||
<li class="px-3 py-2">
|
|
||||||
<div class="d-flex align-items-start">
|
|
||||||
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
|
||||||
<div>
|
|
||||||
<div class="fw-semibold">Admin</div>
|
|
||||||
<small class="text-muted">Welcome to SynLotto!</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- User Greeting -->
|
|
||||||
<span class="navbar-text">Hello, {{ .User.Username }}</span>
|
|
||||||
<a class="btn btn-outline-danger btn-xs" href="/logout">Logout</a>
|
|
||||||
{{ else }}
|
|
||||||
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Main Layout -->
|
<!-- Main Layout -->
|
||||||
<div class="container-fluid flex-grow-1">
|
<div class="container-fluid flex-grow-1">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
|||||||
100
templates/topbar.html
Normal file
100
templates/topbar.html
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
{{ define "topbar" }}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-light bg-light px-3">
|
||||||
|
<a class="navbar-brand d-flex align-items-center" href="/">
|
||||||
|
<img src="/static/img/logo.png" alt="Logo" height="30" class="me-2">
|
||||||
|
<span>SynLotto</span>
|
||||||
|
</a>
|
||||||
|
<div class="ms-auto d-flex align-items-center gap-3">
|
||||||
|
{{ if .User }}
|
||||||
|
{{ if .IsAdmin }}
|
||||||
|
<!-- Admin Dropdown -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
<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><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">Audit Logs</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li class="text-center"><a href="/admin/dashboard" class="dropdown-item">Open Dashboard</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<!-- Notification Dropdown
|
||||||
|
<div class="dropdown">
|
||||||
|
<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">
|
||||||
|
{{ if gt .NotificationCount 0 }}
|
||||||
|
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-warning text-dark badge-small">
|
||||||
|
{{ if gt .NotificationCount 15 }}15+{{ else }}{{ .NotificationCount }}{{ end }}
|
||||||
|
</span>
|
||||||
|
{{ end }}
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
<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><hr class="dropdown-divider"></li>
|
||||||
|
|
||||||
|
{{ $total := len .Notifications }}
|
||||||
|
{{ range $i, $n := .Notifications }}
|
||||||
|
<li class="px-3 py-2">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi bi-info-circle text-primary me-2 fs-4"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">{{ $n.Title }}</div>
|
||||||
|
<small class="text-muted">{{ $n.Message }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{ if lt (add $i 1) $total }}
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
<li class="text-center text-muted py-2">No notifications</li>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li class="text-center"><a href="/notifications" class="dropdown-item">View all notifications</a></li>
|
||||||
|
</ul>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Message Dropdown -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<a class="nav-link text-dark" href="#" id="messageDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="bi bi-envelope fs-5 position-relative">
|
||||||
|
<!-- Unread badge (example: 2 messages) -->
|
||||||
|
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">2</span>
|
||||||
|
</i>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end dropdown-message-box shadow-sm dropdown-with-arrow" aria-labelledby="messageDropdown">
|
||||||
|
<li class="dropdown-header text-center fw-bold">Messages</li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
|
||||||
|
<!-- Example message item -->
|
||||||
|
<li class="px-3 py-2">
|
||||||
|
<div class="d-flex align-items-start">
|
||||||
|
<i class="bi bi-person-circle me-2 fs-4 text-secondary"></i>
|
||||||
|
<div>
|
||||||
|
<div class="fw-semibold">Admin</div>
|
||||||
|
<small class="text-muted">Welcome to SynLotto!</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li class="text-center"><a href="/messages" class="dropdown-item">View all messages</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Greeting -->
|
||||||
|
<span class="navbar-text">Hello, {{ .User.Username }}</span>
|
||||||
|
<a class="btn btn-outline-danger btn-xs" href="/logout">Logout</a>
|
||||||
|
{{ else }}
|
||||||
|
<a class="btn btn-outline-primary btn-sm" href="/login">Login</a>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
Reference in New Issue
Block a user