package web
import "slices"
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
if len(data.Items) == 0 {
if data.Filters.Active {
} else {
}
} else {
for _, header := range data.Headers {
| { header.Label }{ header.Indicator } |
}
for _, item := range data.Items {
{ 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 — first chart: average speed vs. Cotacol per 100 km; second chart: normalized difficulty (Cotacol per 100 km) vs. moving duration.
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 {
} else {
}
} else {
}
Chart
Normalized difficulty vs moving duration
if len(data.Points) > 0 {
{ formatRideCount(len(data.Points)) }
}
if len(data.Points) == 0 {
if data.Filter.Active {
} else {
}
} else {
}
@templ.JSONScript("analytics-points", data.Points)
}
templ RideDetailPage(data RideDetailView) {
@Layout("Ride detail", RideDetailContent(data))
}
templ RideDetailContent(data RideDetailView) {
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 {
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) }
⌄
}
}
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.
}
}
Elevation profile
Hover or focus the profile to inspect the ride at that distance.
}
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 {
Importing rides0 / 0
0 imported · 0 skipped
}
}
templ DataQualityPage(data DataQualityPageData) {
@Layout("Data quality", DataQualityContent(data))
}
templ DataQualityContent(data DataQualityPageData) {
Curation
Data quality
Categorise rides that are missing bike or ride type. Select rides, pick categories, and apply in bulk.
if data.Notice != "" {
{ data.Notice }
}
{ formatRideCount(data.Total) }
if data.Total == 0 {
All rides are categorised.
} else if len(data.Items) == 0 {
No uncategorised rides match the current view.
} else {
}
}