Refactor and remove sqlite and replace with MySQL

This commit is contained in:
2025-10-23 18:43:31 +01:00
parent d53e27eea8
commit 21ebc9c34b
139 changed files with 1013 additions and 529 deletions

124
web/static/css/site.css Normal file
View File

@@ -0,0 +1,124 @@
@import "topbar.css";
body { font-family: Arial, sans-serif; margin: 0px; }
table { border-collapse: collapse; width: 100%; margin-top: 20px; }
th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: center; }
th { background-color: #f5f5f5; }
.form-section { margin-bottom: 20px; }
.topbar {
display: flex;
justify-content: flex-end;
padding: 0.5rem, 0.5rem, 0.5rem, 0.5rem;
font-size: 14px;
}
.topbar .auth p {
margin: 0;
}
.flash { padding: 10px; color: green; background: #e9ffe9; border: 1px solid #c3e6c3; margin-bottom: 15px; }
/* Ball Stuff */
.ball {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: #eee;
border-radius: 50%;
min-width: 32px;
height: 32px;
padding: 6px;
margin-right: 6px;
font-weight: bold;
text-align: center;
will-change: transform;
}
.ball-container {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.bonus {
border: 2px solid #facc15;
}
.matched-bonus {
background-color: #facc15;
color: black;
font-weight: bold;
animation: pulse 2s infinite;
}
.ball.matched {
animation: pulse 2s ease-in-out infinite;
}
.ball.game-lotto.lotto-range-01-09 {
background-color: white;
border: .1px solid #000000;
color: black;
}
.ball.game-lotto.lotto-range-10-19 {
background-color: #add8e6;
color: black;
}
.ball.game-lotto.lotto-range-20-29 {
background-color: #ffc0cb;
color: black;
}
.ball.game-lotto.lotto-range-30-39 {
background-color: #90ee90;
color: black;
}
.ball.game-lotto.lotto-range-40-49 {
background-color: #ffff99;
color: black;
}
.ball.game-lotto.lotto-range-50-plus {
background-color: #B40AE3;
color: black;
}
.ball[class*="game-thunderball"]:not(.bonus) {
background-color: #6B65FC;
border: 2px solid white;
}
.ball[class*="game-thunderball"] {
background-color: #d8b4fe ;
border: 2px solid white;
}
.ball[class*="game-euromillions"]:not(.bonus) {
background-color: #BF1314;
border: 2px solid white;
}
.ball[class*="game-euromillions"] {
background-color: #FFDE59 ;
border: 2px solid white;
}
/* .ball[class*="game-setforlife"]:not(.bonus) {
background-color: #6B65FC;
border: 2px solid white;
} */
.ball[class*="game-setforlife"] {
background-color: #98F5F9 ;
border: 2px solid white;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
.pulse {
animation: pulse 0.8s ease-in-out infinite;
transform-origin: center;
}

50
web/static/css/topbar.css Normal file
View 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%);
}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 dont have any notifications.
</div>
{{ end }}
</div>
{{ end }}

View 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 }}

View 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 }}

View 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 }}

View 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 havent 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }} -->

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View File

@@ -0,0 +1,3 @@
{{ define "content" }}
<h2>Not Found</h2> <p>The page doesn't exist.</p>
{{ end }}

View 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 }}

View 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
View File

@@ -0,0 +1,4 @@
{{ define "content" }}
<h1>Welcome to SynLotto</h1>
<p>Your trusted lottery platform!</p>
{{ end }}

View File

@@ -0,0 +1,17 @@
{{ define "footer" }}
<footer class="bg-light text-center text-muted py-3 mt-auto border-top">
<small>
&copy; 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 }}

View 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 }}

View 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 }}

View 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 }}
&nbsp; Page {{ .Page }} of {{ .TotalPages }} &nbsp;
{{ 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 }}

View 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}}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}

View 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 }}