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.

449 lines
18 KiB

package templates
import (
"track-gopher/models"
"fmt"
"strconv"
)
// RaceManage renders the race management view
templ RaceManage(groups []models.Group, currentGroup models.Group, heats []models.Heat, racers []models.Racer, currentHeatNum int, results []models.HeatResult) {
@Layout("Race Management") {
<div class="container-fluid mt-3">
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Race Controls</h3>
</div>
<div class="card-body">
<div class="mb-3">
<label for="group-select" class="form-label">Racing Group</label>
<select id="group-select" class="form-select" hx-post="/api/race/set-group" hx-trigger="change" hx-vals='js:{"group_id": parseInt(this.value)}' hx-swap="none">
for _, group := range groups {
<option value={ fmt.Sprintf("%d", group.ID) } selected?={ group.ID == currentGroup.ID }>
{ group.Name }
</option>
}
</select>
</div>
<div class="d-flex justify-content-between mb-3">
<button id="prev-heat-btn" class="btn btn-secondary" hx-post="/api/race/previous-heat" hx-swap="none" disabled?={ currentHeatNum <= 1 }>
<i class="bi bi-arrow-left"></i> Previous Heat
</button>
<span class="align-self-center">
<strong>Heat { strconv.Itoa(currentHeatNum) } of { strconv.Itoa(len(heats)) }</strong>
</span>
<button id="next-heat-btn" class="btn btn-secondary" hx-post="/api/race/next-heat" hx-swap="none" disabled?={ currentHeatNum >= len(heats) }>
Next Heat <i class="bi bi-arrow-right"></i>
</button>
</div>
<div class="d-flex justify-content-between">
<button id="reset-timer-btn" class="btn btn-warning" onclick="resetTimer()">
<i class="bi bi-arrow-clockwise"></i> Reset Timer
</button>
<button id="force-end-btn" class="btn btn-danger" onclick="forceEndHeat()">
<i class="bi bi-flag-fill"></i> Force End Heat
</button>
<button id="rerun-heat-btn" class="btn btn-info" hx-post="/api/race/rerun-heat" hx-swap="none">
<i class="bi bi-arrow-repeat"></i> Re-run Heat
</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Timer</h3>
</div>
<div class="card-body">
<div class="timer-display text-center">
<div id="timer" class="display-1 fw-bold">0.000</div>
<div id="status-indicator" class="badge bg-secondary mb-3">Ready</div>
</div>
<div class="d-flex justify-content-center">
<div id="gate-status" class="alert alert-secondary">
Gate Status: <span id="gate-status-text">Unknown</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h3 class="mb-0">Current Heat</h3>
</div>
<div class="card-body">
<div id="current-heat">
@raceManageCurrentHeat(heats, racers, currentHeatNum, results)
</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) {
@raceManageNextHeat(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-secondary text-white">
<h3 class="mb-0">Heat Results</h3>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Heat</th>
<th>Lane 1</th>
<th>Lane 2</th>
<th>Lane 3</th>
<th>Lane 4</th>
</tr>
</thead>
<tbody>
for _, result := range results {
<tr>
<td>{ strconv.Itoa(result.HeatNumber) }</td>
<td>{ fmt.Sprintf("%.3f (#%d)", result.Lane1Time, result.Lane1Position) }</td>
<td>{ fmt.Sprintf("%.3f (#%d)", result.Lane2Time, result.Lane2Position) }</td>
<td>{ fmt.Sprintf("%.3f (#%d)", result.Lane3Time, result.Lane3Position) }</td>
<td>{ fmt.Sprintf("%.3f (#%d)", result.Lane4Time, result.Lane4Position) }</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// WebSocket connections
const timerSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/timer`);
const adminSocket = new WebSocket(`${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/admin`);
const timerDisplay = document.getElementById('timer');
const statusIndicator = document.getElementById('status-indicator');
const gateStatus = document.getElementById('gate-status');
const gateStatusText = document.getElementById('gate-status-text');
// Timer socket handling
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 mb-3 ';
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';
// Auto-save results when race finishes
saveHeatResults();
}
} else if (data.type === 'gate') {
gateStatusText.textContent = data.status;
// Update gate status color
gateStatus.className = 'alert ';
if (data.status === 'Open') {
gateStatus.className += 'alert-danger';
} else if (data.status === 'Closed') {
gateStatus.className += 'alert-success';
} else {
gateStatus.className += 'alert-secondary';
}
} 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;
}
}
};
// Admin socket handling
adminSocket.onmessage = function(event) {
const data = JSON.parse(event.data);
if (data === 'heat-changed' || data === 'group-changed' || data === 'heat-rerun') {
// Reload the page when heat or group changes
window.location.reload();
}
};
// Function to reset the timer
function resetTimer() {
fetch('/api/timer/reset', { method: 'POST' })
.then(response => {
if (!response.ok) {
console.error('Failed to reset timer');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Function to force end the current heat
function forceEndHeat() {
fetch('/api/timer/force-end', { method: 'POST' })
.then(response => {
if (!response.ok) {
console.error('Failed to force end heat');
}
})
.catch(error => {
console.error('Error:', error);
});
}
// Function to save heat results
function saveHeatResults() {
// Get lane times and positions from the UI
const lanes = [1, 2, 3, 4];
const results = {
group_id: parseInt(document.getElementById('group-select').value),
heat_number: parseInt('{ strconv.Itoa(currentHeatNum) }'),
};
lanes.forEach(lane => {
const timeElement = document.getElementById(`lane-${lane}-time`);
const positionElement = document.getElementById(`lane-${lane}-position`);
if (timeElement && positionElement) {
const timeText = timeElement.textContent.trim();
const positionText = positionElement.textContent.trim();
results[`lane${lane}_time`] = timeText !== '--.-' ? parseFloat(timeText) : 0;
results[`lane${lane}_position`] = positionText !== '-' ? parseInt(positionText) : 0;
}
});
// Save results to the server
fetch('/api/race/save-result', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(results),
})
.then(response => {
if (!response.ok) {
console.error('Failed to save heat results');
}
})
.catch(error => {
console.error('Error:', error);
});
}
</script>
}
}
// Helper template for displaying current heat in the management view
templ raceManageCurrentHeat(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="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>Lane</th>
<th>Racer</th>
<th>Car #</th>
<th>Time</th>
<th>Position</th>
</tr>
</thead>
<tbody>
if currentHeat.Lane1ID != nil {
@raceManageLaneRow(1, *currentHeat.Lane1ID, racers, currentResult)
}
if currentHeat.Lane2ID != nil {
@raceManageLaneRow(2, *currentHeat.Lane2ID, racers, currentResult)
}
if currentHeat.Lane3ID != nil {
@raceManageLaneRow(3, *currentHeat.Lane3ID, racers, currentResult)
}
if currentHeat.Lane4ID != nil {
@raceManageLaneRow(4, *currentHeat.Lane4ID, racers, currentResult)
}
</tbody>
</table>
</div>
}
// Helper template for displaying a lane row in the management view
templ raceManageLaneRow(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
}
}
}}
<tr>
<td>{ strconv.Itoa(lane) }</td>
<td>{ racer.FirstName } { racer.LastName }</td>
<td>{ racer.CarNumber }</td>
<td id={ fmt.Sprintf("lane-%d-time", lane) }>
if hasResult {
{ fmt.Sprintf("%.3f", time) }
} else {
--.-
}
</td>
<td id={ fmt.Sprintf("lane-%d-position", lane) }>
if hasResult {
{ strconv.Itoa(position) }
} else {
-
}
</td>
</tr>
}
// Helper template for displaying next heat in the management view
templ raceManageNextHeat(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
}
}
}}
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>Lane</th>
<th>Racer</th>
<th>Car #</th>
</tr>
</thead>
<tbody>
if nextHeat.Lane1ID != nil {
@raceManageNextHeatRow(1, *nextHeat.Lane1ID, racers)
}
if nextHeat.Lane2ID != nil {
@raceManageNextHeatRow(2, *nextHeat.Lane2ID, racers)
}
if nextHeat.Lane3ID != nil {
@raceManageNextHeatRow(3, *nextHeat.Lane3ID, racers)
}
if nextHeat.Lane4ID != nil {
@raceManageNextHeatRow(4, *nextHeat.Lane4ID, racers)
}
</tbody>
</table>
</div>
}
// Helper template for displaying a row in the next heat preview
templ raceManageNextHeatRow(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>
}