init
This commit is contained in:
@@ -19,7 +19,7 @@ export function AiAnalysisCard({ targetType, targetId, analysis }: AiAnalysisCar
|
||||
);
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-3 rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<section className="flex flex-col gap-3 rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="flex items-center gap-1.5 text-lg font-semibold text-fg">
|
||||
<Sparkles size={18} className="text-accent" />
|
||||
@@ -29,7 +29,7 @@ export function AiAnalysisCard({ targetType, targetId, analysis }: AiAnalysisCar
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="rounded-md bg-accent px-3 py-1.5 text-sm font-semibold text-fg transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{pending ? "Generowanie..." : analysis ? "Wygeneruj ponownie" : "Generuj analizę"}
|
||||
</button>
|
||||
|
||||
@@ -98,7 +98,7 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
|
||||
const dayWorkouts = workouts.filter((w) => isSameDay(new Date(w.date), selectedDate));
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-4 rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<section className="flex flex-col gap-4 rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="flex items-center gap-1.5 text-lg font-semibold text-fg">
|
||||
<Sparkles size={18} className="text-accent" />
|
||||
@@ -109,7 +109,7 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="rounded-md bg-accent px-3 py-1.5 text-sm font-semibold text-fg transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{pending ? "Analizuję..." : selectedAnalysis ? "Odśwież analizę" : "Generuj analizę"}
|
||||
</button>
|
||||
@@ -216,7 +216,7 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
|
||||
<li key={run.id}>
|
||||
<Link
|
||||
href={`/running/${run.id}`}
|
||||
className="flex items-center gap-2 rounded-md border border-muted/40 bg-bg/40 px-2.5 py-1.5 transition-colors hover:border-accent/60"
|
||||
className="flex items-center gap-2 rounded-md border border-white/10 bg-bg/40 px-2.5 py-1.5 transition-colors hover:border-accent/60"
|
||||
>
|
||||
<Activity size={14} className="shrink-0 text-accent" />
|
||||
<span className="min-w-0 flex-1">
|
||||
@@ -235,7 +235,7 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
|
||||
<li key={workout.id}>
|
||||
<Link
|
||||
href={`/strength/${workout.id}`}
|
||||
className="flex items-center gap-2 rounded-md border border-muted/40 bg-bg/40 px-2.5 py-1.5 transition-colors hover:border-accent/60"
|
||||
className="flex items-center gap-2 rounded-md border border-white/10 bg-bg/40 px-2.5 py-1.5 transition-colors hover:border-accent/60"
|
||||
>
|
||||
<Dumbbell size={14} className="shrink-0 text-accent" />
|
||||
<span className="min-w-0 flex-1">
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ElevationChart({ data, syncId }: Props) {
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="h-[140px] animate-pulse rounded-lg border border-muted/40 bg-surface" />;
|
||||
return <div className="h-[140px] animate-pulse rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md" />;
|
||||
}
|
||||
|
||||
const altitudes = data.map((p) => p.altM);
|
||||
@@ -46,15 +46,15 @@ export function ElevationChart({ data, syncId }: Props) {
|
||||
const pacePad = Math.max(5, Math.round((maxPace - minPace) * 0.15));
|
||||
|
||||
const tooltipStyle = {
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted/40 bg-surface p-4" style={{ minWidth: 0 }}>
|
||||
<div className="rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4" style={{ minWidth: 0 }}>
|
||||
<div className="mb-2 flex items-center gap-4 text-sm text-fg/60">
|
||||
<span>Profil wysokości</span>
|
||||
{hasPace && (
|
||||
@@ -73,7 +73,7 @@ export function ElevationChart({ data, syncId }: Props) {
|
||||
<stop offset="95%" stopColor="var(--color-accent)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="var(--color-muted)" opacity={0.3} vertical={false} />
|
||||
<CartesianGrid stroke="rgba(255,255,255,0.08)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="distanceKm"
|
||||
stroke="var(--color-fg)"
|
||||
|
||||
@@ -10,14 +10,14 @@ type EmptyStateProps = {
|
||||
|
||||
export function EmptyState({ title, description, action, icon }: EmptyStateProps) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-muted/40 px-6 py-10 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-2 rounded-2xl border border-dashed border-white/10 px-6 py-10 text-center">
|
||||
{icon ? <div className="text-fg/40">{icon}</div> : null}
|
||||
<div className="text-base font-semibold text-fg">{title}</div>
|
||||
{description ? <p className="max-w-sm text-sm text-fg/60">{description}</p> : null}
|
||||
{action ? (
|
||||
<Link
|
||||
href={action.href}
|
||||
className="mt-2 rounded-md bg-accent px-4 py-2 text-sm font-semibold text-fg transition-opacity hover:opacity-90"
|
||||
className="btn btn-primary mt-2"
|
||||
>
|
||||
{action.label}
|
||||
</Link>
|
||||
|
||||
@@ -34,22 +34,22 @@ export function ExerciseProgressChart({ name, data }: ExerciseProgressChartProps
|
||||
|
||||
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 className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 h-4 w-32 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-[150px] animate-pulse rounded bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 flex items-baseline justify-between gap-2">
|
||||
<span className="text-sm text-fg/60">{name}</span>
|
||||
<E1rmDelta data={data} />
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={150}>
|
||||
<LineChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
<CartesianGrid stroke="var(--color-muted)" opacity={0.3} vertical={false} />
|
||||
<CartesianGrid stroke="rgba(255,255,255,0.08)" vertical={false} />
|
||||
<XAxis dataKey="label" stroke="var(--color-fg)" opacity={0.5} fontSize={12} />
|
||||
<YAxis yAxisId="volume" stroke="var(--color-accent)" opacity={0.7} fontSize={12} width={48} />
|
||||
<YAxis
|
||||
@@ -62,9 +62,9 @@ export function ExerciseProgressChart({ name, data }: ExerciseProgressChartProps
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
@@ -25,11 +25,11 @@ export function GcbChart({ data, syncId }: Props) {
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="h-[160px] animate-pulse rounded-lg border border-muted/40 bg-surface" />;
|
||||
return <div className="h-[160px] animate-pulse rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 flex items-center gap-4 text-sm text-fg/60">
|
||||
<span>Balans kontaktu</span>
|
||||
<span className="flex items-center gap-1">
|
||||
@@ -53,7 +53,7 @@ export function GcbChart({ data, syncId }: Props) {
|
||||
<stop offset="95%" stopColor="var(--color-sand)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="var(--color-muted)" opacity={0.3} vertical={false} />
|
||||
<CartesianGrid stroke="rgba(255,255,255,0.08)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="distanceKm"
|
||||
stroke="var(--color-fg)"
|
||||
@@ -73,9 +73,9 @@ export function GcbChart({ data, syncId }: Props) {
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
@@ -6,7 +6,7 @@ export function InfoTooltip({ text }: { text: string }) {
|
||||
return (
|
||||
<span className="group relative inline-flex items-center">
|
||||
<Info size={13} className="cursor-default text-fg/40 transition-colors group-hover:text-fg/70" />
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 w-56 -translate-x-1/2 rounded-md border border-muted/40 bg-bg px-3 py-2 text-xs text-fg/70 opacity-0 shadow-lg transition-opacity group-hover:opacity-100">
|
||||
<span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 w-56 -translate-x-1/2 rounded-md border border-white/10 bg-bg px-3 py-2 text-xs text-fg/70 opacity-0 shadow-lg transition-opacity group-hover:opacity-100">
|
||||
{text}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function LoadRouteButton({ activityId }: { activityId: string }) {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="flex items-center gap-1.5 rounded-md border border-muted/40 bg-surface px-3 py-2 text-sm font-medium text-fg/80 transition-colors hover:border-accent/60 hover:text-accent disabled:opacity-50"
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
<Map size={15} className={pending ? "animate-pulse" : ""} />
|
||||
{pending ? "Pobieranie mapy..." : "Załaduj mapę trasy"}
|
||||
|
||||
43
components/nav-links.tsx
Normal file
43
components/nav-links.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Activity, Dumbbell, LayoutDashboard, Settings } from "lucide-react";
|
||||
|
||||
const links = [
|
||||
{ href: "/", label: "Panel", icon: LayoutDashboard },
|
||||
{ href: "/running", label: "Bieganie", icon: Activity },
|
||||
{ href: "/strength", label: "Siłownia", icon: Dumbbell },
|
||||
{ href: "/settings", label: "Ustawienia", icon: Settings },
|
||||
];
|
||||
|
||||
function isActive(pathname: string, href: string) {
|
||||
return href === "/" ? pathname === "/" : pathname.startsWith(href);
|
||||
}
|
||||
|
||||
export function NavLinks() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<>
|
||||
{links.map(({ href, label, icon: Icon }) => {
|
||||
const active = isActive(pathname, href);
|
||||
return (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
aria-current={active ? "page" : undefined}
|
||||
className={`flex items-center gap-1.5 rounded-full px-2.5 py-1.5 text-sm font-medium transition-colors sm:px-3.5 ${
|
||||
active
|
||||
? "bg-accent/15 text-accent"
|
||||
: "text-fg/70 hover:bg-white/5 hover:text-fg"
|
||||
}`}
|
||||
>
|
||||
<Icon size={16} />
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import Link from "next/link";
|
||||
import { Activity, Dumbbell, LayoutDashboard, Settings } from "lucide-react";
|
||||
import { auth, signOut } from "@/auth";
|
||||
import { NavLinks } from "./nav-links";
|
||||
import { SignOutButton } from "./sign-out-button";
|
||||
|
||||
const links = [
|
||||
{ href: "/", label: "Panel", icon: LayoutDashboard },
|
||||
{ href: "/running", label: "Bieganie", icon: Activity },
|
||||
{ href: "/strength", label: "Siłownia", icon: Dumbbell },
|
||||
{ href: "/settings", label: "Ustawienia", icon: Settings },
|
||||
];
|
||||
import { Wordmark } from "./wordmark";
|
||||
|
||||
export async function Nav() {
|
||||
const session = await auth();
|
||||
@@ -19,33 +13,21 @@ export async function Nav() {
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="border-b border-muted/40 bg-surface">
|
||||
<header className="sticky top-0 z-50 border-b border-white/10 bg-surface/70 backdrop-blur-xl">
|
||||
<div className="mx-auto flex max-w-5xl items-center justify-between px-4 py-3 sm:px-6">
|
||||
<Link href="/" className="flex items-center gap-3 text-lg font-bold text-fg">
|
||||
<Link href="/" className="flex items-center gap-2.5 text-lg font-bold text-fg">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/logo-icon.svg"
|
||||
alt=""
|
||||
width={72}
|
||||
height={72}
|
||||
className="-mt-1 -mb-6 h-[72px] w-[72px] rounded-2xl shadow-lg ring-2 ring-surface"
|
||||
src="/logo-bars.svg"
|
||||
alt="KNUR.APP"
|
||||
width={51}
|
||||
height={48}
|
||||
className="h-7 w-auto sm:h-8"
|
||||
/>
|
||||
<span className="hidden text-2xl tracking-tight text-white [font-family:var(--font-space-grotesk)] sm:block">
|
||||
<span className="font-bold [-webkit-text-stroke:0.7px_white]">KNUR</span>
|
||||
<span className="font-medium text-accent">.APP</span>
|
||||
</span>
|
||||
<Wordmark className="hidden text-2xl sm:block" />
|
||||
</Link>
|
||||
<nav className="flex items-center gap-1 sm:gap-2">
|
||||
{links.map(({ href, label, icon: Icon }) => (
|
||||
<Link
|
||||
key={href}
|
||||
href={href}
|
||||
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-fg/80 transition-colors hover:bg-bg hover:text-accent sm:px-3"
|
||||
>
|
||||
<Icon size={16} />
|
||||
<span className="hidden sm:inline">{label}</span>
|
||||
</Link>
|
||||
))}
|
||||
<NavLinks />
|
||||
{session?.user?.name && (
|
||||
<span className="hidden px-2 text-xs text-fg/40 sm:inline">
|
||||
{session.user.name}
|
||||
|
||||
@@ -25,22 +25,21 @@ export function NotesEditor({ initialNotes, onSave }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="flex flex-col gap-2 rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<label className="text-sm font-medium text-fg/60">Notatki</label>
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => { setValue(e.target.value); setSaved(false); }}
|
||||
placeholder="Dodaj notatki do tego treningu…"
|
||||
rows={3}
|
||||
className="w-full resize-none rounded border border-muted/40 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent focus:outline-none"
|
||||
className="w-full resize-none rounded border border-white/10 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent focus:outline-none"
|
||||
/>
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
{saved && <span className="text-xs text-fg/40">Zapisano</span>}
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!dirty || isPending}
|
||||
className="rounded px-3 py-1.5 text-xs font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40"
|
||||
style={{ background: "var(--color-accent)", color: "#fff" }}
|
||||
className="btn btn-primary px-3 py-1.5 text-xs"
|
||||
>
|
||||
{isPending ? "Zapisuję…" : "Zapisz"}
|
||||
</button>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { RoutePoint } from "@/lib/models/running";
|
||||
|
||||
const RouteMap = nextDynamic(() => import("@/components/route-map").then((m) => m.RouteMap), {
|
||||
ssr: false,
|
||||
loading: () => <div className="h-full w-full animate-pulse rounded-lg bg-surface" />,
|
||||
loading: () => <div className="h-full w-full animate-pulse rounded-2xl bg-surface" />,
|
||||
});
|
||||
|
||||
export function RouteMapSection({ points }: { points: RoutePoint[] }) {
|
||||
|
||||
@@ -42,7 +42,7 @@ export function RunMetricChart({
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
return <div className="h-[160px] animate-pulse rounded-lg border border-muted/40 bg-surface" />;
|
||||
return <div className="h-[160px] animate-pulse rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md" />;
|
||||
}
|
||||
|
||||
const values = data.map((p) => p.value);
|
||||
@@ -59,7 +59,7 @@ export function RunMetricChart({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 text-sm text-fg/60">{label}</div>
|
||||
<ResponsiveContainer width="100%" height={120}>
|
||||
<AreaChart syncId={syncId} data={data} margin={{ top: 4, right: 8, left: 0, bottom: 0 }}>
|
||||
@@ -69,7 +69,7 @@ export function RunMetricChart({
|
||||
<stop offset="95%" stopColor={color} stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke="var(--color-muted)" opacity={0.3} vertical={false} />
|
||||
<CartesianGrid stroke="rgba(255,255,255,0.08)" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="distanceKm"
|
||||
stroke="var(--color-fg)"
|
||||
@@ -89,9 +89,9 @@ export function RunMetricChart({
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
@@ -25,15 +25,15 @@ export function RunningStatsChart({ data }: Props) {
|
||||
|
||||
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-[220px] animate-pulse rounded bg-muted/20" />
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 h-4 w-56 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-[220px] animate-pulse rounded bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 text-sm text-fg/60">Tygodniowy dystans biegowy (od poniedziałku)</div>
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<AreaChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
@@ -48,9 +48,9 @@ export function RunningStatsChart({ data }: Props) {
|
||||
<Tooltip
|
||||
cursor={{ stroke: "var(--color-accent)", strokeWidth: 1 }}
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
@@ -7,7 +7,7 @@ export function SignOutButton({ action }: { action: () => Promise<void> }) {
|
||||
<form action={action}>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm font-medium text-fg/80 transition-colors hover:bg-bg hover:text-accent sm:px-3"
|
||||
className="flex items-center gap-1.5 rounded-full px-2.5 py-1.5 text-sm font-medium text-fg/70 transition-colors hover:bg-white/5 hover:text-fg sm:px-3.5"
|
||||
title="Wyloguj się"
|
||||
>
|
||||
<LogOut size={16} />
|
||||
|
||||
@@ -10,16 +10,25 @@ type StatCardProps = {
|
||||
export function StatCard({ label, value, hint, highlight }: StatCardProps) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
className={`group relative overflow-hidden rounded-2xl border bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4 transition-all duration-200 hover:-translate-y-0.5 ${
|
||||
highlight
|
||||
? "rounded-lg border border-l-2 border-muted/30 border-l-accent/70 bg-surface p-4"
|
||||
: "rounded-lg border border-muted/40 bg-surface px-3 py-2.5"
|
||||
}
|
||||
? "border-accent/30 shadow-accent/5"
|
||||
: "border-white/10 hover:border-white/20"
|
||||
}`}
|
||||
>
|
||||
<div className={highlight ? "text-xs font-medium uppercase tracking-widest text-fg/50" : "text-xs text-fg/55"}>
|
||||
{highlight && (
|
||||
<div className="pointer-events-none absolute -right-8 -top-10 h-24 w-24 rounded-full bg-accent/20 blur-2xl" />
|
||||
)}
|
||||
<div className="text-xs font-medium uppercase tracking-widest text-fg/50">
|
||||
{label}
|
||||
</div>
|
||||
<div className={highlight ? "mt-1.5 text-2xl font-bold text-fg" : "mt-0.5 text-base font-semibold text-fg"}>{value}</div>
|
||||
<div
|
||||
className={`mt-1.5 font-display font-bold tabular-nums text-fg ${
|
||||
highlight ? "text-2xl" : "text-xl"
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
{hint ? <div className="mt-1 text-xs text-fg/50">{hint}</div> : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ export function SyncButton() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="flex items-center gap-1.5 rounded-md bg-accent px-3 py-2 text-sm font-semibold text-fg transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
<RefreshCw size={16} className={pending ? "animate-spin" : ""} />
|
||||
{pending ? "Synchronizowanie..." : "Synchronizuj z Garmin"}
|
||||
@@ -36,12 +36,12 @@ export function SyncButton() {
|
||||
maxLength={6}
|
||||
placeholder="Kod z e-maila"
|
||||
required
|
||||
className="w-32 rounded-md border border-muted/40 bg-bg px-2 py-1.5 text-sm text-fg"
|
||||
className="w-32 rounded-md border border-white/10 bg-bg px-2 py-1.5 text-sm text-fg"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={mfaPending}
|
||||
className="rounded-md bg-accent px-3 py-1.5 text-sm font-semibold text-fg transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{mfaPending ? "Weryfikacja..." : "Zatwierdź kod"}
|
||||
</button>
|
||||
|
||||
@@ -13,15 +13,15 @@ export function VolumeChart({ data }: VolumeChartProps) {
|
||||
|
||||
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 className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 h-4 w-56 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-[180px] animate-pulse rounded bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 text-sm text-fg/60">Wolumen treningowy (ciężar × powtórzenia)</div>
|
||||
<ResponsiveContainer width="100%" height={180}>
|
||||
<BarChart data={data} margin={{ top: 8, right: 8, left: 0, bottom: 0 }}>
|
||||
@@ -30,9 +30,9 @@ export function VolumeChart({ data }: VolumeChartProps) {
|
||||
<Tooltip
|
||||
cursor={{ fill: "var(--color-bg)" }}
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
@@ -38,9 +38,9 @@ export function VolumeTrendChart({ data }: Props) {
|
||||
|
||||
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-[220px] animate-pulse rounded bg-muted/20" />
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 h-4 w-56 animate-pulse rounded bg-white/10" />
|
||||
<div className="h-[220px] animate-pulse rounded bg-white/5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export function VolumeTrendChart({ data }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<div className="w-full rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
|
||||
<div className="mb-2 text-sm text-fg/60">
|
||||
Trend wolumenu treningowego wg typu
|
||||
</div>
|
||||
@@ -75,9 +75,9 @@ export function VolumeTrendChart({ data }: Props) {
|
||||
<Tooltip
|
||||
cursor={{ fill: "var(--color-bg)" }}
|
||||
contentStyle={{
|
||||
background: "var(--color-bg)",
|
||||
border: "1px solid var(--color-muted)",
|
||||
borderRadius: 8,
|
||||
background: "color-mix(in oklab, var(--color-surface) 92%, transparent)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
borderRadius: 12,
|
||||
fontSize: 12,
|
||||
color: "var(--color-fg)",
|
||||
}}
|
||||
|
||||
10
components/wordmark.tsx
Normal file
10
components/wordmark.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
export function Wordmark({ className }: { className?: string }) {
|
||||
return (
|
||||
<span
|
||||
className={`tracking-tight text-white [font-family:var(--font-space-grotesk)] ${className ?? ""}`}
|
||||
>
|
||||
<span className="font-bold [-webkit-text-stroke:0.7px_white]">KNUR</span>
|
||||
<span className="font-medium text-accent">.APP</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user