Add per-route top-10 leaderboard to activity detail
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:
@@ -0,0 +1,101 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { formatDuration, formatDate } from '../../utils/format'
|
||||
|
||||
// Compact +M:SS / +SS gap label (fastest effort shows nothing).
|
||||
function gapLabel(gapS) {
|
||||
if (gapS == null || gapS <= 0.5) return null
|
||||
return `+${formatDuration(gapS)}`
|
||||
}
|
||||
|
||||
export default function RouteLeaderboard({ data }) {
|
||||
if (!data || !data.top || data.top.length === 0) return null
|
||||
|
||||
const { current, total, top } = data
|
||||
const currentGap = current ? gapLabel(current.gap_s) : null
|
||||
const inTop10 = current && current.rank <= 10
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
{/* This activity's standing on the route */}
|
||||
{current && (
|
||||
<div className="mb-3 rounded-lg border border-emerald-500/40 bg-emerald-500/10 px-3 py-2 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-emerald-300/80">This activity</div>
|
||||
<div className="text-lg font-semibold text-emerald-300 font-mono">
|
||||
{formatDuration(current.duration_s)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-medium text-gray-200">
|
||||
#{current.rank} <span className="text-gray-500">of {total}</span>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
{currentGap == null
|
||||
? <span className="text-yellow-400">🏆 Fastest</span>
|
||||
: <span className="text-gray-400">{currentGap} off fastest</span>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="text-xs text-gray-500 border-b border-gray-800">
|
||||
<th className="text-left pb-2 font-medium">#</th>
|
||||
<th className="text-left pb-2 font-medium">Date</th>
|
||||
<th className="text-right pb-2 font-medium">Time</th>
|
||||
<th className="text-right pb-2 font-medium">Δ</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{top.map((e) => {
|
||||
const gap = gapLabel(e.gap_s)
|
||||
return (
|
||||
<tr
|
||||
key={e.activity_id}
|
||||
className={`border-b border-gray-800/50 transition-colors ${
|
||||
e.is_current
|
||||
? 'bg-emerald-500/15 hover:bg-emerald-500/20'
|
||||
: 'hover:bg-gray-800/30'
|
||||
}`}
|
||||
>
|
||||
<td className={`py-2 ${e.rank === 1 ? 'text-yellow-400' : 'text-gray-400'}`}>
|
||||
{e.rank === 1 ? '🏆' : e.rank}
|
||||
</td>
|
||||
<td className="py-2">
|
||||
<Link
|
||||
to={`/activities/${e.activity_id}`}
|
||||
className={`hover:underline ${e.is_current ? 'text-emerald-300 font-medium' : 'text-gray-300'}`}
|
||||
>
|
||||
{formatDate(e.start_time)}
|
||||
</Link>
|
||||
</td>
|
||||
<td className={`py-2 text-right font-mono ${e.is_current ? 'text-emerald-300 font-semibold' : 'text-gray-200'}`}>
|
||||
{formatDuration(e.duration_s)}
|
||||
</td>
|
||||
<td className="py-2 text-right font-mono text-gray-500">
|
||||
{gap == null ? '--' : gap}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{/* If this activity ranks outside the top 10, still surface its row. */}
|
||||
{current && !inTop10 && (
|
||||
<tr className="border-t border-gray-700 bg-emerald-500/15">
|
||||
<td className="py-2 text-emerald-300">{current.rank}</td>
|
||||
<td className="py-2">
|
||||
<span className="text-emerald-300 font-medium">{formatDate(current.start_time)}</span>
|
||||
</td>
|
||||
<td className="py-2 text-right font-mono text-emerald-300 font-semibold">
|
||||
{formatDuration(current.duration_s)}
|
||||
</td>
|
||||
<td className="py-2 text-right font-mono text-gray-500">
|
||||
{gapLabel(current.gap_s) ?? '--'}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user