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