This commit is contained in:
Dominik Klarkowski
2026-06-22 11:32:27 +02:00
parent bf8624c954
commit d908965c95
16 changed files with 642 additions and 103 deletions

View File

@@ -1,93 +1,118 @@
import Link from "next/link";
import { startOfWeek } from "date-fns";
import { Activity, Dumbbell } from "lucide-react";
import { DashboardAnalysisCard } from "@/components/dashboard-analysis-card";
import { EmptyState } from "@/components/empty-state";
import { RunningStatsChart } from "@/components/running-stats-chart";
import { StatCard } from "@/components/stat-card";
import { formatDateShort, formatDistance, formatDuration, formatPace } from "@/lib/format";
import { getDashboardAnalysis, serializeAnalysis } from "@/lib/models/analysis";
import { VolumeTrendChart } from "@/components/volume-trend-chart";
import { formatDateShort } from "@/lib/format";
import { listAnalysesForCalendar, serializeAnalysis } from "@/lib/models/analysis";
import { listRunningActivities } from "@/lib/models/running";
import { listStrengthWorkouts } from "@/lib/models/strength";
import { workoutVolumeKg } from "@/lib/strength/stats";
import { getCurrentUserId } from "@/lib/session";
export const dynamic = "force-dynamic";
const TREND_LIMIT = 20;
export default async function Home() {
const userId = await getCurrentUserId();
const [runs, strengthWorkouts, dashboardAnalysis] = await Promise.all([
const [runs, strengthWorkouts, allAnalyses] = await Promise.all([
listRunningActivities(userId),
listStrengthWorkouts(userId),
getDashboardAnalysis(userId),
listAnalysesForCalendar(userId),
]);
const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 });
const weeklyKm = runs
.filter((run) => run.startTime >= weekStart)
.reduce((sum, run) => sum + run.distanceM, 0) / 1000;
const weeklyStrengthSessions = strengthWorkouts.filter((workout) => workout.date >= weekStart).length;
const weeklyKm =
runs.filter((run) => run.startTime >= weekStart).reduce((sum, run) => sum + run.distanceM, 0) /
1000;
const weeklyStrengthSessions = strengthWorkouts.filter(
(workout) => workout.date >= weekStart
).length;
const latestRun = runs[0];
const latestStrength = strengthWorkouts[0];
const totalRuns = runs.length;
const totalDistanceKm = runs.reduce((sum, r) => sum + r.distanceM, 0) / 1000;
const totalStrengthWorkouts = strengthWorkouts.length;
const volumeTrendData = strengthWorkouts
.slice(0, TREND_LIMIT)
.map((w) => ({
label: formatDateShort(w.date),
date: w.date.toISOString().slice(0, 10),
name: w.name,
volumeKg: workoutVolumeKg(w),
}))
.reverse();
// Aggregate running distance per ISO week (Mon-based).
const weeklyDistanceM = new Map<number, number>();
for (const r of runs) {
const weekStartTs = startOfWeek(r.startTime, { weekStartsOn: 1 }).getTime();
weeklyDistanceM.set(weekStartTs, (weeklyDistanceM.get(weekStartTs) ?? 0) + r.distanceM);
}
const runTrendData = [...weeklyDistanceM.entries()]
.sort((a, b) => a[0] - b[0])
.slice(-TREND_LIMIT)
.map(([ts, meters]) => ({
label: formatDateShort(new Date(ts)),
distanceKm: Math.round((meters / 1000) * 100) / 100,
}));
const serializedAnalyses = allAnalyses.map(serializeAnalysis);
const dayRuns = runs.map((r) => ({
id: r._id.toString(),
name: r.name,
startTime: r.startTime.toISOString(),
distanceM: r.distanceM,
durationSec: r.durationSec,
avgPaceSecPerKm: r.avgPaceSecPerKm,
}));
const dayWorkouts = strengthWorkouts.map((w) => ({
id: w._id.toString(),
name: w.name,
date: w.date.toISOString(),
exerciseCount: w.exercises.length,
}));
return (
<div className="flex flex-col gap-8">
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<StatCard label="Kilometry w tym tygodniu" value={`${weeklyKm.toFixed(1)} km`} hint="Bieganie" />
<StatCard label="Treningi siłowe w tym tygodniu" value={weeklyStrengthSessions} hint="Siłownia" />
<section className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard label="Km w tym tygodniu" value={`${weeklyKm.toFixed(1)} km`} hint="Bieganie" />
<StatCard
label="Siłownia w tym tygodniu"
value={weeklyStrengthSessions}
hint="Treningi"
/>
<StatCard
label="Łączny dystans"
value={`${totalDistanceKm.toFixed(0)} km`}
hint={`${totalRuns} biegów`}
/>
<StatCard
label="Treningi siłowe"
value={totalStrengthWorkouts}
hint="Łącznie"
/>
</section>
<section className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-fg">Ostatni bieg</h2>
{latestRun ? (
<Link
href={`/running/${latestRun._id.toString()}`}
className="flex flex-col gap-2 rounded-lg border border-muted/40 bg-surface p-4 transition-colors hover:border-accent/60"
>
<div className="font-semibold text-fg">{latestRun.name}</div>
<div className="text-sm text-fg/60">{formatDateShort(latestRun.startTime)}</div>
<div className="text-sm text-fg/70">
{formatDistance(latestRun.distanceM)} · {formatDuration(latestRun.durationSec)} ·{" "}
{formatPace(latestRun.avgPaceSecPerKm)}
</div>
</Link>
) : (
<EmptyState
icon={<Activity size={32} />}
title="Brak danych o bieganiu"
description="Zsynchronizuj aktywności z Garmin Connect, aby zobaczyć tutaj swoje biegi."
action={{ href: "/running", label: "Przejdź do biegania" }}
/>
)}
</div>
<div className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-fg">Ostatni trening siłowy</h2>
{latestStrength ? (
<Link
href={`/strength/${latestStrength._id.toString()}`}
className="flex flex-col gap-2 rounded-lg border border-muted/40 bg-surface p-4 transition-colors hover:border-accent/60"
>
<div className="font-semibold text-fg">{latestStrength.name}</div>
<div className="text-sm text-fg/60">{formatDateShort(latestStrength.date)}</div>
<div className="text-sm text-fg/70">
{latestStrength.exercises.length}{" "}
{latestStrength.exercises.length === 1 ? "ćwiczenie" : "ćwiczeń"}
</div>
</Link>
) : (
<EmptyState
icon={<Dumbbell size={32} />}
title="Brak treningów siłowych"
description="Zaimportuj trening wklejając tekst wygenerowany przez aplikację Strong."
action={{ href: "/strength/import", label: "Zaimportuj trening" }}
/>
)}
</div>
</section>
{(volumeTrendData.length > 1 || runTrendData.length > 1) && (
<section className="flex flex-col gap-4">
<h2 className="text-lg font-semibold text-fg">Globalne statystyki</h2>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
{volumeTrendData.length > 1 && <VolumeTrendChart data={volumeTrendData} />}
{runTrendData.length > 1 && <RunningStatsChart data={runTrendData} />}
</div>
</section>
)}
<DashboardAnalysisCard analysis={dashboardAnalysis ? serializeAnalysis(dashboardAnalysis) : null} />
<DashboardAnalysisCard
analyses={serializedAnalyses}
runs={dayRuns}
workouts={dayWorkouts}
/>
</div>
);
}