Initial Commit

This commit is contained in:
2026-06-06 13:23:33 +01:00
commit 1a0d45dd67
58 changed files with 5268 additions and 0 deletions
+177
View File
@@ -0,0 +1,177 @@
import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Link } from 'react-router-dom'
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
import { format } from 'date-fns'
import api from '../utils/api'
import { formatDuration, formatDate } from '../utils/format'
const SPORTS = ['running', 'cycling', 'swimming']
const DISTANCE_ORDER = [
'400m', '800m', '1k', '1 mile', '3k', '5k', '10k',
'Half marathon', 'Marathon', '50k', '100k',
]
export default function RecordsPage() {
const [sport, setSport] = useState('running')
const [selectedDistance, setSelectedDistance] = useState(null)
const { data: records } = useQuery({
queryKey: ['records', sport],
queryFn: () => api.get('/records/', { params: { sport_type: sport } }).then(r => r.data),
})
const { data: history } = useQuery({
queryKey: ['record-history', selectedDistance, sport],
queryFn: () =>
api.get(`/records/history/${encodeURIComponent(selectedDistance)}`, {
params: { sport_type: sport },
}).then(r => r.data),
enabled: !!selectedDistance,
})
// Sort by standard distance order
const sortedRecords = records?.slice().sort((a, b) => {
const ai = DISTANCE_ORDER.indexOf(a.distance_label)
const bi = DISTANCE_ORDER.indexOf(b.distance_label)
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi)
})
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold text-white">Personal Records</h1>
{/* Sport selector */}
<div className="flex gap-2">
{SPORTS.map(s => (
<button
key={s}
onClick={() => { setSport(s); setSelectedDistance(null) }}
className={`capitalize text-sm px-4 py-1.5 rounded-full border transition-colors ${
sport === s
? 'bg-blue-600 border-blue-600 text-white'
: 'border-gray-700 text-gray-400 hover:text-white'
}`}
>
{s}
</button>
))}
</div>
{sortedRecords?.length === 0 && (
<div className="text-center py-16 text-gray-600">
<p className="text-4xl mb-3">🏆</p>
<p>No records yet import activities to track your best times</p>
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Records table */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="text-xs text-gray-500 border-b border-gray-800 bg-gray-900/80">
<th className="text-left px-4 py-3 font-medium">Distance</th>
<th className="text-right px-4 py-3 font-medium">Best time</th>
<th className="text-right px-4 py-3 font-medium">Date</th>
<th className="px-4 py-3" />
</tr>
</thead>
<tbody>
{sortedRecords?.map(rec => (
<tr
key={rec.id}
onClick={() => setSelectedDistance(rec.distance_label)}
className={`border-b border-gray-800/50 cursor-pointer transition-colors ${
selectedDistance === rec.distance_label
? 'bg-blue-900/20'
: 'hover:bg-gray-800/40'
}`}
>
<td className="px-4 py-3 font-medium text-white">{rec.distance_label}</td>
<td className="px-4 py-3 text-right font-mono text-yellow-400 font-semibold">
{formatDuration(rec.duration_s)}
</td>
<td className="px-4 py-3 text-right text-gray-400 text-xs">
{formatDate(rec.achieved_at)}
</td>
<td className="px-4 py-3 text-right">
<Link
to={`/activities/${rec.activity_id}`}
onClick={e => e.stopPropagation()}
className="text-xs text-blue-400 hover:underline"
>
View
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Progress chart */}
<div className="bg-gray-900 rounded-xl border border-gray-800 p-4">
{selectedDistance && history ? (
<>
<h3 className="text-sm font-medium text-gray-300 mb-1">
{selectedDistance} progression
</h3>
<p className="text-xs text-gray-600 mb-4">Lower is faster</p>
{history.length > 1 ? (
<ResponsiveContainer width="100%" height={220}>
<LineChart
data={history.map(h => ({
date: h.achieved_at,
time: h.duration_s,
}))}
margin={{ top: 4, right: 4, bottom: 4, left: 8 }}
>
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" vertical={false} />
<XAxis
dataKey="date"
tick={{ fontSize: 10, fill: '#6b7280' }}
axisLine={false}
tickLine={false}
tickFormatter={d => format(new Date(d), 'MMM yy')}
/>
<YAxis
tick={{ fontSize: 10, fill: '#6b7280' }}
axisLine={false}
tickLine={false}
width={40}
tickFormatter={formatDuration}
reversed
/>
<Tooltip
contentStyle={{ background: '#111827', border: '1px solid #374151', borderRadius: 8, fontSize: 12 }}
labelFormatter={d => format(new Date(d), 'MMM d, yyyy')}
formatter={v => [formatDuration(v), 'Time']}
/>
<Line
type="monotone"
dataKey="time"
stroke="#fbbf24"
strokeWidth={2}
dot={{ fill: '#fbbf24', r: 4 }}
isAnimationActive={false}
/>
</LineChart>
</ResponsiveContainer>
) : (
<div className="flex items-center justify-center h-48 text-gray-600 text-sm">
Only one record complete more activities to see progression
</div>
)}
</>
) : (
<div className="flex items-center justify-center h-full text-gray-600 text-sm">
Select a distance to see your progression
</div>
)}
</div>
</div>
</div>
)
}