diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 10:31:32 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 10:31:32 +0200 |
| commit | 0c3507139795c3117bcdf82ca4b444e934bd3c74 (patch) | |
| tree | 626f62c7dc15ded26bfeedbfa32a6638c07c3dfa /web/templates.templ | |
| parent | c720b572573774504cbbeda5ac1b5da2d1afeff6 (diff) | |
feat(web): improve ride detail climb profiles
Diffstat (limited to 'web/templates.templ')
| -rw-r--r-- | web/templates.templ | 120 |
1 files changed, 73 insertions, 47 deletions
diff --git a/web/templates.templ b/web/templates.templ index 9ebeedb..c640171 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -71,60 +71,85 @@ templ RideDetailContent(data RideDetailView) { <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 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 { + if len(data.Profile.OfficialClimbs) > 0 { + <section class="panel official-climbs-panel"> + <div class="section-heading"> + <p class="eyebrow">Official climbs</p> + </div> + <div class="official-climb-list"> + for _, climb := range data.Profile.OfficialClimbs { + <details class="official-climb-card" data-official-climb-card> + <summary> + <span class="official-climb-card-title"><span class="climb-number">Climb { formatClimbNumber(climb.Index) }</span><strong>{ climb.OfficialName }</strong></span> + <dl class="official-climb-summary-stats"> + <div><dt>Length</dt><dd>{ formatProfileDistance(climb.DistanceKm) }</dd></div> + <div><dt>Cotacol</dt><dd>{ formatCotacol(climb.Cotacol) }</dd></div> + <div><dt>Avg slope</dt><dd>{ formatSlope(climb.SlopePercent) }</dd></div> + </dl> + <span class="official-climb-chevron" aria-hidden="true">⌄</span> + </summary> + <div class="official-climb-card-body"> + <canvas class="official-climb-profile" data-official-profile data-profile-start={ formatRouteIndex(climb.StartIndex) } data-profile-end={ formatRouteIndex(climb.EndIndex) } tabindex="0" role="img" aria-label={ officialProfileLabel(climb.OfficialName) }></canvas> + <div class="official-climb-profile-legend" role="list" aria-label="Average slope color scale"> + <span role="listitem"><i class="profile-band-swatch profile-band-downhill" aria-hidden="true"></i>Downhill</span> + <span role="listitem"><i class="profile-band-swatch profile-band-0-3" aria-hidden="true"></i>0–3%</span> + <span role="listitem"><i class="profile-band-swatch profile-band-3-6" aria-hidden="true"></i>3–6%</span> + <span role="listitem"><i class="profile-band-swatch profile-band-6-9" aria-hidden="true"></i>6–9%</span> + <span role="listitem"><i class="profile-band-swatch profile-band-9-12" aria-hidden="true"></i>9–12%</span> + <span role="listitem"><i class="profile-band-swatch profile-band-12-plus" aria-hidden="true"></i>12%+</span> + </div> + </div> + </details> + } + </div> + </section> + } + <div class={ rideDetailColumnsClass(len(data.Profile.UnmatchedClimbs)) }> + if len(data.Profile.UnmatchedClimbs) > 0 { <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="section-heading"> + <div><p class="eyebrow">Next steps</p><h2>Climbs to match</h2></div> + <span class="section-count">{ formatClimbCount(len(data.Profile.UnmatchedClimbs)) }</span> + </div> + <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 { formatClimbCount(len(data.Profile.UnmatchedClimbs)) }</output> + <button class="button secondary" type="button" data-climb-next aria-label="Next climb">→</button> + </nav> + <div class="climb-list" data-climb-list> + for _, climb := range data.Profile.UnmatchedClimbs { + <article class="climb-item todo-item" data-climb-item data-climb-index={ formatRouteIndex(climb.Index) }> <div class="climb-item-heading"> - <strong><span class="climb-number">Climb { formatClimbNumber(index) }</span>{ climb.Name }</strong> + <strong><span class="todo-check" aria-hidden="true"></span><span class="climb-number">Climb { formatClimbNumber(climb.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> - } + <p class="climb-status">Waiting for an official climb match.</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> - } + } + </div> </section> - } - </div> - if data.HasRoute { - <div class="ride-detail-main"> + } + <div class="ride-detail-main"> <section class="panel profile-panel"> <div class="profile-heading"> <h2>Elevation profile</h2> @@ -138,6 +163,7 @@ templ RideDetailContent(data RideDetailView) { <section class="panel map-panel"> <div id="ride-map" class="ride-map" role="img" aria-label="Recorded ride route map"></div> </section> + </div> </div> } </div> |