import { useState } from 'react'
import { Link } from 'react-router-dom'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '../utils/api'
import ActivityMap from '../components/activity/ActivityMap'
import { formatDistance, formatDuration, formatDate, formatPace, sportIcon } from '../utils/format'
// Decode Google encoded polyline to [[lat,lng], ...]
function decodePolyline(encoded) {
if (!encoded) return []
const points = []
let idx = 0, lat = 0, lng = 0
while (idx < encoded.length) {
let shift = 0, result = 0, byte
do { byte = encoded.charCodeAt(idx++) - 63; result |= (byte & 0x1f) << shift; shift += 5 } while (byte >= 0x20)
lat += result & 1 ? ~(result >> 1) : result >> 1
shift = 0; result = 0
do { byte = encoded.charCodeAt(idx++) - 63; result |= (byte & 0x1f) << shift; shift += 5 } while (byte >= 0x20)
lng += result & 1 ? ~(result >> 1) : result >> 1
points.push([lat / 1e5, lng / 1e5])
}
return points
}
function RouteMap({ polyline, className = '', sportType = '' }) {
const pts = decodePolyline(polyline)
if (pts.length < 2) return (
{selected.reference_polyline
?
:
}
{editingName ? (
setNameInput(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter' && nameInput.trim()) renameRoute.mutate(nameInput.trim()) }}
autoFocus
className="bg-gray-800 border border-gray-700 rounded-lg px-2 py-1 text-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
) : (
{selected.name}
)}
{selected.sport_type && {selected.sport_type}}
{formatDistance(selected.distance_m)}
{selected.auto_detected && (
Auto-detected
)}
{/* Merge panel */}
{merging && (
Merge another route into this one
All activities from the selected route will be moved here, then the other route will be deleted.
{otherRoutes.length === 0 && (
No other {selected.sport_type} routes to merge with.
)}
)}
{/* Podium */}
{routeActivities?.length > 0 && (
{routeActivities.slice(0, 3).map((act, i) => (
{MEDALS[i]}
{formatDuration(act.duration_s)}
{formatDate(act.start_time)}
{i > 0 && crTime != null && (
+{formatDuration(act.duration_s - crTime)}
)}
))}
)}
{/* All completions */}
All completions ({routeActivities?.length ?? 0})
{routeActivities?.map((act, i) => {
const delta = crTime != null ? act.duration_s - crTime : null
return (
{i + 1}
{formatDate(act.start_time)}
{formatDuration(act.duration_s)}
{i === 0 ? 'CR' : delta != null ? `+${formatDuration(delta)}` : ''}
{formatPace(act.avg_speed_ms, selected.sport_type)}
{act.avg_heart_rate
? {Math.round(act.avg_heart_rate)} bpm
: }
→
)
})}
)
}
export default function RoutesPage() {
const [selected, setSelected] = useState(null)
const [showCreate, setShowCreate] = useState(false)
const [newRoute, setNewRoute] = useState({ name: '', activity_id: '' })
const qc = useQueryClient()
const { data: routes } = useQuery({
queryKey: ['routes'],
queryFn: () => api.get('/routes/').then(r => r.data),
})
// Sort by most completions first
const sortedRoutes = [...(routes || [])].sort((a, b) => (b.activity_count || 0) - (a.activity_count || 0))
const { data: recentActivities } = useQuery({
queryKey: ['recent-activities-for-route'],
queryFn: () => api.get('/routes/recent-activities').then(r => r.data),
enabled: showCreate,
})
const createRoute = useMutation({
mutationFn: data => api.post('/routes/', data).then(r => r.data),
onSuccess: route => {
qc.invalidateQueries({ queryKey: ['routes'] })
setShowCreate(false)
setNewRoute({ name: '', activity_id: '' })
setSelected(route)
},
})
return (
Named Routes
Routes are auto-detected when you run the same path twice. You can also create them manually.
{/* Create route panel */}
{showCreate && (
Create named route
Select an activity to use as the reference GPS track. Future activities on the same route will be linked automatically.
)}
{/* Route tile grid — selected route's detail expands inline under its row */}
{routes?.length === 0 && !showCreate ? (
🗺️
No named routes yet
Routes are created automatically when you repeat a run, or create one manually above.
) : (
{sortedRoutes.map(route => {
const style = routeSportStyle(route.sport_type)
const isSelected = selected?.id === route.id
return [
,
isSelected &&
,
]
})}
)}
)
}