init
This commit is contained in:
@@ -5,7 +5,7 @@ import { DashboardAnalysisCard } from "@/components/dashboard-analysis-card";
|
|||||||
import { EmptyState } from "@/components/empty-state";
|
import { EmptyState } from "@/components/empty-state";
|
||||||
import { StatCard } from "@/components/stat-card";
|
import { StatCard } from "@/components/stat-card";
|
||||||
import { formatDateShort, formatDistance, formatDuration, formatPace } from "@/lib/format";
|
import { formatDateShort, formatDistance, formatDuration, formatPace } from "@/lib/format";
|
||||||
import { getDashboardAnalysis } from "@/lib/models/analysis";
|
import { getDashboardAnalysis, serializeAnalysis } from "@/lib/models/analysis";
|
||||||
import { listRunningActivities } from "@/lib/models/running";
|
import { listRunningActivities } from "@/lib/models/running";
|
||||||
import { listStrengthWorkouts } from "@/lib/models/strength";
|
import { listStrengthWorkouts } from "@/lib/models/strength";
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export default async function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<DashboardAnalysisCard analysis={dashboardAnalysis} />
|
<DashboardAnalysisCard analysis={dashboardAnalysis ? serializeAnalysis(dashboardAnalysis) : null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { RouteMapSection } from "@/components/route-map-section";
|
|||||||
import { StatCard } from "@/components/stat-card";
|
import { StatCard } from "@/components/stat-card";
|
||||||
import { formatDate, formatDistance, formatDuration, formatPace } from "@/lib/format";
|
import { formatDate, formatDistance, formatDuration, formatPace } from "@/lib/format";
|
||||||
import { fetchActivityRoutePoints, getAuthorizedClient } from "@/lib/garmin/client";
|
import { fetchActivityRoutePoints, getAuthorizedClient } from "@/lib/garmin/client";
|
||||||
import { getLatestAnalysisForTarget } from "@/lib/models/analysis";
|
import { getLatestAnalysisForTarget, serializeAnalysis } from "@/lib/models/analysis";
|
||||||
import { getRunningActivity, setRunningActivityRoutePoints, type RunningActivity } from "@/lib/models/running";
|
import { getRunningActivity, setRunningActivityRoutePoints, type RunningActivity } from "@/lib/models/running";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -117,7 +117,7 @@ export default async function RunningActivityPage({
|
|||||||
) : null}
|
) : null}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AiAnalysisCard targetType="running" targetId={activity._id.toString()} analysis={analysis} />
|
<AiAnalysisCard targetType="running" targetId={activity._id.toString()} analysis={analysis ? serializeAnalysis(analysis) : null} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AiAnalysisCard } from "@/components/ai-analysis-card";
|
|||||||
import { ExerciseProgressChart } from "@/components/exercise-progress-chart";
|
import { ExerciseProgressChart } from "@/components/exercise-progress-chart";
|
||||||
import { InfoTooltip } from "@/components/info-tooltip";
|
import { InfoTooltip } from "@/components/info-tooltip";
|
||||||
import { formatDate, formatDateShort } from "@/lib/format";
|
import { formatDate, formatDateShort } from "@/lib/format";
|
||||||
import { getLatestAnalysisForTarget } from "@/lib/models/analysis";
|
import { getLatestAnalysisForTarget, serializeAnalysis } from "@/lib/models/analysis";
|
||||||
import { getStrengthWorkout, listStrengthWorkouts } from "@/lib/models/strength";
|
import { getStrengthWorkout, listStrengthWorkouts } from "@/lib/models/strength";
|
||||||
import { exerciseE1rm, getExerciseHistory } from "@/lib/strength/stats";
|
import { exerciseE1rm, getExerciseHistory } from "@/lib/strength/stats";
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ export default async function StrengthWorkoutPage({
|
|||||||
{workout.notes ? <p className="mt-1.5 text-sm text-fg/70">{workout.notes}</p> : null}
|
{workout.notes ? <p className="mt-1.5 text-sm text-fg/70">{workout.notes}</p> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AiAnalysisCard targetType="strength" targetId={workout._id.toString()} analysis={analysis} />
|
<AiAnalysisCard targetType="strength" targetId={workout._id.toString()} analysis={analysis ? serializeAnalysis(analysis) : null} />
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3">
|
<div className="grid grid-cols-2 gap-2.5 sm:grid-cols-3">
|
||||||
{exercisesWithHistory.map(({ exercise }, index) => {
|
{exercisesWithHistory.map(({ exercise }, index) => {
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { useActionState } from "react";
|
|||||||
import { Sparkles } from "lucide-react";
|
import { Sparkles } from "lucide-react";
|
||||||
import { generateAnalysisAction } from "@/app/ai/actions";
|
import { generateAnalysisAction } from "@/app/ai/actions";
|
||||||
import { formatDate } from "@/lib/format";
|
import { formatDate } from "@/lib/format";
|
||||||
import type { AiAnalysis, AiAnalysisTargetType } from "@/lib/models/analysis";
|
import type { AiAnalysisTargetType, SerializedAiAnalysis } from "@/lib/models/analysis";
|
||||||
|
|
||||||
type AiAnalysisCardProps = {
|
type AiAnalysisCardProps = {
|
||||||
targetType: AiAnalysisTargetType;
|
targetType: AiAnalysisTargetType;
|
||||||
targetId: string;
|
targetId: string;
|
||||||
analysis: AiAnalysis | null;
|
analysis: SerializedAiAnalysis | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AiAnalysisCard({ targetType, targetId, analysis }: AiAnalysisCardProps) {
|
export function AiAnalysisCard({ targetType, targetId, analysis }: AiAnalysisCardProps) {
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import { useActionState } from "react";
|
|||||||
import { Sparkles } from "lucide-react";
|
import { Sparkles } from "lucide-react";
|
||||||
import { generateDashboardAnalysisAction } from "@/app/ai/actions";
|
import { generateDashboardAnalysisAction } from "@/app/ai/actions";
|
||||||
import { formatDate } from "@/lib/format";
|
import { formatDate } from "@/lib/format";
|
||||||
import type { AiAnalysis } from "@/lib/models/analysis";
|
import type { SerializedAiAnalysis } from "@/lib/models/analysis";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analysis: AiAnalysis | null;
|
analysis: SerializedAiAnalysis | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DashboardAnalysisCard({ analysis }: Props) {
|
export function DashboardAnalysisCard({ analysis }: Props) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
type ExerciseProgressChartProps = {
|
type ExerciseProgressChartProps = {
|
||||||
@@ -26,8 +27,18 @@ function E1rmDelta({ data }: { data: ExerciseProgressChartProps["data"] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ExerciseProgressChart({ name, data }: ExerciseProgressChartProps) {
|
export function ExerciseProgressChart({ name, data }: ExerciseProgressChartProps) {
|
||||||
if (data.length < 2) {
|
const [mounted, setMounted] = useState(false);
|
||||||
return null;
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
if (data.length < 2) return null;
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||||
|
<div className="mb-2 h-4 w-32 animate-pulse rounded bg-muted/30" />
|
||||||
|
<div className="h-[150px] animate-pulse rounded bg-muted/20" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
import { Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
|
||||||
|
|
||||||
type VolumeChartProps = {
|
type VolumeChartProps = {
|
||||||
@@ -7,6 +8,18 @@ type VolumeChartProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function VolumeChart({ data }: VolumeChartProps) {
|
export function VolumeChart({ data }: VolumeChartProps) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
useEffect(() => setMounted(true), []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||||
|
<div className="mb-2 h-4 w-56 animate-pulse rounded bg-muted/30" />
|
||||||
|
<div className="h-[180px] animate-pulse rounded bg-muted/20" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||||
<div className="mb-2 text-sm text-fg/60">Wolumen treningowy (ciężar × powtórzenia)</div>
|
<div className="mb-2 text-sm text-fg/60">Wolumen treningowy (ciężar × powtórzenia)</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { pl } from "date-fns/locale";
|
import { pl } from "date-fns/locale";
|
||||||
|
|
||||||
export function formatDate(date: Date): string {
|
export function formatDate(date: Date | string): string {
|
||||||
return format(date, "d MMMM yyyy, HH:mm", { locale: pl });
|
return format(new Date(date), "d MMMM yyyy, HH:mm", { locale: pl });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDateShort(date: Date): string {
|
export function formatDateShort(date: Date | string): string {
|
||||||
return format(date, "d MMM yyyy", { locale: pl });
|
return format(new Date(date), "d MMM yyyy", { locale: pl });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDuration(seconds: number): string {
|
export function formatDuration(seconds: number): string {
|
||||||
|
|||||||
@@ -16,6 +16,28 @@ export type AiAnalysis = AiAnalysisInput & {
|
|||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SerializedAiAnalysis = {
|
||||||
|
_id: string;
|
||||||
|
targetType: AiAnalysisTargetType;
|
||||||
|
targetId: string;
|
||||||
|
summary: string;
|
||||||
|
tips: string[];
|
||||||
|
model: string;
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function serializeAnalysis(analysis: AiAnalysis): SerializedAiAnalysis {
|
||||||
|
return {
|
||||||
|
_id: analysis._id.toString(),
|
||||||
|
targetType: analysis.targetType,
|
||||||
|
targetId: analysis.targetId.toString(),
|
||||||
|
summary: analysis.summary,
|
||||||
|
tips: analysis.tips,
|
||||||
|
model: analysis.model,
|
||||||
|
createdAt: analysis.createdAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const COLLECTION = "ai_analyses";
|
const COLLECTION = "ai_analyses";
|
||||||
|
|
||||||
async function getCollection() {
|
async function getCollection() {
|
||||||
|
|||||||
Reference in New Issue
Block a user