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";
export const dynamic = "force-dynamic";
// hasRoute may be undefined for activities synced before the field was added,
// even if routePoints are already in the DB.
function mayHaveRoute(activity: RunningActivity): boolean {
return Boolean(activity.hasRoute) || Boolean(activity.routePoints?.length);
}
async function RouteMapFetcher({ activity }: { activity: RunningActivity }) {
let routePoints = activity.routePoints;
if ((!routePoints || !activity.elevationProfile) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (result) {
await setRunningActivityRoutePoints(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 }: { activity: RunningActivity }) {
let elevationProfile = activity.elevationProfile;
if (!hasValidElevation(elevationProfile) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (result) {
await setRunningActivityRoutePoints(activity.garminActivityId, result.points, result.elevationProfile);
elevationProfile = result.elevationProfile;
}
} catch {
// silent
}
}
if (!elevationProfile || elevationProfile.length < 2) return null;
const data = elevationProfile
.map((altM, i) => ({
distanceKm: Math.round((i / elevationProfile!.length) * activity.distanceM / 10) / 100,
altM,
}))
.filter((p) => p.altM > 0);
if (data.length < 2) return null;
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 }: { activity: RunningActivity }) {
let metrics: RunMetrics | undefined = activity.runMetrics;
const missingCadence = activity.avgCadence && !metrics?.cadenceSpm;
const missingGcb = activity.avgGroundContactBalanceLeftPct && !metrics?.gcbLeftPct;
if ((!metrics || missingCadence || missingGcb) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const fetched = await fetchActivityRunMetrics(client, activity.garminActivityId);
if (fetched) {
await setRunningActivityMetrics(activity.garminActivityId, fetched);
metrics = fetched;
}
} catch {
// silent
}
}
if (!metrics || metrics.distanceKm.length === 0) return null;
const { hrBpm, gcbLeftPct } = metrics;
// Fall back to evenly-spaced distances if Garmin didn't provide them
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 activity = await getRunningActivity(id);
if (!activity) {
notFound();
}
const analysis = await getLatestAnalysisForTarget("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}
);
}