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