diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-14 15:23:48 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-14 15:23:48 +0200 |
| commit | eac1145e29e1a2a2ff5d9654ca010071a5840843 (patch) | |
| tree | 59e62e1e0f67b71083c30e4d7f234a8370075c9a /web/templates.templ | |
| parent | 991ab2f179b2d84e8b0ce6d0634b4033f20e4b86 (diff) | |
feat: add official climb workflow
Diffstat (limited to 'web/templates.templ')
| -rw-r--r-- | web/templates.templ | 100 |
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> |