package web templ Layout(title string, content templ.Component) { { title } · biking_home
@content
} templ RidesPage(data RidesPageData) { @Layout("Rides", RidesContent(data)) } templ RidesContent(data RidesPageData) {

Ride library

Refine

Filter rides

Reset
@RideRangeFilter(rideDistanceFilterConfig(data.Filters)) @RideRangeFilter(rideDurationFilterConfig(data.Filters)) @RideRangeFilter(rideCotacolFilterConfig(data.Filters))
Started
if len(data.Items) == 0 { if data.Filters.Active {
No rides match the selected filters. Clear filters.
} else {
No rides stored yet. Import your first Strava rides.
} } else { for _, header := range data.Headers { } for _, item := range data.Items { }
{ header.Label }
{ item.Ride.Name }
{ item.Ride.Type }
{ formatRideDate(item.Ride.StartDate) }{ formatDistance(item.Ride.DistanceM) }{ item.MovingTime }{ formatSpeed(item.Ride.AverageSpeedMps) }{ formatElevation(item.Ride.TotalElevationGainM) }{ item.Cotacol }{ item.CotacolPer100Km }
}
} templ AnalyticsPage(data AnalyticsPageData) { @Layout("Analytics", AnalyticsContent(data)) } templ AnalyticsContent(data AnalyticsPageData) {

Analytics

Ride analytics

One point per ride: average speed on the x axis, Cotacol per 100 km on the y axis.

Refine

Analyses

Reset
@RideRangeFilter(rideDistanceFilterConfig(data.Filter))

Chart

Speed vs. Cotacol per 100 km

if len(data.Points) > 0 { { formatRideCount(len(data.Points)) } }
if len(data.Points) == 0 { if data.Filter.Active {
No rides match the selected filters. Clear filters.
} else {
No eligible rides yet. Import rides to see the chart.
} } else {
Hover a point to inspect a ride. }
@templ.JSONScript("analytics-points", data.Points) } templ RideDetailPage(data RideDetailView) { @Layout("Ride detail", RideDetailContent(data)) } templ RideDetailContent(data RideDetailView) {

{ data.Ride.Name }

{ data.Ride.Type } · { formatRideDate(data.Ride.StartDate) }

Back to rides
if data.RouteError != "" {
{ data.RouteError }
} if data.Notice != "" {
{ data.Notice }
} if data.ActionError != "" {
{ data.ActionError }
}
Distance
{ formatDistance(data.Ride.DistanceM) }
Moving time
{ data.MovingTime }
Speed
{ formatSpeed(data.Ride.AverageSpeedMps) }
Elevation
{ formatElevation(data.Ride.TotalElevationGainM) }
if data.HasRoute { if len(data.Profile.OfficialClimbs()) > 0 {

Official climbs

for _, climb := range data.Profile.OfficialClimbs() {
Climb { formatClimbNumber(climb.Index) }{ climb.OfficialName }
Length
{ formatProfileDistance(climb.DistanceKm) }
Cotacol
{ formatCotacol(climb.Cotacol) }
Avg slope
{ formatSlope(climb.SlopePercent) }
<=0% 0–3% 3–6% 6–9% 9%+
}
}
if len(data.Profile.UnmatchedClimbs()) > 0 {

Next steps

Climbs to match

{ formatClimbCount(len(data.Profile.UnmatchedClimbs())) }
for _, climb := range data.Profile.UnmatchedClimbs() {
Climb { formatClimbNumber(climb.Index) }{ climb.Name } { formatProfileDistance(climb.StartKm) }–{ formatProfileDistance(climb.EndKm) }

{ climb.Category } · { formatProfileDistance(climb.DistanceKm) } at { formatSlope(climb.SlopePercent) } · Cotacol { formatCotacol(climb.Cotacol) }

Waiting for an official climb match.

Start{ formatProfileDistance(climb.StartKm) }
End{ formatProfileDistance(climb.EndKm) }
Preview the corrected climb boundaries.
}
}

Elevation profile

Hover or focus the profile to inspect the ride at that distance.

Hover or focus the profile to inspect elevation.
}
if data.HasRoute { @templ.JSONScript("ride-route", data.Route) @templ.JSONScript("ride-profile", data.Profile) @templ.JSONScript("ride-passes", data.Passes) } } templ SyncPage(data SyncPageData) { @Layout("Sync Strava", SyncContent(data)) } templ SyncContent(data SyncPageData) {

Data import

Sync Strava rides

Choose a date range. New rides are downloaded as GPX files and recorded in the local library.

if data.Notice != "" {
{ data.Notice }
} else { } if data.Error != "" {
{ data.Error }
} else { }
if !data.HasAuth {

Strava authorization is required before the first sync.

Authorize Strava } else {
}
}