summaryrefslogtreecommitdiff
path: root/web/templates.templ
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-11 12:38:45 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-11 12:38:45 +0200
commitd4e4616ba49f3a18a52e3197e1c77639a93b0330 (patch)
tree50e9cc2b39f72ef8a2e61bff57fbc1239d55d4f0 /web/templates.templ
parent31ce2a1d5ff5746249cd4de96762f5740aff907a (diff)
feat: Add ride detail map
Diffstat (limited to 'web/templates.templ')
-rw-r--r--web/templates.templ66
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: '&copy; <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))
}