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

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