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.

323 lines
12 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>
// WebSocket connection for timer updates
const timerSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/timer`);
const timerDisplay = document.getElementById('timer');
const statusIndicator = document.getElementById('status-indicator');
timerSocket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.type === 'time') {
timerDisplay.textContent = data.time.toFixed(3);
} else if (data.type === 'status') {
statusIndicator.textContent = data.status;
// Update status indicator color
statusIndicator.className = 'badge ';
if (data.status === 'Ready') {
statusIndicator.className += 'bg-secondary';
} else if (data.status === 'Running') {
statusIndicator.className += 'bg-success';
} else if (data.status === 'Finished') {
statusIndicator.className += 'bg-primary';
}
} else if (data.type === 'lane-time') {
// Update lane time display
const laneTimeElement = document.getElementById(`lane-${data.lane}-time`);
if (laneTimeElement) {
laneTimeElement.textContent = data.time.toFixed(3);
}
} else if (data.type === 'lane-position') {
// Update lane position display
const lanePositionElement = document.getElementById(`lane-${data.lane}-position`);
if (lanePositionElement) {
lanePositionElement.textContent = data.position;
}
} else if (data.type === 'reload') {
// Reload the page when heat changes
window.location.reload();
}
};
// Events WebSocket for race events
const eventsSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/events`);
eventsSocket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data.event === 'race_start') {
// Race has started
console.log('Race started!');
// You could add visual effects or sounds here
} else if (data.event === 'lane_finish') {
// A lane has finished
console.log(`Lane ${data.lane} finished with time ${data.time}`);
// You could add visual effects or sounds here
} else if (data.event === 'race_end') {
// Race has ended
console.log('Race ended!');
// You could add visual effects or sounds here
// Reload the page after a short delay to show final results
setTimeout(() => {
window.location.reload();
}, 3000);
}
};
// 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">
<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>
}