Add per-route top-10 leaderboard to activity detail
Build and push images / validate (push) Successful in 2s
Build and push images / build-backend (push) Successful in 5s
Build and push images / build-worker (push) Successful in 4s
Build and push images / build-frontend (push) Successful in 9s

New /activities/{id}/route-leaderboard endpoint ranks the user's timed
efforts on the same route; frontend RouteLeaderboard card sits beside
Laps, showing this activity's time/rank/gap and the top 10 (current
effort highlighted green, also surfaced if outside the top 10).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:37:37 +01:00
parent bdd5f80c7e
commit d350e9caea
3 changed files with 179 additions and 2 deletions
+15 -2
View File
@@ -7,6 +7,7 @@ import MetricTimeline from '../components/activity/MetricTimeline'
import HRZoneBar from '../components/activity/HRZoneBar'
import LapTable from '../components/activity/LapTable'
import SegmentsPanel from '../components/activity/SegmentsPanel'
import RouteLeaderboard from '../components/activity/RouteLeaderboard'
import StatCard from '../components/ui/StatCard'
import {
formatDuration, formatDistance, formatPace, formatElevation,
@@ -74,6 +75,12 @@ export default function ActivityDetailPage() {
enabled: !!activity?.named_route_id,
})
const { data: routeBoard } = useQuery({
queryKey: ['route-leaderboard', id],
queryFn: () => api.get(`/activities/${id}/route-leaderboard`).then(r => r.data),
enabled: !!activity?.named_route_id,
})
const handleMapClick = ({ lat, lng }) => {
if (!segCreate || !dataPoints) return
const dist = nearestDistance(dataPoints, lat, lng)
@@ -293,8 +300,8 @@ export default function ActivityDetailPage() {
)}
</div>
{/* Laps + Segments side by side */}
{((laps && laps.length > 0) || (actSegments && actSegments.length > 0)) && (
{/* Laps + Route leaderboard + Segments side by side */}
{((laps && laps.length > 0) || (actSegments && actSegments.length > 0) || (routeBoard && routeBoard.top?.length > 0)) && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{laps && laps.length > 0 && (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
@@ -302,6 +309,12 @@ export default function ActivityDetailPage() {
<LapTable laps={laps} sportType={activity.sport_type} lapBests={lapBests} />
</div>
)}
{routeBoard && routeBoard.top?.length > 0 && (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<h3 className="text-sm font-medium text-gray-300 mb-3">Route Top 10 Times</h3>
<RouteLeaderboard data={routeBoard} />
</div>
)}
{actSegments && actSegments.length > 0 && (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
<h3 className="text-sm font-medium text-gray-300 mb-3">Segments</h3>