Add race and station management
Build & Deploy / build-and-deploy (push) Successful in 2m18s

- races table: name, date, description, is_active
- stations table: ordered checkpoints with GPS per race
- New /api/races and /api/races/{id}/stations endpoints
- Upload now requires race + station selection; uses station GPS
  so images without GPS EXIF are accepted
- passages filtered by active race throughout
- RacePage: create races, manage stations (add/edit/delete checkpoints)
- Navbar shows active race name
- Start and finish stations created automatically per race

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 09:44:45 +01:00
parent 3dcf979e6f
commit 5393e85a74
15 changed files with 841 additions and 139 deletions
+25 -9
View File
@@ -1,36 +1,52 @@
import { useEffect, useState } from 'react'
import { NavLink, Route, Routes } from 'react-router-dom'
import { api } from './api.js'
import StartlistPage from './pages/StartlistPage.jsx'
import ReviewPage from './pages/ReviewPage.jsx'
import ResultsPage from './pages/ResultsPage.jsx'
import PassagesPage from './pages/PassagesPage.jsx'
import UploadPage from './pages/UploadPage.jsx'
import RacePage from './pages/RacePage.jsx'
import './App.css'
function Nav() {
function Nav({ activeRace }) {
const linkClass = ({ isActive }) => (isActive ? 'nav-link active' : 'nav-link')
return (
<nav className="navbar">
<span className="navbar-brand">Timing Admin</span>
<NavLink to="/" className={linkClass} end>Startliste</NavLink>
<span className="navbar-brand">Timing</span>
<NavLink to="/" className={linkClass} end>Løp</NavLink>
<NavLink to="/startlist" className={linkClass}>Startliste</NavLink>
<NavLink to="/upload" className={linkClass}>Last opp</NavLink>
<NavLink to="/passages" className={linkClass}>Passeringer</NavLink>
<NavLink to="/review" className={linkClass}>Gjennomgang</NavLink>
<NavLink to="/results" className={linkClass}>Resultater</NavLink>
<NavLink to="/upload" className={linkClass}>Last opp</NavLink>
{activeRace && (
<span style={{ marginLeft: 'auto', fontSize: '0.8rem', color: '#2ecc71', fontWeight: 600 }}>
{activeRace.name}
</span>
)}
</nav>
)
}
export default function App() {
const [activeRace, setActiveRace] = useState(null)
useEffect(() => {
api.getActiveRace().then(r => setActiveRace(r?.race_id ? r : null))
}, [])
return (
<>
<Nav />
<Nav activeRace={activeRace} />
<main className="container">
<Routes>
<Route path="/" element={<StartlistPage />} />
<Route path="/passages" element={<PassagesPage />} />
<Route path="/review" element={<ReviewPage />} />
<Route path="/results" element={<ResultsPage />} />
<Route path="/" element={<RacePage onRaceChange={() => api.getActiveRace().then(r => setActiveRace(r?.race_id ? r : null))} />} />
<Route path="/startlist" element={<StartlistPage />} />
<Route path="/upload" element={<UploadPage />} />
<Route path="/passages" element={<PassagesPage activeRace={activeRace} />} />
<Route path="/review" element={<ReviewPage activeRace={activeRace} />} />
<Route path="/results" element={<ResultsPage activeRace={activeRace} />} />
</Routes>
</main>
</>