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.

357 lines
14 KiB

package templates
import (
"track-gopher/models"
"fmt"
"strconv"
)
// RacePublic renders the public race view
templ RacePublic(currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
@LayoutPublic("Race - " + currentGroup.Name) {
<div class="container-fluid mt-3">
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h2 class="mb-0 text-center">{ currentGroup.Name } - Heat { strconv.Itoa(currentHeatNum) } of { strconv.Itoa(len(heats)) }</h2>
</div>
<div class="card-body">
<div id="current-heat" class="mb-4">
<div class="row">
<div class="col-12">
<div class="timer-display text-center mb-4">
<div id="timer" class="display-1 fw-bold">0.000</div>
<div id="status-indicator" class="badge bg-secondary">Ready</div>
</div>
<div class="lanes-container">
@raceCurrentHeatLanes(heats, racers, currentHeatNum, results)
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h3 class="mb-0">Next Heat</h3>
</div>
<div class="card-body">
if currentHeatNum < len(heats) {
@raceNextHeatPreview(heats, racers, currentHeatNum+1)
} else {
<div class="alert alert-info">No more heats in this group</div>
}
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-info text-white">
<h3 class="mb-0">Upcoming Heat</h3>
</div>
<div class="card-body">
if currentHeatNum+1 < len(heats) {
@raceNextHeatPreview(heats, racers, currentHeatNum+2)
} else {
<div class="alert alert-info">No more heats in this group</div>
}
</div>
</div>
</div>
</div>
</div>
<script>
// Set up SSE connection
console.log("Setting up SSE connection...");
const eventSource = new EventSource('/api/events');
eventSource.onopen = function() {
console.log("SSE connection opened");
};
eventSource.onerror = function(error) {
console.error("SSE connection error:", error);
};
eventSource.addEventListener('debug', function(event) {
console.log("Debug event received:", event.data);
});
eventSource.addEventListener('lane-finish', function(event) {
console.log("Lane finish event received:", event.data);
try {
const laneFinishData = JSON.parse(event.data);
const laneTimeElement = document.getElementById(`lane-${laneFinishData.lane}-time`);
if (laneTimeElement) {
laneTimeElement.textContent = laneFinishData.time.toFixed(4);
}
const lanePositionElement = document.getElementById(`lane-${laneFinishData.lane}-position`);
if (lanePositionElement) {
lanePositionElement.textContent = getOrdinal(laneFinishData.place);
}
// Highlight the lane card
const laneCard = document.querySelector(`.lane-card[data-lane="${laneFinishData.lane}"]`);
if (laneCard) {
laneCard.classList.add('bg-success-subtle');
}
} catch (error) {
console.error("Error processing lane finish event:", error);
}
});
eventSource.addEventListener('status', function(event) {
console.log("Status event received:", event.data);
try {
const statusData = JSON.parse(event.data);
let statusText = 'Unknown';
let statusClass = 'bg-secondary';
if (statusData.status === 'idle') {
statusText = 'Ready';
statusClass = 'bg-primary';
// Reset all lanes
document.querySelectorAll('.lane-card').forEach(lane => {
lane.classList.remove('bg-success-subtle');
});
// Reset all times and positions
document.querySelectorAll('[id^="lane-"][id$="-time"]').forEach(el => {
el.textContent = '--.-';
});
document.querySelectorAll('[id^="lane-"][id$="-position"]').forEach(el => {
el.textContent = '-';
});
} else if (statusData.status === 'running') {
statusText = 'Race Running';
statusClass = 'bg-success';
} else if (statusData.status === 'finished') {
statusText = 'Race Complete';
statusClass = 'bg-info';
// Reload the page after a short delay to show final results
setTimeout(() => {
window.location.reload();
}, 3000);
}
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.textContent = statusText;
statusIndicator.className = `badge ${statusClass}`;
}
const timerDisplay = document.getElementById('timer');
if (timerDisplay && statusData.status === 'idle') {
timerDisplay.textContent = '0.000';
}
} catch (error) {
console.error("Error processing status event:", error);
}
});
function getOrdinal(n) {
const s = ["th", "st", "nd", "rd"];
const v = n % 100;
return n + (s[(v-20)%10] || s[v] || s[0]);
}
// Auto-refresh the page every 30 seconds to keep data current
setTimeout(() => {
window.location.reload();
}, 30000);
</script>
}
}
// Helper template for displaying current heat lanes
templ raceCurrentHeatLanes(heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
{{
// Find current heat
var currentHeat models.Heat
for _, heat := range heats {
if heat.HeatNum == currentHeatNum {
currentHeat = heat
break
}
}
// Find heat result if available
var currentResult *models.HeatResult
for _, result := range results {
if result.HeatNumber == currentHeatNum {
currentResult = &result
break
}
}
}}
<div class="row row-cols-1 row-cols-md-4 g-4">
if currentHeat.Lane1ID != nil {
@raceLaneCard(1, *currentHeat.Lane1ID, racers, currentResult)
}
if currentHeat.Lane2ID != nil {
@raceLaneCard(2, *currentHeat.Lane2ID, racers, currentResult)
}
if currentHeat.Lane3ID != nil {
@raceLaneCard(3, *currentHeat.Lane3ID, racers, currentResult)
}
if currentHeat.Lane4ID != nil {
@raceLaneCard(4, *currentHeat.Lane4ID, racers, currentResult)
}
</div>
}
// Helper template for displaying a lane card
templ raceLaneCard(lane int, racerID int64, racers []models.Racer, result *models.HeatResult) {
{{
// Find racer
var racer models.Racer
for _, r := range racers {
if r.ID == racerID {
racer = r
break
}
}
// Get time and position from result if available
var time float64
var position int
var hasResult bool
if result != nil {
hasResult = true
switch lane {
case 1:
time = result.Lane1Time
position = result.Lane1Position
case 2:
time = result.Lane2Time
position = result.Lane2Position
case 3:
time = result.Lane3Time
position = result.Lane3Position
case 4:
time = result.Lane4Time
position = result.Lane4Position
}
}
}}
<div class="col">
<div class="card h-100 lane-card" data-lane="{ strconv.Itoa(lane) }">
<div class="card-header bg-primary text-white">
<h4 class="mb-0">Lane { strconv.Itoa(lane) }</h4>
</div>
<div class="card-body">
<h5 class="card-title">{ racer.FirstName } { racer.LastName }</h5>
<p class="card-text">
<strong>Car #:</strong> { racer.CarNumber }<br/>
<strong>Weight:</strong> { fmt.Sprintf("%.1f oz", racer.CarWeight) }
</p>
<div class="result-area">
<div class="row">
<div class="col-6">
<div class="text-center">
<h6>Time</h6>
<div id={ fmt.Sprintf("lane-%d-time", lane) } class="display-6">
if hasResult {
{ fmt.Sprintf("%.3f", time) }
} else {
--.-
}
</div>
</div>
</div>
<div class="col-6">
<div class="text-center">
<h6>Position</h6>
<div id={ fmt.Sprintf("lane-%d-position", lane) } class="display-6">
if hasResult {
{ strconv.Itoa(position) }
} else {
-
}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
// Helper template for displaying next heat preview
templ raceNextHeatPreview(heats []models.Heat, racers []models.Racer, heatNum int) {
{{
// Find the heat
var nextHeat models.Heat
for _, heat := range heats {
if heat.HeatNum == heatNum {
nextHeat = heat
break
}
}
}}
<h4 class="mb-3">Heat { strconv.Itoa(heatNum) }</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Lane</th>
<th>Racer</th>
<th>Car #</th>
</tr>
</thead>
<tbody>
if nextHeat.Lane1ID != nil {
@raceNextHeatRow(1, *nextHeat.Lane1ID, racers)
}
if nextHeat.Lane2ID != nil {
@raceNextHeatRow(2, *nextHeat.Lane2ID, racers)
}
if nextHeat.Lane3ID != nil {
@raceNextHeatRow(3, *nextHeat.Lane3ID, racers)
}
if nextHeat.Lane4ID != nil {
@raceNextHeatRow(4, *nextHeat.Lane4ID, racers)
}
</tbody>
</table>
</div>
}
// Helper template for displaying a row in the next heat preview
templ raceNextHeatRow(lane int, racerID int64, racers []models.Racer) {
{{
// Find racer
var racer models.Racer
for _, r := range racers {
if r.ID == racerID {
racer = r
break
}
}
}}
<tr>
<td>{ strconv.Itoa(lane) }</td>
<td>{ racer.FirstName } { racer.LastName }</td>
<td>{ racer.CarNumber }</td>
</tr>
}