Refactor and remove sqlite and replace with MySQL
This commit is contained in:
23
web/templates/account/login.html
Normal file
23
web/templates/account/login.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{{ define "content" }}
|
||||
<h2>Login</h2>
|
||||
<form method="POST" action="/account/login" class="form">
|
||||
{{ .CSRFField }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="username">Username:</label>
|
||||
<input type="text" name="username" id="username" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password">Password:</label>
|
||||
<input type="password" name="password" id="password" required class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" name="remember" id="remember" class="form-check-input">
|
||||
<label for="remember" class="form-check-label">Remember Me</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
44
web/templates/account/messages/archived.html
Normal file
44
web/templates/account/messages/archived.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Archived Messages</h2>
|
||||
|
||||
{{ if .Messages }}
|
||||
{{ range .Messages }}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ .Subject }}</h5>
|
||||
<p class="card-text">{{ .Message }}</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">Archived: {{ .ArchivedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/account/messages/restore" class="m-0">
|
||||
{{ $.CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success">Restore</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{ if gt .Page 1 }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ minus1 .Page }}">Previous</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ if .HasMore }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ plus1 .Page }}">Next</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
{{ else }}
|
||||
<div class="alert alert-info text-center">No archived messages.</div>
|
||||
{{ end }}
|
||||
|
||||
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
55
web/templates/account/messages/index.html
Normal file
55
web/templates/account/messages/index.html
Normal file
@@ -0,0 +1,55 @@
|
||||
{{ define "content" }}
|
||||
<!-- Todo lists messages but doesn't show which ones have been read and unread-->
|
||||
<div class="container py-5">
|
||||
<h2>Your Inbox</h2>
|
||||
{{ if .Messages }}
|
||||
<ul class="list-group mb-4">
|
||||
{{ range .Messages }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a>
|
||||
<br>
|
||||
<small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||
</div>
|
||||
<form method="POST" action="/account/messages/archive?id={{ .ID }}" class="m-0">
|
||||
{{ $.CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
||||
</form>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<!-- Pagination -->
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{ if gt .CurrentPage 1 }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ sub .CurrentPage 1 }}">Previous</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
{{ range $i := .PageRange }}
|
||||
<li class="page-item {{ if eq $i $.CurrentPage }}active{{ end }}">
|
||||
<a class="page-link" href="?page={{ $i }}">{{ $i }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
{{ if lt .CurrentPage .TotalPages }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ add .CurrentPage 1 }}">Next</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
{{ else }}
|
||||
<div class="alert alert-info">No messages found.</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="/account/messages/send" class="btn btn-primary">Compose Message</a>
|
||||
<a href="/account/messages/archived" class="btn btn-outline-secondary ms-2">View Archived</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
15
web/templates/account/messages/read.html
Normal file
15
web/templates/account/messages/read.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
{{ if .Message }}
|
||||
<h2>{{ .Message.Subject }}</h2>
|
||||
<p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p>
|
||||
<hr>
|
||||
<p>{{ .Message.Message }}</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>
|
||||
{{ else }}
|
||||
<div class="alert alert-danger text-center">
|
||||
Message not found or access denied.
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
27
web/templates/account/messages/send.html
Normal file
27
web/templates/account/messages/send.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Send a Message</h2>
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST" action="/account/messages/send">
|
||||
{{ .CSRFField }}
|
||||
<div class="mb-3">
|
||||
<label for="recipient_id" class="form-label">Recipient User ID</label>
|
||||
<input type="number" class="form-control" name="recipient_id" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">Subject</label>
|
||||
<input type="text" class="form-control" name="subject" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Message</label>
|
||||
<textarea class="form-control" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send</button>
|
||||
</form>
|
||||
|
||||
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
29
web/templates/account/notifications/index.html
Normal file
29
web/templates/account/notifications/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2 class="mb-4">Your Notifications</h2>
|
||||
|
||||
{{ if .Notifications }}
|
||||
<ul class="list-group">
|
||||
{{ range .Notifications }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-start {{ if not .IsRead }}bg-light{{ end }}">
|
||||
<div class="ms-2 me-auto">
|
||||
<div class="fw-bold">
|
||||
<a href="/account/notifications/read?id={{ .ID }}" class="{{ if not .IsRead }}text-primary fw-bold{{ end }}">
|
||||
{{ .Subject }}
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">{{ .CreatedAt.Format "Jan 2, 2006 15:04" }}</small>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<span class="badge bg-warning text-dark">New</span>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="alert alert-info text-center">
|
||||
You don’t have any notifications.
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
13
web/templates/account/notifications/read.html
Normal file
13
web/templates/account/notifications/read.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
{{ if .Notification }}
|
||||
<h2>{{ .Notification.Subject }}</h2>
|
||||
<p>{{ .Notification.Body }}</p>
|
||||
{{ else }}
|
||||
<div class="alert alert-danger text-center">
|
||||
Notification not found or access denied.
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="/account/notifications" class="btn btn-secondary mt-4">Back to Notifications</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
9
web/templates/account/signup.html
Normal file
9
web/templates/account/signup.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ define "content" }}
|
||||
<h2>Sign Up</h2>
|
||||
<form method="POST" action="/account/signup">
|
||||
{{ .csrfField }}
|
||||
<label>Username: <input type="text" name="username" required></label><br>
|
||||
<label>Password: <input type="password" name="password" required></label><br>
|
||||
<button type="submit">Sign Up</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
155
web/templates/account/tickets/add_ticket.html
Normal file
155
web/templates/account/tickets/add_ticket.html
Normal file
@@ -0,0 +1,155 @@
|
||||
{{ define "content" }}
|
||||
<a href="/">← Back</a>
|
||||
<h2>Log My Ticket</h2>
|
||||
|
||||
<form method="POST" action="/account/tickets/add_ticket" enctype="multipart/form-data" id="ticketForm">
|
||||
{{ .csrfField }}
|
||||
|
||||
<div class="form-section">
|
||||
<label>Game:
|
||||
<select name="game_type" id="gameType" required>
|
||||
<option value="">-- Select Game --</option>
|
||||
<option value="Thunderball" selected>Thunderball</option>
|
||||
<option value="Lotto">Lotto</option>
|
||||
<option value="EuroMillions">EuroMillions</option>
|
||||
<option value="SetForLife">Set For Life</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label>Draw Date:
|
||||
<select name="draw_date" required>
|
||||
{{ range .DrawDates }}
|
||||
<option value="{{ . }}">{{ . }}</option>
|
||||
{{ else }}
|
||||
<option disabled>No draws available</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label>Purchase Method:
|
||||
<select name="purchase_method" required>
|
||||
<option value="In-store">In-store</option>
|
||||
<option value="National-lottery.com">National-lottery.com</option>
|
||||
<option value="SynLotto">SynLotto</option>
|
||||
<option value="Other Online">Other Online</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="purchase_date">Purchase Date:</label>
|
||||
<input type="date" name="purchase_date" id="purchase_date" required>
|
||||
|
||||
<label for="purchase_time">(Optional) Time:</label>
|
||||
<input type="time" name="purchase_time" id="purchase_time">
|
||||
</div>
|
||||
|
||||
<div id="ticketLinesContainer">
|
||||
<!-- JS will insert ticket lines here -->
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label for="ticket_image">Upload Ticket Image (optional):</label>
|
||||
<input type="file" name="ticket_image" id="ticket_image" accept="image/*">
|
||||
</div>
|
||||
|
||||
<button type="button" onclick="addLine()">+ Add Line</button><br><br>
|
||||
<button type="submit">Save Ticket(s)</button>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
const gameConfig = {
|
||||
Thunderball: { balls: 5, bonus: 1, ballRange: [1, 39], bonusRange: [1, 14] },
|
||||
Lotto: { balls: 6, bonus: 0, ballRange: [1, 59], bonusRange: [] },
|
||||
EuroMillions: { balls: 5, bonus: 2, ballRange: [1, 50], bonusRange: [1, 12] },
|
||||
SetForLife: { balls: 5, bonus: 1, ballRange: [1, 47], bonusRange: [1, 10] }
|
||||
};
|
||||
|
||||
const container = document.getElementById("ticketLinesContainer");
|
||||
let lineCount = 0;
|
||||
|
||||
document.getElementById('gameType').addEventListener('change', () => {
|
||||
container.innerHTML = '';
|
||||
lineCount = 0;
|
||||
addLine();
|
||||
});
|
||||
|
||||
function addLine() {
|
||||
const game = document.getElementById("gameType").value;
|
||||
if (!game) {
|
||||
alert("Please select a game first.");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = gameConfig[game];
|
||||
const lineId = `line-${lineCount++}`;
|
||||
const div = document.createElement('div');
|
||||
div.className = "ticket-line";
|
||||
div.style.marginBottom = "10px";
|
||||
|
||||
div.innerHTML = `
|
||||
<strong>Line ${lineCount}</strong><br>
|
||||
${Array.from({ length: 6 }, (_, i) => `
|
||||
<input type="number" name="ball${i+1}[]" ${i < config.balls ? '' : 'disabled'} required min="${config.ballRange[0]}" max="${config.ballRange[1]}" placeholder="Ball ${i+1}">
|
||||
`).join('')}
|
||||
${Array.from({ length: 2 }, (_, i) => `
|
||||
<input type="number" name="bonus${i+1}[]" ${i < config.bonus ? '' : 'disabled'} ${i < config.bonus ? 'required' : ''} min="${config.bonusRange[0] || 0}" max="${config.bonusRange[1] || 0}" placeholder="Bonus ${i+1}">
|
||||
`).join('')}
|
||||
`;
|
||||
container.appendChild(div);
|
||||
console.log(`🆕 Line ${lineCount} added for ${game}`);
|
||||
}
|
||||
|
||||
const form = document.getElementById('ticketForm');
|
||||
form.addEventListener('submit', function (e) {
|
||||
const lines = document.querySelectorAll(".ticket-line");
|
||||
if (lines.length === 0) {
|
||||
e.preventDefault();
|
||||
alert("Please select a game and add at least one line.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const mainBalls = Array.from(line.querySelectorAll('input[name^="ball"]'))
|
||||
.filter(i => !i.disabled)
|
||||
.map(i => parseInt(i.value, 10));
|
||||
|
||||
const bonusBalls = Array.from(line.querySelectorAll('input[name^="bonus"]'))
|
||||
.filter(i => !i.disabled)
|
||||
.map(i => parseInt(i.value, 10));
|
||||
|
||||
const mainSet = new Set(mainBalls);
|
||||
const bonusSet = new Set(bonusBalls);
|
||||
|
||||
if (mainBalls.includes(NaN) || bonusBalls.includes(NaN)) {
|
||||
alert("All fields must be filled with valid numbers.");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainSet.size !== mainBalls.length) {
|
||||
alert("Duplicate main numbers detected.");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (bonusSet.size !== bonusBalls.length) {
|
||||
alert("Duplicate bonus numbers detected.");
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const gameSelect = document.getElementById('gameType');
|
||||
if (gameSelect.value) {
|
||||
addLine();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{ end }}
|
||||
92
web/templates/account/tickets/my_tickets.html
Normal file
92
web/templates/account/tickets/my_tickets.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{{ define "content" }}
|
||||
<a href="/account/tickets/add_ticket">+ Add Ticket</a>
|
||||
<h2>My Tickets</h2>
|
||||
|
||||
{{ if eq (len .Tickets) 0 }}
|
||||
<p>You haven’t logged any tickets yet.</p>
|
||||
{{ else }}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>Draw Date</th>
|
||||
<th>Numbers</th>
|
||||
<th>Bonus</th>
|
||||
<th>Purchased</th>
|
||||
<th>Via</th>
|
||||
<th>Image</th>
|
||||
<th>Prize</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Tickets }}
|
||||
{{ $ticket := . }}
|
||||
<tr>
|
||||
<td>{{ $ticket.GameType }}</td>
|
||||
<td>{{ $ticket.DrawDate }}</td>
|
||||
<td>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{{ range $i, $ball := $ticket.Balls }}
|
||||
<div class="ball game-{{lower $ticket.GameType}} {{ if eq $ticket.GameType "Lotto" }}lotto-range-{{rangeClass $ball}} {{ end }}{{ if inSlice $ball $ticket.MatchedDraw.Balls }}matched pulse{{ end }}">
|
||||
{{ $ball }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ if or (gt $ticket.MatchedMain 0) (gt $ticket.MatchedBonus 0) }}
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{{ $ticket.MatchedMain }} match{{ if ne $ticket.MatchedMain 1 }}es{{ end }}
|
||||
{{ if gt $ticket.MatchedBonus 0 }}, {{ $ticket.MatchedBonus }} bonus{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ if eq $ticket.GameType "Lotto" }}
|
||||
<span style="color: lightgray; font-style: italic;">–</span>
|
||||
{{ else if gt (intVal $ticket.Bonus2) 0 }}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{{ if gt (intVal $ticket.Bonus1) 0 }}
|
||||
{{ $b1 := intVal $ticket.Bonus1 }}
|
||||
<div class="ball game-{{lower $ticket.GameType}} bonus{{ if inSlice $b1 $ticket.MatchedDraw.BonusBalls }} matched-bonus{{ end }}">
|
||||
{{ $b1 }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ $b2 := intVal $ticket.Bonus2 }}
|
||||
<div class="ball game-{{lower $ticket.GameType}} bonus{{ if inSlice $b2 $ticket.MatchedDraw.BonusBalls }} matched-bonus{{ end }}">
|
||||
{{ $b2 }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else if gt (intVal $ticket.Bonus1) 0 }}
|
||||
{{ $b := intVal $ticket.Bonus1 }}
|
||||
<div class="ball game-{{lower $ticket.GameType}} bonus{{ if inSlice $b $ticket.MatchedDraw.BonusBalls }} matched-bonus{{ end }}">
|
||||
{{ $b }}
|
||||
</div>
|
||||
{{ else }}
|
||||
<span style="color: lightgray;">–</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{ .PurchaseDate }}</td>
|
||||
<td>{{ .PurchaseMethod }}</td>
|
||||
<td>
|
||||
{{ if .ImagePath }}
|
||||
<a href="/{{ .ImagePath }}" target="_blank">View</a>
|
||||
{{ else }}–{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ if $ticket.IsWinner }}
|
||||
{{ if eq $ticket.PrizeLabel "" }}
|
||||
<span class="text-yellow-500 italic">pending</span>
|
||||
{{ else if eq $ticket.PrizeLabel "Free Ticket" }}
|
||||
🎟️ {{ $ticket.PrizeLabel }}
|
||||
{{ else }}
|
||||
💷 {{ $ticket.PrizeLabel }}
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
–
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
49
web/templates/admin/dashboard.html
Normal file
49
web/templates/admin/dashboard.html
Normal file
@@ -0,0 +1,49 @@
|
||||
{{ define "content" }}
|
||||
<h2>📊 Admin Dashboard</h2>
|
||||
<p class="text-sm text-gray-600">Welcome back, admin.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 my-6">
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Total Tickets</h3>
|
||||
<p class="text-2xl font-bold">{{ .Stats.TotalTickets }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Total Winners</h3>
|
||||
<p class="text-2xl font-bold text-green-600">{{ .Stats.TotalWinners }}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-xl p-4 shadow">
|
||||
<h3 class="text-sm text-gray-500">Prize Fund Awarded</h3>
|
||||
<p class="text-2xl font-bold text-blue-600">£{{ printf "%.2f" .Stats.TotalPrizeAmount }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-6">
|
||||
<h3 class="text-lg font-semibold mb-2">Recent Ticket Matches</h3>
|
||||
<table class="w-full text-sm border">
|
||||
<thead>
|
||||
<tr class="bg-gray-100">
|
||||
<th class="px-2 py-1">Draw Date</th>
|
||||
<th class="px-2 py-1">Triggered By</th>
|
||||
<th class="px-2 py-1">Matched</th>
|
||||
<th class="px-2 py-1">Winners</th>
|
||||
<th class="px-2 py-1">Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .MatchLogs }}
|
||||
<tr class="border-t">
|
||||
<td class="px-2 py-1">{{ .RunAt }}</td>
|
||||
<td class="px-2 py-1">{{ .TriggeredBy }}</td>
|
||||
<td class="px-2 py-1">{{ .TicketsMatched }}</td>
|
||||
<td class="px-2 py-1">{{ .WinnersFound }}</td>
|
||||
<td class="px-2 py-1 text-xs text-gray-500">{{ .Notes }}</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-2 italic text-gray-400">No match history found</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
9
web/templates/admin/draws/delete_draw.html
Normal file
9
web/templates/admin/draws/delete_draw.html
Normal file
@@ -0,0 +1,9 @@
|
||||
{{ define "delete_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Delete Draw</h2>
|
||||
<form method="POST" action="/admin/draws/delete">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .Draw.ID }}">
|
||||
<p>Are you sure you want to delete the draw on <strong>{{ .Draw.DrawDate }}</strong>?</p>
|
||||
<button class="btn bg-red-600 hover:bg-red-700">Delete</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
46
web/templates/admin/draws/list_draws.html
Normal file
46
web/templates/admin/draws/list_draws.html
Normal file
@@ -0,0 +1,46 @@
|
||||
{{ define "draw_list" }}
|
||||
<h2 class="text-xl font-bold mb-4">Draws Overview</h2>
|
||||
|
||||
<table class="w-full table-auto border">
|
||||
<thead class="bg-gray-100">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Game</th>
|
||||
<th>Date</th>
|
||||
<th>Ball Set</th>
|
||||
<th>Machine</th>
|
||||
<th>Prizes?</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Draws }}
|
||||
<tr class="border-t">
|
||||
<td>{{ .ID }}</td>
|
||||
<td>{{ .GameType }}</td>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>{{ .BallSet }}</td>
|
||||
<td>{{ .Machine }}</td>
|
||||
<td>
|
||||
{{ if .PrizeSet }}
|
||||
✅
|
||||
{{ else }}
|
||||
❌
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
<a href="/admin/draws/modify?id={{ .ID }}" class="text-blue-600">Edit</a> |
|
||||
<a href="/admin/draws/delete?id={{ .ID }}" class="text-red-600">Delete</a> |
|
||||
{{ if .PrizeSet }}
|
||||
<a href="/admin/draws/prizes/modify?draw_date={{ .DrawDate }}" class="text-green-600">Modify Prizes</a>
|
||||
{{ else }}
|
||||
<a href="/admin/draws/prizes/add?draw_date={{ .DrawDate }}" class="text-gray-600">Add Prizes</a>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr><td colspan="7" class="text-center text-gray-500">No draws available.</td></tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
12
web/templates/admin/draws/modify_draw.html
Normal file
12
web/templates/admin/draws/modify_draw.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "modify_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Modify Draw</h2>
|
||||
<form method="POST" action="/admin/draws/modify">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .Draw.ID }}">
|
||||
<label class="block">Game Type: <input name="game_type" class="input" value="{{ .Draw.GameType }}"></label>
|
||||
<label class="block">Draw Date: <input name="draw_date" type="date" class="input" value="{{ .Draw.DrawDate }}"></label>
|
||||
<label class="block">Ball Set: <input name="ball_set" class="input" value="{{ .Draw.BallSet }}"></label>
|
||||
<label class="block">Machine: <input name="machine" class="input" value="{{ .Draw.Machine }}"></label>
|
||||
<button class="btn">Update Draw</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
42
web/templates/admin/draws/new_draw.html
Normal file
42
web/templates/admin/draws/new_draw.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{ define "new_draw" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Add New Draw</h2>
|
||||
<form method="POST" action="/admin/draws/submit">
|
||||
{{ .CSRFField }}
|
||||
<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">Ball Set: <input name="ball_set" class="input"></label>
|
||||
<label class="block">Machine: <input name="machine" class="input"></label>
|
||||
<button class="btn">Create Draw</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
|
||||
<!-- Old new draw
|
||||
|
||||
{{ define "content" }}
|
||||
<a href="/">← Back</a>
|
||||
<h2>Add New Thunderball Draw</h2>
|
||||
<form method="POST" action="/submit">
|
||||
{{ .csrfField }}
|
||||
<div class="form-section">
|
||||
<label>Date: <input type="date" name="date" required></label>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label>Machine: <input type="text" name="machine" required></label>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label>Ball Set: <input type="text" name="ballset" required></label>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label>Ball 1: <input type="text" name="ball1" required></label>
|
||||
<label>Ball 2: <input type="text" name="ball2" required></label>
|
||||
<label>Ball 3: <input type="text" name="ball3" required></label>
|
||||
<label>Ball 4: <input type="text" name="ball4" required></label>
|
||||
<label>Ball 5: <input type="text" name="ball5" required></label>
|
||||
</div>
|
||||
<div class="form-section">
|
||||
<label>Thunderball: <input type="text" name="thunderball" required></label>
|
||||
</div>
|
||||
<button type="submit">Save Draw</button>
|
||||
</form>
|
||||
{{ end }} -->
|
||||
13
web/templates/admin/draws/prizes/add_prizes.html
Normal file
13
web/templates/admin/draws/prizes/add_prizes.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "add_prizes" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Add Prize Breakdown</h2>
|
||||
<form method="POST" action="/admin/draws/prizes/add">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="draw_date" value="{{ .DrawDate }}">
|
||||
{{ range $i, $ := .PrizeLabels }}
|
||||
<label class="block">{{ . }}
|
||||
<input name="prize{{ add $i 1 }}_per_winner" class="input">
|
||||
</label>
|
||||
{{ end }}
|
||||
<button class="btn">Save Prizes</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
13
web/templates/admin/draws/prizes/modify_prizes.html
Normal file
13
web/templates/admin/draws/prizes/modify_prizes.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ define "modify_prizes" }}
|
||||
<h2 class="text-xl font-semibold mb-4">Modify Prize Breakdown</h2>
|
||||
<form method="POST" action="/admin/draws/prizes/modify">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="draw_date" value="{{ .DrawDate }}">
|
||||
{{ range $i, $ := .PrizeLabels }}
|
||||
<label class="block">{{ . }}
|
||||
<input name="prize{{ add $i 1 }}_per_winner" class="input" value="{{ index $.Prizes (print "prize" (add $i 1) "_per_winner") }}">
|
||||
</label>
|
||||
{{ end }}
|
||||
<button class="btn">Update Prizes</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
26
web/templates/admin/logs/access_logs.html
Normal file
26
web/templates/admin/logs/access_logs.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ define "content" }}
|
||||
<h2>Admin Access Log</h2>
|
||||
|
||||
<table class="table-auto w-full text-sm mt-4">
|
||||
<thead>
|
||||
<tr class="bg-gray-200">
|
||||
<th class="px-2 py-1 text-left">Time</th>
|
||||
<th class="px-2 py-1">User ID</th>
|
||||
<th class="px-2 py-1">Path</th>
|
||||
<th class="px-2 py-1">IP</th>
|
||||
<th class="px-2 py-1">User Agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .AuditLogs }}
|
||||
<tr class="border-b">
|
||||
<td class="px-2 py-1">{{ .AccessedAt }}</td>
|
||||
<td class="px-2 py-1 text-center">{{ .UserID }}</td>
|
||||
<td class="px-2 py-1">{{ .Path }}</td>
|
||||
<td class="px-2 py-1">{{ .IP }}</td>
|
||||
<td class="px-2 py-1 text-xs text-gray-600">{{ .UserAgent }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
27
web/templates/admin/logs/audit.html
Normal file
27
web/templates/admin/logs/audit.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ define "content" }}
|
||||
<h2>Audit Log</h2>
|
||||
<p>Recent sensitive admin events and system activity:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>User ID</th>
|
||||
<th>Action</th>
|
||||
<th>IP</th>
|
||||
<th>User Agent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .AuditLogs }}
|
||||
<tr>
|
||||
<td>{{ .Timestamp }}</td>
|
||||
<td>{{ .UserID }}</td>
|
||||
<td>{{ .Action }}</td>
|
||||
<td>{{ .IP }}</td>
|
||||
<td>{{ .UserAgent }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
29
web/templates/admin/triggers.html
Normal file
29
web/templates/admin/triggers.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{{ define "content" }}
|
||||
<h2>Manual Admin Triggers</h2>
|
||||
|
||||
<form method="POST" action="/admin/triggers">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="action" value="match">
|
||||
<button>Run Ticket Matching</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/admin/triggers" class="mt-4">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="action" value="prizes">
|
||||
<button>Update Missing Prizes</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/admin/triggers" class="mt-4">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="action" value="run_all">
|
||||
<button>Run All</button>
|
||||
</form>
|
||||
|
||||
<form method="POST" action="/admin/triggers" class="mt-4">
|
||||
{{ .CSRFField }}
|
||||
<input type="hidden" name="action" value="refresh_prizes">
|
||||
<button class="bg-indigo-500 text-white px-4 py-2 rounded hover:bg-indigo-600">
|
||||
Refresh Ticket Prizes
|
||||
</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
5
web/templates/error/403.html
Normal file
5
web/templates/error/403.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{{ define "content" }}
|
||||
<h2 class="text-red-600 text-xl font-bold">🚫 Forbidden</h2>
|
||||
<p class="mt-2">You do not have permission to access this page.</p>
|
||||
<a href="/" class="text-blue-500 underline mt-4 inline-block">Return to Home</a>
|
||||
{{ end }}
|
||||
3
web/templates/error/404.html
Normal file
3
web/templates/error/404.html
Normal file
@@ -0,0 +1,3 @@
|
||||
{{ define "content" }}
|
||||
<h2>Not Found</h2> <p>The page doesn't exist.</p>
|
||||
{{ end }}
|
||||
7
web/templates/error/429.html
Normal file
7
web/templates/error/429.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5 text-center">
|
||||
<h1 class="text-danger">🚫 Too Many Requests</h1>
|
||||
<p>Whoa there! You're making requests too quickly. Please slow down and try again in a moment.</p>
|
||||
<a href="/" class="btn btn-primary mt-3">Back to home</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
14
web/templates/error/500.html
Normal file
14
web/templates/error/500.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5 text-center">
|
||||
<h1 class="display-4 text-danger">500 - Server Error</h1>
|
||||
<p class="lead">Something went wrong on our end. We're working to fix it.</p>
|
||||
|
||||
<div class="mt-4">
|
||||
<i class="bi bi-exclamation-triangle-fill text-warning" style="font-size: 3rem;"></i>
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-muted">Please try again later or contact support if the issue persists.</p>
|
||||
|
||||
<a href="/" class="btn btn-primary mt-3">Return to Homepage</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
4
web/templates/index.html
Normal file
4
web/templates/index.html
Normal file
@@ -0,0 +1,4 @@
|
||||
{{ define "content" }}
|
||||
<h1>Welcome to SynLotto</h1>
|
||||
<p>Your trusted lottery platform!</p>
|
||||
{{ end }}
|
||||
17
web/templates/main/footer.html
Normal file
17
web/templates/main/footer.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{{ define "footer" }}
|
||||
<footer class="bg-light text-center text-muted py-3 mt-auto border-top">
|
||||
<small>
|
||||
© Copyright {{ .SiteName }}
|
||||
{{ $currentYear := now.Year }}
|
||||
{{ if eq .CopyrightYearStart $currentYear }}
|
||||
{{ $currentYear }}
|
||||
{{ else }}
|
||||
{{ .CopyrightYearStart }} - {{ $currentYear }}
|
||||
{{ end }}
|
||||
All rights reserved.
|
||||
| <a href="/legal/privacy">Privacy Policy</a> |
|
||||
<a href="/legal/terms">Terms & Conditions</a> |
|
||||
<a href="/contact">Contact Us</a>
|
||||
</small>
|
||||
</footer>
|
||||
{{ end }}
|
||||
86
web/templates/main/layout.html
Normal file
86
web/templates/main/layout.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{{ define "layout" }}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{{ .SiteName }}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/site.css">
|
||||
</head>
|
||||
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<!-- Topbar -->
|
||||
{{ template "topbar" . }}
|
||||
|
||||
<!-- Main layout using Flexbox -->
|
||||
<div class="d-flex flex-grow-1">
|
||||
<!-- Sidebar -->
|
||||
<nav class="col-md-2 d-none d-md-block bg-light sidebar pt-3">
|
||||
<div class="position-sticky">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#lotterySubmenu" role="button" aria-expanded="false" aria-controls="lotterySubmenu">
|
||||
<strong>Lottery Results</strong>
|
||||
<i class="bi bi-chevron-down small"></i>
|
||||
</a>
|
||||
<div class="collapse ps-3" id="lotterySubmenu">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="/lottery/today">Today's Results</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/results/lotto">Lotto</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/results/thunderball">Thunderball</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/results/euromillions">EuroMillions</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/results/Set For Life">Set For Life</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#statisticsSubmenu" role="button" aria-expanded="false" aria-controls="statisticsSubmenu">
|
||||
<strong>Statistics</strong>
|
||||
</a>
|
||||
<div class="collapse ps-3" id="statisticsSubmenu">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="/lottery/today">Today's Results</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/lottery/history">Lotto</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/statistics/thunderball">Thunderball</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/lottery/history">Set For Life</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/lottery/stats">EuroMillions</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link d-flex justify-content-between align-items-center" data-bs-toggle="collapse" href="#syndicateSubmenu" role="button" aria-expanded="false" aria-controls="syndicateSubmenu">
|
||||
<strong>Syndicate</strong>
|
||||
</a>
|
||||
<div class="collapse ps-3" id="syndicateSubmenu">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item"><a class="nav-link" href="/syndicate/create">Create new Syndicate</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="col px-md-4 pt-4">
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{ .Flash }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "content" . }}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
{{ template "footer" . }}
|
||||
|
||||
<!-- JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
||||
129
web/templates/main/topbar.html
Normal file
129
web/templates/main/topbar.html
Normal file
@@ -0,0 +1,129 @@
|
||||
{{ 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">
|
||||
<a href="/account/notifications/read?id={{ $n.ID }}" class="text-decoration-none text-dark d-block">
|
||||
<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.Subject }}</div>
|
||||
<small class="text-muted">{{ $n.Body }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{{ if lt (add $i 1) $total }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ if not .Notifications }}
|
||||
<li class="text-center text-muted py-2">No notifications</li>
|
||||
{{ end }}
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/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">
|
||||
{{ if gt .MessageCount 0 }}
|
||||
<span class="position-absolute top-0 start-0 translate-middle badge rounded-pill bg-danger text-dark badge-small">
|
||||
{{ if gt .MessageCount 15 }}15+{{ else }}{{ .MessageCount }}{{ end }}
|
||||
</span>
|
||||
{{ end }}
|
||||
</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>
|
||||
|
||||
{{ if .Messages }}
|
||||
{{ range $i, $m := .Messages }}
|
||||
<li class="px-3 py-2">
|
||||
<a href="/account/messages/read?id={{ $m.ID }}" class="text-decoration-none text-dark d-block">
|
||||
<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">{{ $m.Subject }}</div>
|
||||
<small class="text-muted">{{ truncate $m.Message 40 }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No messages</li>
|
||||
{{ end }}
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- User Greeting/Dropdown -->
|
||||
<div class="dropdown">
|
||||
<a class="nav-link dropdown-toggle text-dark" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Hello, {{ .User.Username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end shadow-sm" aria-labelledby="userDropdown">
|
||||
<li><a class="dropdown-item" href="/account/profile">Update Profile</a></li>
|
||||
<li><a class="dropdown-item" href="/account/password">Change Password</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item text-danger" href="/logout">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ else }}
|
||||
<a class="btn btn-outline-primary btn-sm" href="/account/login">Login</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
</nav>
|
||||
{{ end }}
|
||||
98
web/templates/results/thunderball.html
Normal file
98
web/templates/results/thunderball.html
Normal file
@@ -0,0 +1,98 @@
|
||||
{{ define "content" }}
|
||||
<h2>Thunderball Results</h2>
|
||||
|
||||
<form method="GET" action="/results/thunderball" style="margin-bottom: 1rem;">
|
||||
<input type="text" name="q" placeholder="Search by date or draw number" value="{{ .Query }}">
|
||||
|
||||
<select name="year">
|
||||
<option value="">All Years</option>
|
||||
{{ range .Years }}
|
||||
<option value="{{ . }}" {{ if eq $.YearFilter . }}selected{{ end }}>{{ . }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
|
||||
<select name="machine">
|
||||
<option value="">All Machines</option>
|
||||
{{ range .Machines }}
|
||||
<option value="{{ . }}" {{ if eq $.MachineFilter . }}selected{{ end }}>{{ . }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
|
||||
<select name="ballset">
|
||||
<option value="">All Ball Sets</option>
|
||||
{{ range .BallSets }}
|
||||
<option value="{{ . }}" {{ if eq $.BallSetFilter . }}selected{{ end }}>{{ . }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
|
||||
<button type="submit">Search</button>
|
||||
{{ if .Query }}
|
||||
<a href="/results/thunderball" style="margin-left: 10px;">Clear</a>
|
||||
{{ end }}
|
||||
</form>
|
||||
|
||||
{{ if .NoResultsMsg }}
|
||||
<p>{{ .NoResultsMsg }}</p>
|
||||
{{ end }}
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Draw Number</th>
|
||||
<th>Date</th>
|
||||
<th>Machine</th>
|
||||
<th>Ball Set</th>
|
||||
<th>Numbers</th>
|
||||
<th>Thunderball</th>
|
||||
</tr>
|
||||
{{ range .Results }}
|
||||
<tr>
|
||||
<td>{{ .Id }}</td>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>{{ .Machine }}</td>
|
||||
<td>{{ .BallSet }}</td>
|
||||
<td>
|
||||
{{ range $i, $n := .SortedBalls }}
|
||||
{{ if $i }}, {{ end }}{{ $n }}
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{ .Thunderball }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
|
||||
<style>
|
||||
.pagination a {
|
||||
margin: 0 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pagination .disabled {
|
||||
color: #aaa;
|
||||
pointer-events: none;
|
||||
cursor: default;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pagination" style="margin-top: 20px;">
|
||||
{{ if gt .Page 1 }}
|
||||
<a href="/results/thunderball?page=1{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">« First</a>
|
||||
<a href="/results/thunderball?page={{ minus1 .Page }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">← Prev</a>
|
||||
{{ else }}
|
||||
<span class="disabled">« First</span>
|
||||
<span class="disabled">← Prev</span>
|
||||
{{ end }}
|
||||
|
||||
Page {{ .Page }} of {{ .TotalPages }}
|
||||
|
||||
{{ if lt .Page .TotalPages }}
|
||||
<a href="/results/thunderball?page={{ plus1 .Page }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">Next →</a>
|
||||
<a href="/results/thunderball?page={{ .TotalPages }}{{ if .Query }}&q={{ .Query }}{{ end }}{{ if .YearFilter }}&year={{ .YearFilter }}{{ end }}{{ if .MachineFilter }}&machine={{ .MachineFilter }}{{ end }}{{ if .BallSetFilter }}&ballset={{ .BallSetFilter }}{{ end }}">Last »</a>
|
||||
{{ else }}
|
||||
<span class="disabled">Next →</span>
|
||||
<span class="disabled">Last »</span>
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
Showing {{ add (mul (minus1 .Page) 20) 1 }}–{{ min (mul .Page 20) .TotalResults }} of {{ .TotalResults }} results
|
||||
</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
18
web/templates/statistics/thunderball.html
Normal file
18
web/templates/statistics/thunderball.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{{ define "content" }}
|
||||
<div class="wrap">
|
||||
<h1>Thunderball Statistics</h1>
|
||||
|
||||
<div class="grid">
|
||||
<div class="card">
|
||||
<h3>Top 5 (since {{.Since}})</h3>
|
||||
<table>
|
||||
<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>
|
||||
{{end}}
|
||||
26
web/templates/syndicate/create.html
Normal file
26
web/templates/syndicate/create.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Create New Syndicate</h2>
|
||||
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST">
|
||||
{{ .CSRFField }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Syndicate Name</label>
|
||||
<input type="text" name="name" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description (optional)</label>
|
||||
<textarea name="description" class="form-control"></textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
<a href="/syndicate" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
41
web/templates/syndicate/index.html
Normal file
41
web/templates/syndicate/index.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Your Syndicates</h2>
|
||||
|
||||
{{ if .ManagedSyndicates }}
|
||||
<h4 class="mt-4">Managed</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .ManagedSyndicates }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ .Name }}</strong><br>
|
||||
<small class="text-muted">{{ .Description }}</small>
|
||||
</div>
|
||||
<a href="/syndicate/view?id={{ .ID }}" class="btn btn-outline-primary btn-sm">Manage</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ if .JoinedSyndicates }}
|
||||
<h4 class="mt-4">Member</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .JoinedSyndicates }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{{ .Name }}</strong><br>
|
||||
<small class="text-muted">{{ .Description }}</small>
|
||||
</div>
|
||||
<a href="/syndicate/view?id={{ .ID }}" class="btn btn-outline-secondary btn-sm">View</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
{{ if not .ManagedSyndicates | and (not .JoinedSyndicates) }}
|
||||
<div class="alert alert-info">You are not part of any syndicates yet.</div>
|
||||
{{ end }}
|
||||
|
||||
<a href="/syndicate/create" class="btn btn-primary mt-3">Create New Syndicate</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
18
web/templates/syndicate/invite.html
Normal file
18
web/templates/syndicate/invite.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Invite Member to "{{ .Syndicate.Name }}"</h2>
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST">
|
||||
{{ .CSRFField }}
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username to Invite</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send Invite</button>
|
||||
<a href="/syndicate/view?id={{ .Syndicate.ID }}" class="btn btn-secondary ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
34
web/templates/syndicate/log_ticket.html
Normal file
34
web/templates/syndicate/log_ticket.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Add Ticket for {{ .Syndicate.Name }}</h2>
|
||||
|
||||
<form method="POST">
|
||||
{{ .CSRFField }}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="game_type" class="form-label">Game Type</label>
|
||||
<select class="form-select" id="game_type" name="game_type" required>
|
||||
<option value="Thunderball">Thunderball</option>
|
||||
<option value="Lotto">Lotto</option>
|
||||
<!-- Add more as needed -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="draw_date" class="form-label">Draw Date</label>
|
||||
<input type="date" class="form-control" id="draw_date" name="draw_date" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="purchase_method" class="form-label">Purchase Method</label>
|
||||
<input type="text" class="form-control" id="purchase_method" name="purchase_method">
|
||||
</div>
|
||||
|
||||
<!-- Ball Inputs -->
|
||||
{{ template "ballInputs" . }}
|
||||
|
||||
<button type="submit" class="btn btn-success">Submit Ticket</button>
|
||||
<a href="/syndicate/view?id={{ .Syndicate.ID }}" class="btn btn-secondary ms-2">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
57
web/templates/syndicate/manager/invite_links.html
Normal file
57
web/templates/syndicate/manager/invite_links.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Manage Invite Links</h2>
|
||||
|
||||
{{ if .Tokens }}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invite Link</th>
|
||||
<th>Invited By</th>
|
||||
<th>Status</th>
|
||||
<th>Expires</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Tokens }}
|
||||
<tr>
|
||||
<td>
|
||||
<code>/syndicate/join?token={{ .Token }}</code>
|
||||
</td>
|
||||
<td>User #{{ .InvitedByUserID }}</td>
|
||||
<td>
|
||||
{{ if .AcceptedByUserID.Valid }}
|
||||
<span class="text-success" title="Joined on {{ .AcceptedAt.Time.Format \"02 Jan 2006 15:04\" }}">
|
||||
Accepted by User #{{ .AcceptedByUserID.Int64 }}
|
||||
</span>
|
||||
{{ else if .ExpiresAt.Before (now) }}
|
||||
<span class="text-danger" title="Expired on {{ .ExpiresAt.Format \"02 Jan 2006 15:04\" }}">Expired</span>
|
||||
{{ else }}
|
||||
<span class="text-warning" title="Expires in {{ humanizeTime .ExpiresAt }}">Pending</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td>{{ .ExpiresAt.Format "02 Jan 2006 15:04" }}</td>
|
||||
<td>
|
||||
{{ if not .AcceptedByUserID.Valid }}
|
||||
<form method="POST" action="/account/syndicates/invite/revoke?token={{ .Token }}&id={{ $.SyndicateID }}" class="d-inline">
|
||||
{{ $.CSRFField }}
|
||||
<button class="btn btn-sm btn-outline-danger" title="Invalidate this token">Revoke</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
<form method="POST" action="/account/syndicates/invite/token?id={{ $.SyndicateID }}" class="d-inline">
|
||||
{{ $.CSRFField }}
|
||||
<button class="btn btn-sm btn-outline-secondary" title="Create a new invite token">Regenerate</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<div class="alert alert-info">No invite links found for this syndicate.</div>
|
||||
{{ end }}
|
||||
|
||||
<a href="/syndicate/view?id={{ .SyndicateID }}" class="btn btn-secondary mt-3">← Back</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
42
web/templates/syndicate/tickets.html
Normal file
42
web/templates/syndicate/tickets.html
Normal file
@@ -0,0 +1,42 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Syndicate Tickets</h2>
|
||||
|
||||
{{ if .Tickets }}
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Game</th>
|
||||
<th>Draw Date</th>
|
||||
<th>Numbers</th>
|
||||
<th>Matched</th>
|
||||
<th>Prize</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Tickets }}
|
||||
<tr>
|
||||
<td>{{ .GameType }}</td>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>
|
||||
{{ .Ball1 }} {{ .Ball2 }} {{ .Ball3 }} {{ .Ball4 }} {{ .Ball5 }} {{ .Ball6 }}
|
||||
{{ if .Bonus1 }}+{{ .Bonus1 }}{{ end }}
|
||||
{{ if .Bonus2 }} {{ .Bonus2 }}{{ end }}
|
||||
</td>
|
||||
<td>{{ .MatchedMain }} + {{ .MatchedBonus }}</td>
|
||||
<td>
|
||||
{{ if .IsWinner }}
|
||||
💷 {{ .PrizeLabel }}
|
||||
{{ else }}
|
||||
<span class="text-muted">–</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ else }}
|
||||
<div class="alert alert-info">No tickets found for this syndicate.</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
38
web/templates/syndicate/view.html
Normal file
38
web/templates/syndicate/view.html
Normal file
@@ -0,0 +1,38 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>{{ .Syndicate.Name }}</h2>
|
||||
<p class="text-muted">{{ .Syndicate.Description }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<h4>Members</h4>
|
||||
<ul class="list-group mb-3">
|
||||
{{ range .Members }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<span>{{ .Username }}</span>
|
||||
<small class="text-muted">Joined: {{ .JoinedAt.Format "02 Jan 2006" }}</small>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
|
||||
{{ if .IsManager }}
|
||||
<div class="alert alert-warning">
|
||||
<strong>Manager Controls</strong><br>
|
||||
You can add or remove members, and manage tickets.
|
||||
</div>
|
||||
|
||||
<a href="/syndicate/invite?id={{ .Syndicate.ID }}" class="btn btn-outline-primary">Invite Members</a>
|
||||
|
||||
<form method="POST" action="/account/syndicates/invite/token?id={{ .Syndicate.ID }}" class="mt-3">
|
||||
{{ .CSRFField }}
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">Generate Invite Link</button>
|
||||
</form>
|
||||
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info mt-2">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<a href="/syndicate" class="btn btn-secondary mt-3">← Back to Syndicates</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
34
web/templates/tickets.html
Normal file
34
web/templates/tickets.html
Normal file
@@ -0,0 +1,34 @@
|
||||
{{ define "content" }}
|
||||
<a href="/">← Back to Home</a>
|
||||
<h2>My Tickets</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Game</th>
|
||||
<th>Numbers</th>
|
||||
<th>Bonus</th>
|
||||
<th>Duplicate?</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range . }}
|
||||
<tr>
|
||||
<td>{{ .DrawDate }}</td>
|
||||
<td>{{ .GameType }}</td>
|
||||
<td>{{ .Ball1 }}, {{ .Ball2 }}, {{ .Ball3 }}, {{ .Ball4 }}, {{ .Ball5 }}</td>
|
||||
<td>
|
||||
{{ if .Bonus1 }}{{ .Bonus1 }}{{ end }}
|
||||
{{ if .Bonus2 }}, {{ .Bonus2 }}{{ end }}
|
||||
</td>
|
||||
<td>
|
||||
{{ if .Duplicate }}⚠️ Yes{{ else }}✔️ No{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr><td colspan="5">No tickets logged yet.</td></tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user