All tweaks added
Build and push images / build-backend (push) Successful in 33s
Build and push images / build-worker (push) Successful in 32s
Build and push images / build-frontend (push) Failing after 6s

This commit is contained in:
2026-06-06 18:10:35 +01:00
parent 043b3b7269
commit ec5a01d12a
92 changed files with 7517 additions and 784 deletions
+64 -83
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import api from '../utils/api'
import { formatDistance, formatDuration, formatDate, formatPace } from '../utils/format'
import { formatDistance, formatDuration, formatDate, formatPace, sportIcon } from '../utils/format'
export default function RoutesPage() {
const [selected, setSelected] = useState(null)
@@ -20,18 +20,19 @@ export default function RoutesPage() {
enabled: !!selected,
})
const { data: segments } = useQuery({
queryKey: ['route-segments', selected?.id],
queryFn: () => api.get(`/routes/${selected.id}/segments`).then(r => r.data),
enabled: !!selected,
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: () => {
onSuccess: (route) => {
qc.invalidateQueries({ queryKey: ['routes'] })
setShowCreate(false)
setNewRoute({ name: '', activity_id: '' })
setSelected(route)
},
})
@@ -40,55 +41,62 @@ export default function RoutesPage() {
return (
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white">Named Routes</h1>
<button
onClick={() => setShowCreate(true)}
className="bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 rounded-lg transition-colors"
>
<div>
<h1 className="text-2xl font-bold text-white">Named Routes</h1>
<p className="text-xs text-gray-500 mt-1">
Routes are auto-detected when you run the same path twice. You can also create them manually.
</p>
</div>
<button onClick={() => setShowCreate(true)}
className="bg-blue-600 hover:bg-blue-700 text-white text-sm px-4 py-2 rounded-lg transition-colors">
+ New route
</button>
</div>
{/* Create route modal */}
{/* Create route */}
{showCreate && (
<div className="bg-gray-900 border border-gray-700 rounded-xl p-5 space-y-4">
<h3 className="text-sm font-semibold text-white">Create named route</h3>
<p className="text-xs text-gray-500">
Pick an activity to use as the reference GPS track. Future activities on the same route will be linked automatically.
Select an activity to use as the reference GPS track. Future activities on the same route will be linked automatically.
</p>
<div className="grid grid-cols-2 gap-3">
<div className="space-y-3">
<div>
<label className="text-xs text-gray-400 mb-1 block">Route name</label>
<input
value={newRoute.name}
<input value={newRoute.name}
onChange={e => setNewRoute(r => ({ ...r, name: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Morning park loop"
/>
placeholder="e.g. Morning park loop" />
</div>
<div>
<label className="text-xs text-gray-400 mb-1 block">Reference activity ID</label>
<input
type="number"
value={newRoute.activity_id}
onChange={e => setNewRoute(r => ({ ...r, activity_id: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Activity ID"
/>
<label className="text-xs text-gray-400 mb-1 block">Reference activity (last 2 weeks)</label>
{recentActivities?.length === 0 ? (
<p className="text-xs text-gray-600 py-2">No recent activities found.</p>
) : (
<select
value={newRoute.activity_id}
onChange={e => setNewRoute(r => ({ ...r, activity_id: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select an activity</option>
{recentActivities?.map(a => (
<option key={a.id} value={a.id}>
{sportIcon(a.sport_type)} {a.name} {formatDistance(a.distance_m)} on {formatDate(a.start_time)}
</option>
))}
</select>
)}
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => createRoute.mutate({ ...newRoute, activity_id: parseInt(newRoute.activity_id) })}
disabled={!newRoute.name || !newRoute.activity_id}
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-40 text-white text-sm px-4 py-2 rounded-lg transition-colors"
>
disabled={!newRoute.name || !newRoute.activity_id || createRoute.isPending}
className="bg-blue-600 hover:bg-blue-700 disabled:opacity-40 text-white text-sm px-4 py-2 rounded-lg transition-colors">
Create
</button>
<button
onClick={() => setShowCreate(false)}
className="text-gray-400 hover:text-white text-sm px-4 py-2 rounded-lg transition-colors"
>
<button onClick={() => setShowCreate(false)}
className="text-gray-400 hover:text-white text-sm px-4 py-2 rounded-lg transition-colors">
Cancel
</button>
</div>
@@ -98,23 +106,24 @@ export default function RoutesPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Route list */}
<div className="space-y-2">
{routes?.length === 0 && (
{routes?.length === 0 && !showCreate && (
<div className="text-center py-12 text-gray-600">
<p className="text-3xl mb-2">🗺</p>
<p className="text-sm">No named routes yet</p>
<p className="text-xs mt-1">Routes are created automatically when you repeat a run, or create one manually above.</p>
</div>
)}
{routes?.map(route => (
<button
key={route.id}
onClick={() => setSelected(route)}
<button key={route.id} onClick={() => setSelected(route)}
className={`w-full text-left p-4 rounded-xl border transition-all ${
selected?.id === route.id
? 'bg-blue-900/20 border-blue-700'
: 'bg-gray-900 border-gray-800 hover:border-gray-600'
}`}
>
<p className="font-medium text-white">{route.name}</p>
selected?.id === route.id ? 'bg-blue-900/20 border-blue-700' : 'bg-gray-900 border-gray-800 hover:border-gray-600'
}`}>
<div className="flex items-start justify-between">
<p className="font-medium text-white">{route.name}</p>
{route.auto_detected && (
<span className="text-xs bg-gray-800 text-gray-400 px-2 py-0.5 rounded-full ml-2">auto</span>
)}
</div>
<div className="flex gap-3 mt-1 text-xs text-gray-500">
<span>{formatDistance(route.distance_m)}</span>
{route.sport_type && <span className="capitalize">{route.sport_type}</span>}
@@ -128,19 +137,20 @@ export default function RoutesPage() {
{selected && (
<div className="lg:col-span-2 space-y-4">
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5">
<h2 className="text-lg font-semibold text-white mb-1">{selected.name}</h2>
{selected.description && (
<p className="text-sm text-gray-400 mb-3">{selected.description}</p>
)}
<div className="flex items-start justify-between mb-3">
<h2 className="text-lg font-semibold text-white">{selected.name}</h2>
{selected.auto_detected && (
<span className="text-xs bg-blue-900/40 text-blue-400 border border-blue-700/40 px-2 py-0.5 rounded-full">
Auto-detected
</span>
)}
</div>
{/* CR */}
{fastest && (
<div className="bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-3 mb-4">
<p className="text-xs text-yellow-600 mb-1">Course record</p>
<p className="text-xs text-yellow-600 mb-1">Course record 🏆</p>
<div className="flex items-center gap-4">
<span className="text-xl font-bold text-yellow-400">
{formatDuration(fastest.duration_s)}
</span>
<span className="text-xl font-bold text-yellow-400">{formatDuration(fastest.duration_s)}</span>
<span className="text-sm text-gray-400">
{formatDate(fastest.start_time)} · {formatPace(fastest.avg_speed_ms, selected.sport_type)}
</span>
@@ -148,16 +158,12 @@ export default function RoutesPage() {
</div>
)}
{/* All runs on route */}
<h3 className="text-sm font-medium text-gray-400 mb-2">
All runs ({routeActivities?.length ?? 0})
</h3>
<div className="space-y-2">
{routeActivities?.map((act, i) => (
<div
key={act.id}
className="flex items-center gap-4 py-2 border-b border-gray-800/50 text-sm"
>
<div key={act.id} className="flex items-center gap-4 py-2 border-b border-gray-800/50 text-sm">
<span className="text-gray-600 w-5 text-right">{i + 1}</span>
<span className="text-gray-400 flex-1">{formatDate(act.start_time)}</span>
<span className="font-mono text-white font-medium">{formatDuration(act.duration_s)}</span>
@@ -166,37 +172,12 @@ export default function RoutesPage() {
<span className="text-red-400 text-xs">{Math.round(act.avg_heart_rate)} bpm</span>
)}
{i === 0 && (
<span className="text-xs bg-yellow-900/40 text-yellow-400 px-2 py-0.5 rounded-full border border-yellow-700/40">
CR
</span>
<span className="text-xs bg-yellow-900/40 text-yellow-400 px-2 py-0.5 rounded-full border border-yellow-700/40">CR</span>
)}
</div>
))}
</div>
</div>
{/* Segments */}
{segments && segments.length > 0 && (
<div className="bg-gray-900 rounded-xl border border-gray-800 p-5">
<h3 className="text-sm font-medium text-gray-300 mb-3">Segments</h3>
<div className="space-y-2">
{segments.map(seg => (
<div key={seg.id} className="flex items-center justify-between py-2 border-b border-gray-800/50">
<div>
<p className="text-sm font-medium text-white">{seg.name}</p>
{seg.description && (
<p className="text-xs text-gray-500">{seg.description}</p>
)}
</div>
<div className="text-xs text-gray-400 text-right">
<p>{formatDistance(seg.start_distance_m)} {formatDistance(seg.end_distance_m)}</p>
<p>{formatDistance(seg.end_distance_m - seg.start_distance_m)}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>