Compare commits

..

3 Commits

Author SHA1 Message Date
Dominik Klarkowski
16b3767e43 init 2026-06-23 08:08:35 +02:00
Dominik Klarkowski
4b3373869b init 2026-06-22 15:19:38 +02:00
Dominik Klarkowski
763321ec68 init 2026-06-22 14:28:05 +02:00
36 changed files with 322 additions and 157 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -20,6 +20,7 @@
--color-sand: var(--color-sand);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--font-display: var(--font-space-grotesk);
}
body {
@@ -27,3 +28,87 @@ body {
color: var(--color-fg);
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
}
/* Ambient accent glow behind everything — modern dark-UI depth. */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
radial-gradient(60rem 40rem at 15% -10%, color-mix(in oklab, var(--color-accent) 14%, transparent), transparent 70%),
radial-gradient(50rem 40rem at 100% 0%, color-mix(in oklab, var(--color-secondary) 45%, transparent), transparent 65%);
}
/* Display font for all headings. */
h1,
h2,
h3 {
font-family: var(--font-display), var(--font-sans), sans-serif;
letter-spacing: -0.015em;
}
/* Tabular figures for stats so numbers line up. */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
/* Confine Leaflet's internal panes (z-index 4001000) to their own stacking
context so the map never overlaps the sticky navbar. */
.leaflet-container {
z-index: 0;
}
/* Unified button system. In @layer components so Tailwind utility overrides
(e.g. px-8, text-xs, w-full) still win over these base styles. */
@layer components {
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
border-radius: 0.75rem;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 600;
line-height: 1.25rem;
white-space: nowrap;
cursor: pointer;
transition: transform 0.12s ease, background-color 0.15s ease,
border-color 0.15s ease, color 0.15s ease, box-shadow 0.15s ease,
filter 0.15s ease;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-primary {
background: var(--color-accent);
color: #fff;
box-shadow: 0 6px 18px -6px color-mix(in oklab, var(--color-accent) 70%, transparent);
}
.btn-primary:hover:not(:disabled) {
filter: brightness(1.08);
transform: translateY(-1px);
}
.btn-primary:active:not(:disabled) {
filter: brightness(0.96);
transform: translateY(0);
}
.btn-secondary {
background: color-mix(in oklab, var(--color-surface) 55%, transparent);
border: 1px solid rgba(255, 255, 255, 0.12);
color: color-mix(in oklab, var(--color-fg) 88%, transparent);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
}
.btn-secondary:hover:not(:disabled) {
border-color: color-mix(in oklab, var(--color-accent) 55%, transparent);
color: var(--color-accent);
transform: translateY(-1px);
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 7.6 KiB

After

Width:  |  Height:  |  Size: 450 B

View File

@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, Space_Grotesk } from "next/font/google";
import { Nav } from "@/components/nav";
import { auth } from "@/auth";
import "./globals.css";
@@ -14,6 +14,11 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const spaceGrotesk = Space_Grotesk({
variable: "--font-space-grotesk",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "KNUR - Książka Notowań Udźwigów i Rezultatów",
description: "Analiza treningów biegowych i siłowych",
@@ -29,7 +34,7 @@ export default async function RootLayout({
return (
<html
lang="pl"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${spaceGrotesk.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col bg-bg text-fg">
{session && <Nav />}

View File

@@ -1,36 +1,31 @@
import { signIn } from "@/auth";
import { Wordmark } from "@/components/wordmark";
export default function LoginPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center gap-10">
<div className="flex min-h-screen flex-col items-center justify-center px-4">
<div className="flex w-full max-w-sm flex-col items-center gap-7 rounded-2xl border border-white/10 bg-surface/55 p-8 shadow-lg shadow-black/10 backdrop-blur-md">
<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">
<img src="/logo-bars.svg" alt="" width={51} height={48} className="h-12 w-auto" />
<Wordmark className="text-3xl" />
<p className="text-center text-sm text-fg/50">
Książka Notowań Udźwigów i Rezultatów
</p>
</div>
<form
className="w-full"
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"
>
<button type="submit" className="btn btn-primary w-full px-8 py-3 text-base">
Zaloguj
</button>
</form>
</div>
</div>
);
}

View File

@@ -42,7 +42,7 @@ function buildGrid(maxDistKm: number): number[] {
function RouteMap({ activity }: { activity: RunningActivity }) {
const routePoints = activity.routePoints;
return (
<div className="col-span-2 row-span-3 min-h-[240px] overflow-hidden rounded-lg border border-muted/40">
<div className="col-span-2 row-span-3 min-h-[240px] overflow-hidden rounded-2xl border border-white/10">
{routePoints && routePoints.length > 0 ? (
<RouteMapSection points={routePoints} />
) : (

View File

@@ -34,7 +34,7 @@ export default async function RunningPage() {
<li key={activity._id.toString()}>
<Link
href={`/running/${activity._id.toString()}`}
className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4 transition-colors hover:border-accent/60"
className="flex items-center justify-between rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4 transition-colors hover:border-accent/60"
>
<div>
<div className="font-semibold text-fg">{activity.name}</div>

View File

@@ -22,7 +22,7 @@ export function GarminCredentialsForm({ savedEmail }: { savedEmail: string | nul
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"
className="rounded-md border border-white/10 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">
@@ -35,7 +35,7 @@ export function GarminCredentialsForm({ savedEmail }: { savedEmail: string | nul
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"
className="rounded-md border border-white/10 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent/60 focus:outline-none"
/>
</div>
@@ -49,7 +49,7 @@ export function GarminCredentialsForm({ savedEmail }: { savedEmail: string | nul
<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"
className="btn btn-primary self-start"
>
{pending ? "Zapisywanie…" : "Zapisz"}
</button>

View File

@@ -15,7 +15,7 @@ function StatusRow({ label, ok, okLabel = "Skonfigurowano", failLabel = "Brak ko
failLabel?: string;
}) {
return (
<div className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4">
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
<span className="text-fg">{label}</span>
{ok ? (
<span className="flex items-center gap-1.5 text-sm text-fg/70">
@@ -67,14 +67,14 @@ export default async function SettingsPage() {
<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">
<div className="rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md 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">
<div className="flex items-center justify-between rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4">
<span className="text-sm text-fg/70">
{lastSyncAt
? `Ostatnia synchronizacja: ${formatDate(lastSyncAt)}`

View File

@@ -55,7 +55,7 @@ export default async function StrengthWorkoutPage({
{exercisesWithHistory.map(({ exercise }, index) => {
const e1rm = exerciseE1rm(exercise);
return (
<div key={index} className="flex flex-col gap-2 rounded-lg border border-muted/40 bg-surface px-3 py-2.5">
<div key={index} className="flex flex-col gap-2 rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md px-3 py-2.5">
<p className="text-xs font-semibold text-fg">{exercise.name}</p>
{exercise.notes ? <p className="text-xs text-fg/50 italic">{exercise.notes}</p> : null}
<div className="flex flex-wrap gap-1">

View File

@@ -18,12 +18,12 @@ export function ImportForm() {
rows={16}
required
placeholder={"Trening A\nWednesday, 10 June 2026 at 06:40\n\nDeadlift (Barbell)\nSet 1: 80 kg × 8\n..."}
className="w-full rounded-md border border-muted/40 bg-surface p-3 font-mono text-sm text-fg placeholder:text-fg/30 focus:border-accent focus:outline-none"
className="w-full rounded-md border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-3 font-mono text-sm text-fg placeholder:text-fg/30 focus:border-accent focus:outline-none"
/>
<button
type="submit"
disabled={pending}
className="self-start rounded-md bg-accent px-4 py-2 text-sm font-semibold text-fg transition-opacity hover:opacity-90 disabled:opacity-50"
className="btn btn-primary self-start"
>
{pending ? "Importowanie..." : "Importuj"}
</button>

View File

@@ -31,7 +31,7 @@ export default async function StrengthPage() {
</div>
<Link
href="/strength/import"
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"
className="btn btn-primary"
>
<Plus size={16} />
Importuj
@@ -52,7 +52,7 @@ export default async function StrengthPage() {
<li key={workout._id.toString()}>
<Link
href={`/strength/${workout._id.toString()}`}
className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4 transition-colors hover:border-accent/60"
className="flex items-center justify-between rounded-2xl border border-white/10 bg-surface/55 shadow-lg shadow-black/10 backdrop-blur-md p-4 transition-colors hover:border-accent/60"
>
<div>
<div className="font-semibold text-fg">{workout.name}</div>

View File

@@ -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>

View File

@@ -14,6 +14,8 @@ import {
isSameMonth,
isSameDay,
isToday,
isAfter,
startOfDay,
} from "date-fns";
import { pl } from "date-fns/locale";
import { Activity, ChevronLeft, ChevronRight, Dumbbell, Sparkles } from "lucide-react";
@@ -64,9 +66,16 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
const [currentMonth, setCurrentMonth] = useState(() => new Date());
const [selectedDate, setSelectedDate] = useState<Date>(() => new Date());
function hasAnalysis(date: Date) {
return byDay.has(dayKey(date));
}
const runDays = useMemo(
() => new Set(runs.map((r) => dayKey(new Date(r.startTime)))),
[runs]
);
const workoutDays = useMemo(
() => new Set(workouts.map((w) => dayKey(new Date(w.date)))),
[workouts]
);
const todayStart = startOfDay(new Date());
const monthStart = startOfMonth(currentMonth);
const monthEnd = endOfMonth(monthStart);
@@ -89,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" />
@@ -100,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>
@@ -142,12 +151,17 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
<div className="grid grid-cols-7 gap-1">
{days.map((d, i) => {
const key = dayKey(d);
const inMonth = isSameMonth(d, monthStart);
const has = hasAnalysis(d);
const today = isToday(d);
const selectable = has || today;
const future = isAfter(startOfDay(d), todayStart);
const selectable = !future;
const selected = isSameDay(d, selectedDate);
const hasRun = runDays.has(key);
const hasWorkout = workoutDays.has(key);
const hasAnalysis = byDay.has(key);
return (
<button
type="button"
@@ -159,21 +173,37 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
!inMonth && "opacity-30",
selectable && !selected && "cursor-pointer text-fg hover:bg-accent/20",
selected && "bg-accent font-semibold text-fg",
!selectable && "cursor-default text-fg/40",
!selectable && "cursor-not-allowed text-fg/30",
today && !selected && "ring-1 ring-accent/50",
]
.filter(Boolean)
.join(" ")}
>
{d.getDate()}
{has && !selected && (
<span className="absolute bottom-0.5 left-1/2 h-1 w-1 -translate-x-1/2 rounded-full bg-accent" />
{!selected && (hasRun || hasWorkout || hasAnalysis) && (
<span className="absolute bottom-0.5 left-1/2 flex -translate-x-1/2 gap-0.5">
{hasRun && <span className="h-1 w-1 shrink-0 rounded-full bg-[#e0a82e]" />}
{hasWorkout && <span className="h-1 w-1 shrink-0 rounded-full bg-sand" />}
{hasAnalysis && <span className="h-1 w-1 shrink-0 rounded-full bg-accent" />}
</span>
)}
</button>
);
})}
</div>
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-fg/55">
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-[#e0a82e]" /> Bieg
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-sand" /> Siłownia
</span>
<span className="flex items-center gap-1">
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-accent" /> Analiza
</span>
</div>
<div className="mt-4 flex flex-col gap-1.5">
<p className="text-xs font-medium text-fg/50">
Treningi · {format(selectedDate, "d MMM yyyy", { locale: pl })}
@@ -186,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">
@@ -205,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">
@@ -250,7 +280,7 @@ export function DashboardAnalysisCard({ analyses, runs, workouts }: Props) {
</p>
) : (
<p className="text-sm text-fg/60">
Wybierz dzień oznaczony kropką, aby zobaczyć analizę z tego dnia.
Brak analizy AI dla {format(selectedDate, "d MMMM yyyy", { locale: pl })}.
</p>
)}
</div>

View File

@@ -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)"

View File

@@ -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>

View File

@@ -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)",
}}

View File

@@ -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)",
}}

View File

@@ -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>

View File

@@ -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
View 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>
);
})}
</>
);
}

View File

@@ -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,35 +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.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-xs font-normal tracking-wide text-fg/50 sm:block">
<span className="font-semibold text-accent">K</span>siążka{" "}
<span className="font-semibold text-accent">N</span>otowań{" "}
<span className="font-semibold text-accent">U</span>dźwigów i{" "}
<span className="font-semibold text-accent">R</span>ezultatów
</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}

View File

@@ -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>

View File

@@ -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[] }) {

View File

@@ -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)",
}}

View File

@@ -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)",
}}

View File

@@ -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} />

View File

@@ -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>
);

View File

@@ -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>

View File

@@ -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)",
}}

View File

@@ -23,7 +23,13 @@ type Props = {
};
const COLORS: Record<string, string> = {};
const PALETTE = ["var(--color-accent)", "#5b9bd5", "#70ad47", "#ffc000", "#ed7d31"];
const PALETTE = [
"var(--color-accent)",
"var(--color-sand)",
"#ff9466",
"#8f3415",
"#f4c9a3",
];
function getColor(name: string): string {
if (!COLORS[name]) {
@@ -38,9 +44,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 +70,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 +81,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
View 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>
);
}

View File

@@ -18,6 +18,6 @@ export default auth((req) => {
export const config = {
matcher: [
"/((?!api/auth|_next/static|_next/image|favicon.ico|icon.svg|logo.svg|public/).*)",
"/((?!api/auth|_next/static|_next/image|.*\\.(?:svg|ico|png|jpg|jpeg|webp|gif)$).*)",
],
};

1
public/logo-bars.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8" ?><svg xmlns="http://www.w3.org/2000/svg" width="51" height="48" viewBox="0 0 51 48"><rect x="0" y="30" width="9" height="18" rx="3" fill="#f7f3e9"/><rect x="14" y="20" width="9" height="28" rx="3" fill="#f7f3e9"/><rect x="28" y="10" width="9" height="38" rx="3" fill="#f7f3e9"/><rect x="42" y="0" width="9" height="48" rx="3" fill="#fb4617"/></svg>

After

Width:  |  Height:  |  Size: 387 B

1
public/logo-icon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

1
public/logo-mark.svg Normal file
View File

@@ -0,0 +1 @@
<?xml version="1.0" encoding="utf-8" ?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="738" height="444" viewBox="144 173 738 444"><path fill="#FB4617" d="M589.071 183.464C588.989 200.986 587.545 218.73 584.386 235.974C581.445 252.03 573.953 266.541 566.054 280.654C578.512 268.568 585.684 260.918 593.143 244.721C605.705 249.265 617.021 253.152 629.062 258.986C659.52 274.056 686.77 294.881 709.308 320.313C712.759 324.175 717.78 329.744 720.783 333.87C705.571 328.15 692.428 322.932 676.623 318.914C664.264 315.773 653.535 314.191 640.993 312.06C694.298 344.092 730.223 388.478 752.218 446.707C754.895 453.794 757.871 461.496 759.981 468.74C744.97 462.908 732.874 457.141 716.92 452.462C666.376 437.132 612.003 440.5 563.738 461.951C545.769 469.881 533.769 477.827 517.007 487.548C516.492 476.189 515.701 463.88 515.466 452.584C519.489 449.076 539.421 433.622 540.968 431.308C548.098 420.647 552.047 386.799 552.894 374.485C552.995 373.012 553.063 371.538 553.099 370.062C552.431 369.296 551.94 368.713 551.534 367.778L551.245 367.138C545.705 381.527 536.061 406.355 526.555 418.298C524.188 421.272 499.614 441.531 495.652 444.455C469.031 431.736 442.395 428.96 414.2 438.853C403.784 442.509 394.849 447.798 385.04 452.675C395.157 439.475 404.789 426.436 415.147 413.388C407.454 405.28 398.662 393.894 391.503 384.985C371.281 395.789 362.475 398.797 339.005 391.662C336.708 390.948 334.421 390.201 332.146 389.423C338.733 384.047 345.998 378.953 352.794 373.761C370.36 360.281 387.995 346.892 405.7 333.594C413.181 327.916 422.823 320.998 429.782 315.125C431.767 294.387 431.691 291.982 447.552 278.715C468.828 260.918 488.288 250.944 513.807 241.011C514.264 248.793 515.056 256.551 516.18 264.264C519.882 252.327 522.577 238.619 525.272 226.302C532.058 220.996 543.089 214.204 550.498 209.268C563.301 200.585 576.159 191.984 589.071 183.464Z"/><path fill="#2E162E" d="M553.336 360.501C553.556 363.982 553.677 366.593 553.099 370.062C552.431 369.296 551.94 368.713 551.534 367.778L551.245 367.138C551.868 365.025 552.647 362.601 553.336 360.501Z"/><path fill="#2B2D42" d="M570.935 212.987L571.307 213.432C571.912 217.117 571.411 221.206 571.342 224.941C570.998 243.577 561.401 269.281 542.992 277.612L542.385 276.866C533.473 239.579 543.969 235.242 570.935 212.987Z"/><path fill="#2B2D42" d="M471.391 297.628C475.757 297.206 480.158 297.302 484.502 297.915C486.611 298.21 488.709 298.58 490.791 299.023C479.083 306.75 467.269 312.814 455.468 320.065C451.181 322.6 447.58 325.094 443.074 327.371C449.046 313.381 454.987 300.83 471.391 297.628Z"/><path fill="#F7F3E9" d="M267.117 499.251L776.99 499.438L777.019 539.616L756.867 539.62L247.708 539.582C247.643 526.181 247.67 512.78 247.788 499.379C253.977 499.169 260.874 499.28 267.117 499.251Z"/><path fill="#F7F3E9" d="M785.179 425.193C799.766 424.912 815.041 425.141 829.68 425.145C830.294 452.897 829.793 482.691 829.706 510.519L829.969 605.863C828.359 606.902 790.399 606.472 785.239 606.47L785.179 425.193Z"/><path fill="#F7F3E9" d="M194.401 425.072C209.055 425.056 223.709 425.118 238.363 425.258L238.253 606.168C223.581 606.317 208.908 606.356 194.236 606.285C193.972 545.881 194.027 485.475 194.401 425.072Z"/><path fill="#F7F3E9" d="M287.314 332.065C287.556 331.302 287.585 330.915 288.092 330.379L288.208 330.366C288.681 330.813 288.499 333.528 288.495 334.087C288.83 342.987 292.198 352.474 295.94 360.434C306.213 382.29 326.212 397.322 350.151 400.935C364.862 403.156 374.434 400.556 387.564 394.073C391.551 400.051 400.91 410.545 405.812 416.649C400.346 420.821 395.704 424.163 389.838 427.831C376.36 436.258 354.31 435.91 339.387 431.892C319.937 426.65 303.397 413.836 293.462 396.312C282.286 376.63 281.364 353.542 287.314 332.065Z"/><path fill="#D4CBBB" d="M287.314 332.065C287.556 331.302 287.585 330.915 288.092 330.379L288.208 330.366C288.681 330.813 288.499 333.528 288.495 334.087L287.314 332.065Z"/><path fill="#F7F3E9" d="M154.418 461.74L188.238 461.567C188.333 470.275 188.612 568.781 187.857 570.438L153.317 570.429C152.984 542.388 153.316 514.27 153.281 486.222C153.272 478.985 152.763 469.256 153.482 462.279L154.418 461.74Z"/><path fill="#F7F3E9" d="M836.544 461.666L871.531 461.719C871.513 497.627 872.037 534.539 871.57 570.312C859.879 570.388 848.187 570.328 836.498 570.133L836.544 461.666Z"/><path fill="#FB4617" d="M315.795 429C318.261 429.843 321.362 431.778 323.908 432.934C337.347 439.033 349.753 442.893 364.656 442.573C362.576 444.323 360.586 446.234 358.574 447.893C356.092 450.049 352.715 452.791 350.04 454.766C347.289 453.311 342.152 450.745 339.749 449.249C334.614 446.054 318.477 433.622 315.795 429Z"/></svg>

After

Width:  |  Height:  |  Size: 4.5 KiB