summaryrefslogtreecommitdiff
path: root/web/templates.templ
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-14 15:23:48 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-14 15:23:48 +0200
commiteac1145e29e1a2a2ff5d9654ca010071a5840843 (patch)
tree59e62e1e0f67b71083c30e4d7f234a8370075c9a /web/templates.templ
parent991ab2f179b2d84e8b0ce6d0634b4033f20e4b86 (diff)
feat: add official climb workflow
Diffstat (limited to 'web/templates.templ')
-rw-r--r--web/templates.templ100
1 files changed, 78 insertions, 22 deletions
diff --git a/web/templates.templ b/web/templates.templ
index 515c482..9ebeedb 100644
--- a/web/templates.templ
+++ b/web/templates.templ
@@ -57,35 +57,91 @@ templ RideDetailPage(data RideDetailView) {
}
templ RideDetailContent(data RideDetailView) {
- <p class="eyebrow">Ride detail</p>
<div class="toolbar">
- <div><h1>{ data.Ride.Name }</h1><p class="lead">{ data.Ride.Type } · { formatRideDate(data.Ride.StartDate) }</p></div>
+ <div><h1 class="ride-detail-title">{ data.Ride.Name }</h1><p class="lead">{ data.Ride.Type } · { formatRideDate(data.Ride.StartDate) }</p></div>
<a class="button secondary" href="/">Back to rides</a>
</div>
- <section class="panel">
- <dl>
- <div><dt>Distance</dt><dd>{ formatDistance(data.Ride.DistanceM) }</dd></div>
- <div><dt>Moving time</dt><dd>{ formatDuration(data.Ride.MovingTimeS) }</dd></div>
- <div><dt>Elevation</dt><dd>{ formatElevation(data.Ride.TotalElevationGainM) }</dd></div>
- </dl>
- </section>
if data.RouteError != "" {
<div class="error">{ data.RouteError }</div>
}
- if data.HasRoute {
- <section class="panel profile-panel">
- <div class="profile-heading">
- <h2>Elevation profile</h2>
- <p>Hover or focus the profile to inspect the ride at that distance.</p>
- </div>
- <div class="profile-chart">
- <canvas id="ride-profile-chart" tabindex="0" role="img" aria-describedby="ride-profile-hover" aria-label="Interactive ride elevation profile"></canvas>
+ if data.Notice != "" {
+ <div class="notice">{ data.Notice }</div>
+ }
+ if data.ActionError != "" {
+ <div class="error">{ data.ActionError }</div>
+ }
+ <div class="ride-detail-grid">
+ <div class="ride-detail-sidebar">
+ <section class="panel summary-panel">
+ <dl>
+ <div><dt>Distance</dt><dd>{ formatDistance(data.Ride.DistanceM) }</dd></div>
+ <div><dt>Moving time</dt><dd>{ formatDuration(data.Ride.MovingTimeS) }</dd></div>
+ <div><dt>Elevation</dt><dd>{ formatElevation(data.Ride.TotalElevationGainM) }</dd></div>
+ </dl>
+ </section>
+ if data.HasRoute {
+ <section class="panel climbs-panel">
+ if len(data.Profile.Climbs) > 0 {
+ <nav class="climb-navigation" aria-label="Climb navigation">
+ <button class="button secondary" type="button" data-climb-previous aria-label="Previous climb">←</button>
+ <output data-climb-position>Climb 1 of { formatClimbNumber(len(data.Profile.Climbs) - 1) }</output>
+ <button class="button secondary" type="button" data-climb-next aria-label="Next climb">→</button>
+ </nav>
+ }
+ if len(data.Profile.Climbs) == 0 {
+ <p class="empty">No climbs detected in this ride.</p>
+ }
+ if len(data.Profile.Climbs) > 0 {
+ <div class="climb-list" data-climb-list>
+ for index, climb := range data.Profile.Climbs {
+ <article class="climb-item" data-climb-item data-climb-index={ formatRouteIndex(index) }>
+ <div class="climb-item-heading">
+ <strong><span class="climb-number">Climb { formatClimbNumber(index) }</span>{ climb.Name }</strong>
+ <span class="climb-range" data-climb-summary>{ formatProfileDistance(climb.StartKm) }–{ formatProfileDistance(climb.EndKm) }</span>
+ </div>
+ <p class="climb-metrics" data-climb-metrics>{ climb.Category } · { formatProfileDistance(climb.DistanceKm) } at { formatSlope(climb.SlopePercent) } · Cotacol { formatCotacol(climb.Cotacol) }</p>
+ if climb.OfficialClimbID > 0 {
+ <p class="climb-status matched">Official climb matched: { climb.OfficialName }</p>
+ } else {
+ <p class="climb-status">No official climb matched yet.</p>
+ <form class="official-climb-form" data-official-climb-form method="post" action={ templ.URL(officialClimbCreateURL(data.Ride.ID)) }>
+ <label>Name<input type="text" name="name" required placeholder="Official climb name"/></label>
+ <input type="hidden" name="start_index" value={ formatRouteIndex(climb.StartIndex) } data-boundary-input="start"/>
+ <input type="hidden" name="end_index" value={ formatRouteIndex(climb.EndIndex) } data-boundary-input="end"/>
+ <div class="boundary-controls">
+ <div><strong>Start</strong><div><button class="button secondary" type="button" data-boundary-button="start" data-boundary-source="profile">Select on profile</button><button class="button secondary" type="button" data-boundary-button="start" data-boundary-source="map">Select on map</button></div><output data-boundary-output="start">{ formatProfileDistance(climb.StartKm) }</output></div>
+ <div><strong>End</strong><div><button class="button secondary" type="button" data-boundary-button="end" data-boundary-source="profile">Select on profile</button><button class="button secondary" type="button" data-boundary-button="end" data-boundary-source="map">Select on map</button></div><output data-boundary-output="end">{ formatProfileDistance(climb.EndKm) }</output></div>
+ </div>
+ <output class="boundary-preview" data-boundary-preview>Preview the corrected climb boundaries.</output>
+ <button class="button" type="submit">Save official climb</button>
+ </form>
+ }
+ </article>
+ }
+ </div>
+ }
+ </section>
+ }
+ </div>
+ if data.HasRoute {
+ <div class="ride-detail-main">
+ <section class="panel profile-panel">
+ <div class="profile-heading">
+ <h2>Elevation profile</h2>
+ <p>Hover or focus the profile to inspect the ride at that distance.</p>
+ </div>
+ <div class="profile-chart">
+ <canvas id="ride-profile-chart" tabindex="0" role="img" aria-describedby="ride-profile-hover" aria-label="Interactive ride elevation profile"></canvas>
+ </div>
+ <output id="ride-profile-hover" class="profile-hover" aria-live="polite">Hover or focus the profile to inspect elevation.</output>
+ </section>
+ <section class="panel map-panel">
+ <div id="ride-map" class="ride-map" role="img" aria-label="Recorded ride route map"></div>
+ </section>
</div>
- <output id="ride-profile-hover" class="profile-hover" aria-live="polite">Hover or focus the profile to inspect elevation.</output>
- </section>
- <section class="panel map-panel">
- <div id="ride-map" class="ride-map" role="img" aria-label="Recorded ride route map"></div>
- </section>
+ }
+ </div>
+ if data.HasRoute {
@templ.JSONScript("ride-route", data.Route)
@templ.JSONScript("ride-profile", data.Profile)
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" defer></script>