import { Suspense } from "react"; import { notFound } from "next/navigation"; import { AiAnalysisCard } from "@/components/ai-analysis-card"; import { ElevationChart } from "@/components/elevation-chart"; import { RouteMapSection } from "@/components/route-map-section"; import { GcbChart } from "@/components/gcb-chart"; import { RunMetricChart } from "@/components/run-metric-chart"; import { StatCard } from "@/components/stat-card"; import { formatDate, formatDistance, formatDuration, formatPace } from "@/lib/format"; import { fetchActivityRoutePoints, fetchActivityRunMetrics, getAuthorizedClient, } from "@/lib/garmin/client"; import { getLatestAnalysisForTarget, serializeAnalysis } from "@/lib/models/analysis"; import { getRunningActivity, setRunningActivityMetrics, setRunningActivityRoutePoints, type RunMetrics, type RunningActivity, } from "@/lib/models/running"; import { getCurrentUserId } from "@/lib/session"; export const dynamic = "force-dynamic"; function mayHaveRoute(activity: RunningActivity): boolean { return Boolean(activity.hasRoute) || Boolean(activity.routePoints?.length); } async function RouteMapFetcher({ activity, userId, }: { activity: RunningActivity; userId: string; }) { let routePoints = activity.routePoints; if ((!routePoints || !activity.elevationProfile) && mayHaveRoute(activity)) { try { const client = await getAuthorizedClient(userId); const result = await fetchActivityRoutePoints(client, activity.garminActivityId); if (result) { await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile); routePoints = result.points; } } catch { // GPS fetch failed silently } } return (
{routePoints && routePoints.length > 0 ? ( ) : (
Brak danych GPS
)}
); } function hasValidElevation(profile: number[] | undefined): boolean { return Array.isArray(profile) && profile.some((v) => v > 0); } async function ElevationFetcher({ activity, userId, }: { activity: RunningActivity; userId: string; }) { let elevationProfile = activity.elevationProfile; if (!hasValidElevation(elevationProfile) && mayHaveRoute(activity)) { try { const client = await getAuthorizedClient(userId); const result = await fetchActivityRoutePoints(client, activity.garminActivityId); if (result) { await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile); elevationProfile = result.elevationProfile; } } catch { // silent } } if (!elevationProfile || elevationProfile.length < 2) return null; const elevData = elevationProfile .map((altM, i) => ({ distanceKm: Math.round((i / elevationProfile!.length) * activity.distanceM / 10) / 100, altM, })) .filter((p) => p.altM > 0); if (elevData.length < 2) return null; // Merge pace by fractional position (both arrays span the same run, different sample counts) const paceSrc = activity.runMetrics?.paceSec; const data = elevData.map((ep, i) => { if (!paceSrc || paceSrc.length === 0) return ep; const pi = Math.min(Math.round((i / elevData.length) * paceSrc.length), paceSrc.length - 1); const v = paceSrc[pi]; return { ...ep, paceSec: v > 0 && v < 1800 ? v : undefined }; }); return ; } function toChartData( values: number[] | undefined, distances: number[] ): { distanceKm: number; value: number }[] { if (!values) return []; return distances .map((distanceKm, i) => ({ distanceKm, value: values[i] ?? 0 })) .filter((p) => p.value > 0); } async function RunMetricsFetcher({ activity, userId, }: { activity: RunningActivity; userId: string; }) { let metrics: RunMetrics | undefined = activity.runMetrics; const missingCadence = activity.avgCadence && !metrics?.cadenceSpm; const missingGcb = activity.avgGroundContactBalanceLeftPct && !metrics?.gcbLeftPct; // Re-fetch if paceSec missing or was computed from integer-rounded speeds (< 15 unique values = rounding artifact) const validPace = metrics?.paceSec?.filter((v) => v > 0) ?? []; const missingPace = validPace.length === 0 || new Set(validPace).size < 15; if ((!metrics || missingCadence || missingGcb || missingPace) && mayHaveRoute(activity)) { try { const client = await getAuthorizedClient(userId); const fetched = await fetchActivityRunMetrics(client, activity.garminActivityId); if (fetched) { await setRunningActivityMetrics(userId, activity.garminActivityId, fetched); metrics = fetched; } } catch { // silent } } if (!metrics || metrics.distanceKm.length === 0) return null; const { hrBpm, gcbLeftPct } = metrics; const maxDist = Math.max(...metrics.distanceKm); const distanceKm = maxDist > 0 ? metrics.distanceKm : Array.from({ length: metrics.distanceKm.length }, (_, i) => Math.round(((i / (metrics.distanceKm.length - 1)) * activity.distanceM) / 10) / 100 ); const hrData = toChartData(hrBpm, distanceKm); const gcbData = gcbLeftPct ? distanceKm .map((d, i) => { const left = gcbLeftPct[i] ?? 0; return left > 0 ? { distanceKm: d, left, right: Math.round((100 - left) * 10) / 10 } : null; }) .filter((p): p is NonNullable => p !== null) : []; if (!hrData.length && !gcbData.length) return null; return (
{hrData.length > 1 && ( )} {gcbData.length > 1 && }
); } function MapSkeleton() { return (
); } export default async function RunningActivityPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; const userId = await getCurrentUserId(); const activity = await getRunningActivity(userId, id); if (!activity) { notFound(); } const analysis = await getLatestAnalysisForTarget(userId, "running", activity._id); return (

{activity.name}

{formatDate(activity.startTime)}

}> {activity.maxHr ? : null} {activity.elevationGainM ? : null} {activity.vo2Max ? : null} {activity.avgGroundContactTimeMs ? ( ) : null} {activity.avgVerticalOscillationCm ? ( ) : null} {activity.avgVerticalRatioPct ? ( ) : null} {activity.avgStrideLengthCm ? ( ) : null} {activity.avgGroundContactBalanceLeftPct ? ( ) : null} {activity.avgPowerW ? : null} {activity.maxPowerW ? : null} {activity.avgRespirationRate ? ( ) : null} {activity.aerobicTrainingEffect ? ( ) : null} {activity.anaerobicTrainingEffect ? ( ) : null}
); }