diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-11 12:38:45 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-11 12:38:45 +0200 |
| commit | d4e4616ba49f3a18a52e3197e1c77639a93b0330 (patch) | |
| tree | 50e9cc2b39f72ef8a2e61bff57fbc1239d55d4f0 /web/templates.templ | |
| parent | 31ce2a1d5ff5746249cd4de96762f5740aff907a (diff) | |
feat: Add ride detail map
Diffstat (limited to 'web/templates.templ')
| -rw-r--r-- | web/templates.templ | 66 |
1 files changed, 61 insertions, 5 deletions
diff --git a/web/templates.templ b/web/templates.templ index b061f1e..85ee03d 100644 --- a/web/templates.templ +++ b/web/templates.templ @@ -4,10 +4,11 @@ templ Layout(title string, content templ.Component) { <!doctype html> <html lang="en"> <head> - <meta charset="utf-8"/> - <meta name="viewport" content="width=device-width, initial-scale=1"/> - <title>{ title } · biking_home</title> - <style> + <meta charset="utf-8"/> + <meta name="viewport" content="width=device-width, initial-scale=1"/> + <title>{ title } · biking_home</title> + <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/> + <style> :root { color-scheme: light; font-family: system-ui, sans-serif; background: #f5f3ee; color: #1d2a22; } * { box-sizing: border-box; } body { margin: 0; } @@ -37,7 +38,13 @@ templ Layout(title string, content templ.Component) { .sort-indicator { display: inline-block; margin-left: .2rem; } .numeric { text-align: right; white-space: nowrap; } .empty { padding: 2.5rem 1rem; text-align: center; color: #68746c; } + .map-panel { padding: 0; overflow: hidden; } + .ride-map { min-height: 24rem; height: min(65vh, 40rem); } + dl { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 0; } + dt { color: #68746c; font-size: .78rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; } + dd { margin: .25rem 0 0; font-size: 1.2rem; font-weight: 700; } @media (max-width: 700px) { .container { width: min(100% - 1rem, 70rem); padding-top: 1.5rem; } .site-header { padding-inline: 1rem; } th:nth-child(n+4), td:nth-child(n+4) { display: none; } .panel { padding: .8rem; } } + @media (max-width: 500px) { dl { grid-template-columns: repeat(2, 1fr); } } </style> </head> <body> @@ -74,7 +81,7 @@ templ RidesContent(items []RideView, headers []RideSortHeader) { </tr></thead> <tbody> for _, item := range items { - <tr><td><strong>{ item.Name }</strong><br/><small>{ item.Type }</small></td><td>{ formatRideDate(item.StartDate) }</td><td class="numeric">{ formatDistance(item.DistanceM) }</td><td class="numeric">{ formatDuration(item.MovingTimeS) }</td><td class="numeric">{ formatElevation(item.TotalElevationGainM) }</td><td class="numeric">{ item.Cotacol }</td><td class="numeric">{ item.CotacolPer100Km }</td></tr> + <tr><td><strong><a href={ templ.URL(rideDetailURL(item.ID)) }>{ item.Name }</a></strong><br/><small>{ item.Type }</small></td><td>{ formatRideDate(item.StartDate) }</td><td class="numeric">{ formatDistance(item.DistanceM) }</td><td class="numeric">{ formatDuration(item.MovingTimeS) }</td><td class="numeric">{ formatElevation(item.TotalElevationGainM) }</td><td class="numeric">{ item.Cotacol }</td><td class="numeric">{ item.CotacolPer100Km }</td></tr> } </tbody> </table> @@ -82,6 +89,55 @@ templ RidesContent(items []RideView, headers []RideSortHeader) { </section> } +templ RideDetailPage(data RideDetailView) { + @Layout("Ride detail", RideDetailContent(data)) +} + +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> + <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 map-panel"> + <div id="ride-map" class="ride-map" role="img" aria-label="Recorded ride route map"></div> + </section> + @templ.JSONScript("ride-route", data.Route) + <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> + <script> + (() => { + const mapElement = document.getElementById("ride-map"); + const routeElement = document.getElementById("ride-route"); + const route = JSON.parse(routeElement.textContent); + const map = L.map(mapElement); + const tiles = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', + }); + tiles.addTo(map); + const routeLayer = L.geoJSON(route, { + style: { color: "#d96932", weight: 4, opacity: 0.9 }, + }).addTo(map); + const bounds = routeLayer.getBounds(); + if (bounds.isValid()) { + map.fitBounds(bounds, { padding: [24, 24], maxZoom: 15 }); + } + })(); + </script> + } +} + templ SyncPage(data SyncPageData) { @Layout("Sync Strava", SyncContent(data)) } |