You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
8.5 KiB

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pinewood Derby Race Manager</title>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<style>
.lane {
transition: all 0.3s ease-in-out;
}
.lane.finished {
background-color: #d1fae5;
}
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<div class="container mx-auto px-4 py-8">
<header class="mb-8">
<h1 class="text-3xl font-bold text-center text-blue-800">Pinewood Derby Race Manager</h1>
</header>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<div class="flex justify-between items-center mb-6">
<h2 class="text-xl font-semibold">Race Control</h2>
<div id="race-status" class="px-4 py-2 rounded-full text-white bg-gray-500">
Connecting...
</div>
</div>
<div class="flex space-x-4 justify-center">
<button
hx-post="/api/reset"
hx-swap="none"
class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Reset Race
</button>
<button
hx-post="/api/force-end"
hx-swap="none"
class="bg-yellow-600 hover:bg-yellow-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
>
Force End
</button>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-6 mb-8">
<h2 class="text-xl font-semibold mb-4">Race Results</h2>
<div id="lanes-container" class="space-y-4">
<div id="lane-1" class="lane p-4 border rounded-lg flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center text-white font-bold">1</div>
<span class="ml-4 font-medium">Lane 1</span>
</div>
<div class="text-right">
<span class="place hidden"></span>
<span class="time text-lg font-mono">--.--.---</span>
</div>
</div>
<div id="lane-2" class="lane p-4 border rounded-lg flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold">2</div>
<span class="ml-4 font-medium">Lane 2</span>
</div>
<div class="text-right">
<span class="place hidden"></span>
<span class="time text-lg font-mono">--.--.---</span>
</div>
</div>
<div id="lane-3" class="lane p-4 border rounded-lg flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center text-white font-bold">3</div>
<span class="ml-4 font-medium">Lane 3</span>
</div>
<div class="text-right">
<span class="place hidden"></span>
<span class="time text-lg font-mono">--.--.---</span>
</div>
</div>
<div id="lane-4" class="lane p-4 border rounded-lg flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-yellow-500 flex items-center justify-center text-white font-bold">4</div>
<span class="ml-4 font-medium">Lane 4</span>
</div>
<div class="text-right">
<span class="place hidden"></span>
<span class="time text-lg font-mono">--.--.---</span>
</div>
</div>
</div>
</div>
</div>
<!-- SSE Events Source -->
<div hx-ext="sse" sse-connect="/api/events">
<div sse-swap="race-start" hx-swap-oob="true">
<script>
document.getElementById('race-status').textContent = 'Race Running';
document.getElementById('race-status').className = 'px-4 py-2 rounded-full text-white bg-green-600';
// Reset all lanes
document.querySelectorAll('.lane').forEach(lane => {
lane.classList.remove('finished');
lane.querySelector('.time').textContent = '--.--.---';
lane.querySelector('.place').classList.add('hidden');
});
</script>
</div>
<div sse-swap="lane-finish" hx-swap-oob="true">
<script>
const laneFinishData = JSON.parse(event.data);
const lane = document.getElementById(`lane-${laneFinishData.lane}`);
if (lane) {
lane.classList.add('finished');
lane.querySelector('.time').textContent = laneFinishData.time.toFixed(4);
const placeEl = lane.querySelector('.place');
placeEl.textContent = `${getOrdinal(laneFinishData.place)} Place`;
placeEl.classList.remove('hidden');
}
function getOrdinal(n) {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v-20)%10] || s[v] || s[0]);
}
</script>
</div>
<div sse-swap="race-complete" hx-swap-oob="true">
<script>
document.getElementById('race-status').textContent = 'Race Complete';
document.getElementById('race-status').className = 'px-4 py-2 rounded-full text-white bg-purple-600';
</script>
</div>
<div sse-swap="status" hx-swap-oob="true">
<script>
const statusData = JSON.parse(event.data);
let statusText = 'Unknown';
let statusClass = 'bg-gray-500';
if (statusData.status === 'idle') {
statusText = 'Ready';
statusClass = 'bg-blue-600';
// Reset all lanes
document.querySelectorAll('.lane').forEach(lane => {
lane.classList.remove('finished');
lane.querySelector('.time').textContent = '--.--.---';
lane.querySelector('.place').classList.add('hidden');
});
} else if (statusData.status === 'running') {
statusText = 'Race Running';
statusClass = 'bg-green-600';
} else if (statusData.status === 'finished') {
statusText = 'Race Complete';
statusClass = 'bg-purple-600';
}
document.getElementById('race-status').textContent = statusText;
document.getElementById('race-status').className = `px-4 py-2 rounded-full text-white ${statusClass}`;
</script>
</div>
</div>
<script>
// Handle button states based on race status
document.addEventListener('htmx:afterRequest', function(event) {
if (event.detail.path === '/api/reset' || event.detail.path === '/api/force-end') {
// Refresh the page status after reset or force end
fetch('/api/status')
.then(response => response.json())
.then(data => {
// Update UI based on status
console.log('Race status updated:', data.status);
});
}
});
</script>
</body>
</html>