This commit is contained in:
Dominik Klarkowski
2026-06-18 11:02:31 +02:00
parent d00a5a42ac
commit 047e580da0
32 changed files with 735 additions and 189 deletions

View File

@@ -2,6 +2,7 @@
import { revalidatePath } from "next/cache";
import { generateAnalysis, generateDashboardAnalysis } from "@/lib/ai/claude";
import { getCurrentUserId } from "@/lib/session";
import type { AiAnalysisTargetType } from "@/lib/models/analysis";
export type GenerateAnalysisState = { error: string } | { success: true } | null;
@@ -10,8 +11,9 @@ export async function generateAnalysisAction(
targetType: AiAnalysisTargetType,
targetId: string
): Promise<GenerateAnalysisState> {
const userId = await getCurrentUserId();
try {
await generateAnalysis(targetType, targetId);
await generateAnalysis(userId, targetType, targetId);
} catch (error) {
return { error: error instanceof Error ? error.message : "Nie udało się wygenerować analizy." };
}
@@ -22,8 +24,9 @@ export async function generateAnalysisAction(
}
export async function generateDashboardAnalysisAction(): Promise<GenerateAnalysisState> {
const userId = await getCurrentUserId();
try {
await generateDashboardAnalysis();
await generateDashboardAnalysis(userId);
} catch (error) {
return { error: error instanceof Error ? error.message : "Nie udało się wygenerować analizy." };
}

View File

@@ -0,0 +1,3 @@
import { handlers } from "@/auth";
export const { GET, POST } = handlers;

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Nav } from "@/components/nav";
import { auth } from "@/auth";
import "./globals.css";
const geistSans = Geist({
@@ -18,18 +19,20 @@ export const metadata: Metadata = {
description: "Analiza treningów biegowych i siłowych",
};
export default function RootLayout({
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await auth();
return (
<html
lang="pl"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col bg-bg text-fg">
<Nav />
{session && <Nav />}
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-6 sm:px-6">
{children}
</main>

36
app/login/page.tsx Normal file
View File

@@ -0,0 +1,36 @@
import { signIn } from "@/auth";
export default function LoginPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-10">
<div className="flex flex-col items-center gap-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/logo.svg"
alt="KNUR"
width={160}
height={160}
className="rounded-3xl shadow-2xl ring-2 ring-surface"
/>
<h1 className="text-3xl font-bold tracking-tight text-fg">KNUR</h1>
<p className="text-sm text-fg/50">
Książka Notowań Udźwigów i Rezultatów
</p>
</div>
<form
action={async () => {
"use server";
await signIn("keycloak", { redirectTo: "/" });
}}
>
<button
type="submit"
className="rounded-lg bg-accent px-8 py-3 text-sm font-semibold text-white transition-opacity hover:opacity-90"
>
Zaloguj
</button>
</form>
</div>
);
}

View File

@@ -8,14 +8,17 @@ import { formatDateShort, formatDistance, formatDuration, formatPace } from "@/l
import { getDashboardAnalysis, serializeAnalysis } from "@/lib/models/analysis";
import { listRunningActivities } from "@/lib/models/running";
import { listStrengthWorkouts } from "@/lib/models/strength";
import { getCurrentUserId } from "@/lib/session";
export const dynamic = "force-dynamic";
export default async function Home() {
const userId = await getCurrentUserId();
const [runs, strengthWorkouts, dashboardAnalysis] = await Promise.all([
listRunningActivities(),
listStrengthWorkouts(),
getDashboardAnalysis(),
listRunningActivities(userId),
listStrengthWorkouts(userId),
getDashboardAnalysis(userId),
]);
const weekStart = startOfWeek(new Date(), { weekStartsOn: 1 });

View File

@@ -20,24 +20,29 @@ import {
type RunMetrics,
type RunningActivity,
} from "@/lib/models/running";
import { getCurrentUserId } from "@/lib/session";
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 }) {
async function RouteMapFetcher({
activity,
userId,
}: {
activity: RunningActivity;
userId: string;
}) {
let routePoints = activity.routePoints;
if ((!routePoints || !activity.elevationProfile) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const client = await getAuthorizedClient(userId);
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (result) {
await setRunningActivityRoutePoints(activity.garminActivityId, result.points, result.elevationProfile);
await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile);
routePoints = result.points;
}
} catch {
@@ -62,15 +67,21 @@ function hasValidElevation(profile: number[] | undefined): boolean {
return Array.isArray(profile) && profile.some((v) => v > 0);
}
async function ElevationFetcher({ activity }: { activity: RunningActivity }) {
async function ElevationFetcher({
activity,
userId,
}: {
activity: RunningActivity;
userId: string;
}) {
let elevationProfile = activity.elevationProfile;
if (!hasValidElevation(elevationProfile) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const client = await getAuthorizedClient(userId);
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (result) {
await setRunningActivityRoutePoints(activity.garminActivityId, result.points, result.elevationProfile);
await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile);
elevationProfile = result.elevationProfile;
}
} catch {
@@ -80,14 +91,23 @@ async function ElevationFetcher({ activity }: { activity: RunningActivity }) {
if (!elevationProfile || elevationProfile.length < 2) return null;
const data = elevationProfile
const elevData = 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;
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 <ElevationChart data={data} />;
}
@@ -102,18 +122,27 @@ function toChartData(
.filter((p) => p.value > 0);
}
async function RunMetricsFetcher({ activity }: { activity: RunningActivity }) {
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) && mayHaveRoute(activity)) {
if ((!metrics || missingCadence || missingGcb || missingPace) && mayHaveRoute(activity)) {
try {
const client = await getAuthorizedClient();
const client = await getAuthorizedClient(userId);
const fetched = await fetchActivityRunMetrics(client, activity.garminActivityId);
if (fetched) {
await setRunningActivityMetrics(activity.garminActivityId, fetched);
await setRunningActivityMetrics(userId, activity.garminActivityId, fetched);
metrics = fetched;
}
} catch {
@@ -125,7 +154,6 @@ async function RunMetricsFetcher({ activity }: { activity: RunningActivity }) {
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
@@ -151,12 +179,7 @@ async function RunMetricsFetcher({ activity }: { activity: RunningActivity }) {
return (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
{hrData.length > 1 && (
<RunMetricChart
data={hrData}
label="Tętno"
unit="bpm"
color="var(--color-accent)"
/>
<RunMetricChart data={hrData} label="Tętno" unit="bpm" color="var(--color-accent)" />
)}
{gcbData.length > 1 && <GcbChart data={gcbData} />}
</div>
@@ -175,13 +198,14 @@ export default async function RunningActivityPage({
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const activity = await getRunningActivity(id);
const userId = await getCurrentUserId();
const activity = await getRunningActivity(userId, id);
if (!activity) {
notFound();
}
const analysis = await getLatestAnalysisForTarget("running", activity._id);
const analysis = await getLatestAnalysisForTarget(userId, "running", activity._id);
return (
<div className="flex flex-col gap-6">
@@ -192,7 +216,7 @@ export default async function RunningActivityPage({
<section className="grid grid-cols-3 gap-4">
<Suspense fallback={<MapSkeleton />}>
<RouteMapFetcher activity={activity} />
<RouteMapFetcher activity={activity} userId={userId} />
</Suspense>
<StatCard highlight label="Dystans" value={formatDistance(activity.distanceM)} />
@@ -238,14 +262,18 @@ export default async function RunningActivityPage({
</section>
<Suspense fallback={null}>
<ElevationFetcher activity={activity} />
<ElevationFetcher activity={activity} userId={userId} />
</Suspense>
<Suspense fallback={null}>
<RunMetricsFetcher activity={activity} />
<RunMetricsFetcher activity={activity} userId={userId} />
</Suspense>
<AiAnalysisCard targetType="running" targetId={activity._id.toString()} analysis={analysis ? serializeAnalysis(analysis) : null} />
<AiAnalysisCard
targetType="running"
targetId={activity._id.toString()}
analysis={analysis ? serializeAnalysis(analysis) : null}
/>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import { revalidatePath } from "next/cache";
import type { GarminConnect } from "garmin-connect";
import {
GarminLoginRequiredError,
GarminCredentialsMissingError,
beginGarminLogin,
completeGarminMfaLogin,
fetchActivityRoutePoints,
@@ -23,19 +24,20 @@ import {
saveOauth1Token,
savePendingMfaState,
} from "@/lib/models/garmin-auth";
import { getCurrentUserId } from "@/lib/session";
export type SyncGarminState = { error: string } | { success: string } | { mfaRequired: true } | null;
async function syncWithClient(client: GarminConnect): Promise<SyncGarminState> {
const since = await getLastSyncAt();
async function syncWithClient(userId: string, client: GarminConnect): Promise<SyncGarminState> {
const since = await getLastSyncAt(userId);
const activities = await fetchRunningActivities(client);
const newCount = activities.filter((activity) => !since || activity.startTime > since).length;
for (const activity of activities) {
await upsertRunningActivity(activity);
await upsertRunningActivity(userId, activity);
}
await setLastSyncAt(new Date());
await setLastSyncAt(userId, new Date());
revalidatePath("/running");
revalidatePath("/settings");
@@ -45,39 +47,45 @@ async function syncWithClient(client: GarminConnect): Promise<SyncGarminState> {
}
export async function syncGarminActivities(): Promise<SyncGarminState> {
const userId = await getCurrentUserId();
try {
const client = await getAuthorizedClient();
return await syncWithClient(client);
const client = await getAuthorizedClient(userId);
return await syncWithClient(userId, client);
} catch (error) {
if (error instanceof GarminCredentialsMissingError) {
return { error: error.message };
}
if (!(error instanceof GarminLoginRequiredError)) {
return { error: error instanceof Error ? error.message : "Synchronizacja z Garmin nie powiodła się." };
}
}
try {
const result = await beginGarminLogin();
const result = await beginGarminLogin(userId);
if ("mfaRequired" in result) {
await savePendingMfaState(result.pendingState);
await savePendingMfaState(userId, result.pendingState);
return { mfaRequired: true };
}
await saveOauth1Token(result.oauth1Token);
return await syncWithClient(result.client);
await saveOauth1Token(userId, result.oauth1Token);
return await syncWithClient(userId, result.client);
} catch (error) {
return { error: error instanceof Error ? error.message : "Logowanie do Garmin nie powiodło się." };
}
}
export async function submitGarminMfaCode(code: string): Promise<SyncGarminState> {
const pending = await getPendingMfaState();
const userId = await getCurrentUserId();
const pending = await getPendingMfaState(userId);
if (!pending) {
return { error: "Sesja logowania do Garmin wygasła. Kliknij \"Synchronizuj z Garmin\" ponownie." };
}
try {
const result = await completeGarminMfaLogin(pending, code);
await saveOauth1Token(result.oauth1Token);
await clearPendingMfaState();
return await syncWithClient(result.client);
await saveOauth1Token(userId, result.oauth1Token);
await clearPendingMfaState(userId);
return await syncWithClient(userId, result.client);
} catch (error) {
return { error: error instanceof Error ? error.message : "Weryfikacja kodu MFA nie powiodła się." };
}
@@ -86,12 +94,13 @@ export async function submitGarminMfaCode(code: string): Promise<SyncGarminState
export type LoadRouteState = { error: string } | { success: true } | null;
export async function loadActivityRoute(activityMongoId: string): Promise<LoadRouteState> {
const activity = await getRunningActivity(activityMongoId);
const userId = await getCurrentUserId();
const activity = await getRunningActivity(userId, activityMongoId);
if (!activity) return { error: "Nie znaleziono aktywności." };
let client: GarminConnect;
try {
client = await getAuthorizedClient();
client = await getAuthorizedClient(userId);
} catch {
return { error: "Brak połączenia z Garmin Connect. Wykonaj synchronizację." };
}
@@ -99,7 +108,7 @@ export async function loadActivityRoute(activityMongoId: string): Promise<LoadRo
try {
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (!result) return { error: "Brak danych GPS dla tej aktywności." };
await setRunningActivityRoutePoints(activity.garminActivityId, result.points, result.elevationProfile);
await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile);
revalidatePath(`/running/${activityMongoId}`);
return { success: true };
} catch (error) {

View File

@@ -4,11 +4,13 @@ import { EmptyState } from "@/components/empty-state";
import { SyncButton } from "@/components/sync-button";
import { formatDateShort, formatDistance, formatDuration, formatPace } from "@/lib/format";
import { listRunningActivities } from "@/lib/models/running";
import { getCurrentUserId } from "@/lib/session";
export const dynamic = "force-dynamic";
export default async function RunningPage() {
const activities = await listRunningActivities();
const userId = await getCurrentUserId();
const activities = await listRunningActivities(userId);
return (
<div className="flex flex-col gap-6">

28
app/settings/actions.ts Normal file
View File

@@ -0,0 +1,28 @@
"use server";
import { revalidatePath } from "next/cache";
import { saveGarminCredentials } from "@/lib/models/garmin-auth";
import { getCurrentUserId } from "@/lib/session";
export type SaveGarminCredentialsState = { error: string } | { success: true } | null;
export async function saveGarminCredentialsAction(
_prevState: SaveGarminCredentialsState,
formData: FormData
): Promise<SaveGarminCredentialsState> {
const email = formData.get("email");
const password = formData.get("password");
if (typeof email !== "string" || !email.includes("@")) {
return { error: "Podaj prawidłowy adres e-mail." };
}
if (typeof password !== "string" || password.length < 1) {
return { error: "Podaj hasło." };
}
const userId = await getCurrentUserId();
await saveGarminCredentials(userId, email.trim(), password);
revalidatePath("/settings");
return { success: true };
}

View File

@@ -0,0 +1,58 @@
"use client";
import { useActionState } from "react";
import { saveGarminCredentialsAction, type SaveGarminCredentialsState } from "./actions";
export function GarminCredentialsForm({ savedEmail }: { savedEmail: string | null }) {
const [state, action, pending] = useActionState<SaveGarminCredentialsState, FormData>(
saveGarminCredentialsAction,
null
);
return (
<form action={action} className="flex flex-col gap-3">
<div className="flex flex-col gap-1.5">
<label htmlFor="garmin-email" className="text-sm text-fg/70">
E-mail Garmin Connect
</label>
<input
id="garmin-email"
name="email"
type="email"
defaultValue={savedEmail ?? ""}
placeholder="twoj@email.com"
required
className="rounded-md border border-muted/40 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent/60 focus:outline-none"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="garmin-password" className="text-sm text-fg/70">
Hasło Garmin Connect
</label>
<input
id="garmin-password"
name="password"
type="password"
placeholder="••••••••"
required
className="rounded-md border border-muted/40 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent/60 focus:outline-none"
/>
</div>
{state && "error" in state && (
<p className="text-sm text-red-400">{state.error}</p>
)}
{state && "success" in state && (
<p className="text-sm text-accent">Zapisano dane logowania.</p>
)}
<button
type="submit"
disabled={pending}
className="self-start rounded-md bg-accent px-4 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
>
{pending ? "Zapisywanie…" : "Zapisz"}
</button>
</form>
);
}

View File

@@ -2,22 +2,30 @@ import { CheckCircle2, XCircle } from "lucide-react";
import { SyncButton } from "@/components/sync-button";
import { formatDate } from "@/lib/format";
import { getLastSyncAt } from "@/lib/models/running";
import { getGarminCredentials, getSavedOauth1Token } from "@/lib/models/garmin-auth";
import { getCurrentUserId } from "@/lib/session";
import { GarminCredentialsForm } from "./garmin-credentials-form";
export const dynamic = "force-dynamic";
function ConfigRow({ label, configured }: { label: string; configured: boolean }) {
function StatusRow({ label, ok, okLabel = "Skonfigurowano", failLabel = "Brak konfiguracji" }: {
label: string;
ok: boolean;
okLabel?: string;
failLabel?: string;
}) {
return (
<div className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4">
<span className="text-fg">{label}</span>
{configured ? (
{ok ? (
<span className="flex items-center gap-1.5 text-sm text-fg/70">
<CheckCircle2 size={16} className="text-accent" />
Skonfigurowano
{okLabel}
</span>
) : (
<span className="flex items-center gap-1.5 text-sm text-fg/50">
<XCircle size={16} />
Brak w .env.local
{failLabel}
</span>
)}
</div>
@@ -25,31 +33,52 @@ function ConfigRow({ label, configured }: { label: string; configured: boolean }
}
export default async function SettingsPage() {
const lastSyncAt = await getLastSyncAt();
const mongoConfigured = Boolean(process.env.MONGODB_URI);
const garminConfigured = Boolean(process.env.GARMIN_EMAIL && process.env.GARMIN_PASSWORD);
const claudeConfigured = Boolean(process.env.ANTHROPIC_API_KEY);
const userId = await getCurrentUserId();
const [lastSyncAt, garminCreds, garminToken] = await Promise.all([
getLastSyncAt(userId),
getGarminCredentials(userId),
getSavedOauth1Token(userId),
]);
return (
<div className="flex flex-col gap-6">
<div>
<h1 className="text-2xl font-bold text-fg">Ustawienia</h1>
<p className="mt-1 text-sm text-fg/60">Status konfiguracji i synchronizacja Garmin.</p>
<p className="mt-1 text-sm text-fg/60">Konfiguracja konta i synchronizacja Garmin.</p>
</div>
<section className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-fg">Konfiguracja</h2>
<ConfigRow label="MongoDB" configured={mongoConfigured} />
<ConfigRow label="Garmin Connect" configured={garminConfigured} />
<ConfigRow label="Claude API" configured={claudeConfigured} />
<h2 className="text-lg font-semibold text-fg">Status</h2>
<StatusRow label="MongoDB" ok={Boolean(process.env.MONGODB_URI)} />
<StatusRow
label="Garmin Connect — token sesji"
ok={Boolean(garminToken)}
okLabel="Aktywny (synchronizacja działa)"
failLabel="Brak tokenu — wymagane logowanie"
/>
<StatusRow
label="Garmin Connect — dane logowania"
ok={Boolean(garminCreds)}
okLabel={`Zapisano (${garminCreds?.email})`}
failLabel="Brak — potrzebne gdy token wygaśnie"
/>
<StatusRow label="Claude API" ok={Boolean(process.env.ANTHROPIC_API_KEY)} />
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-fg">Konto Garmin Connect</h2>
<div className="rounded-lg border border-muted/40 bg-surface p-4">
<GarminCredentialsForm savedEmail={garminCreds?.email ?? null} />
</div>
</section>
<section className="flex flex-col gap-3">
<h2 className="text-lg font-semibold text-fg">Synchronizacja z Garmin</h2>
<div className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4">
<span className="text-sm text-fg/70">
{lastSyncAt ? `Ostatnia synchronizacja: ${formatDate(lastSyncAt)}` : "Jeszcze nie zsynchronizowano"}
{lastSyncAt
? `Ostatnia synchronizacja: ${formatDate(lastSyncAt)}`
: "Jeszcze nie zsynchronizowano"}
</span>
<SyncButton />
</div>

View File

@@ -6,6 +6,7 @@ import { formatDate, formatDateShort } from "@/lib/format";
import { getLatestAnalysisForTarget, serializeAnalysis } from "@/lib/models/analysis";
import { getStrengthWorkout, listStrengthWorkouts } from "@/lib/models/strength";
import { exerciseE1rm, getExerciseHistory } from "@/lib/strength/stats";
import { getCurrentUserId } from "@/lib/session";
export const dynamic = "force-dynamic";
@@ -17,14 +18,15 @@ export default async function StrengthWorkoutPage({
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const workout = await getStrengthWorkout(id);
const userId = await getCurrentUserId();
const workout = await getStrengthWorkout(userId, id);
if (!workout) {
notFound();
}
const analysis = await getLatestAnalysisForTarget("strength", workout._id);
const allWorkouts = await listStrengthWorkouts();
const analysis = await getLatestAnalysisForTarget(userId, "strength", workout._id);
const allWorkouts = await listStrengthWorkouts(userId);
const pastWorkouts = allWorkouts.filter((w) => w.date <= workout.date);
const exercisesWithHistory = workout.exercises.map((exercise) => ({
@@ -73,9 +75,9 @@ export default async function StrengthWorkoutPage({
{exercisesWithHistory.some(({ history }) => history.length >= 2) ? (
<div className="flex flex-col gap-3">
<h2 className="flex items-center gap-1.5 text-sm font-semibold text-fg/70">
Postęp ćwiczeń
<InfoTooltip text="Wolumen (ciężar × powtórzenia) i maksymalny ciężar na tle poprzednich sesji z tym samym ćwiczeniem." />
</h2>
Postęp ćwiczeń
<InfoTooltip text="Wolumen (ciężar × powtórzenia) i maksymalny ciężar na tle poprzednich sesji z tym samym ćwiczeniem." />
</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{exercisesWithHistory
.filter(({ history }) => history.length >= 2)

View File

@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { parseStrongShareText } from "@/lib/strong/parser";
import { upsertStrengthWorkout } from "@/lib/models/strength";
import { getCurrentUserId } from "@/lib/session";
export type ImportStrongWorkoutState = { error: string } | null;
@@ -27,8 +28,9 @@ export async function importStrongWorkout(
return { error: "Nie znaleziono żadnego treningu w podanym tekście." };
}
const userId = await getCurrentUserId();
for (const workout of workouts) {
await upsertStrengthWorkout(workout);
await upsertStrengthWorkout(userId, workout);
}
revalidatePath("/strength");

View File

@@ -5,13 +5,15 @@ import { VolumeChart } from "@/components/volume-chart";
import { formatDateShort } from "@/lib/format";
import { listStrengthWorkouts } from "@/lib/models/strength";
import { workoutVolumeKg } from "@/lib/strength/stats";
import { getCurrentUserId } from "@/lib/session";
export const dynamic = "force-dynamic";
const VOLUME_CHART_LIMIT = 12;
export default async function StrengthPage() {
const workouts = await listStrengthWorkouts();
const userId = await getCurrentUserId();
const workouts = await listStrengthWorkouts(userId);
const volumeData = workouts
.slice(0, VOLUME_CHART_LIMIT)