refactor: centralize gold tokens, extract sub-components, clean up unused code

- Replace hardcoded hex colors with gold/gold-light/gold-dark Tailwind tokens
- Extract Schedule into DayCard, ScheduleFilters, MobileSchedule sub-components
- Extract Team into TeamCarousel, TeamMemberInfo sub-components
- Add UI_CONFIG for centralized magic numbers (timings, thresholds)
- Add reusable IconBadge component, simplify Contact section
- Convert Pricing clickable divs to semantic buttons for a11y
- Remove unused SocialLinks, btn-outline, btn-ghost, nav-link CSS classes
- Fix React setState-during-render error in TeamCarousel (deferred update pattern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 14:57:39 +03:00
parent 08e4af1d55
commit d5afaf92ba
31 changed files with 784 additions and 691 deletions

View File

@@ -0,0 +1,127 @@
"use client";
import { User, X } from "lucide-react";
import type { ScheduleDay } from "@/types/content";
import { TYPE_DOT } from "./constants";
interface MobileScheduleProps {
filteredDays: ScheduleDay[];
filterType: string | null;
setFilterType: (type: string | null) => void;
filterTrainer: string | null;
setFilterTrainer: (trainer: string | null) => void;
hasActiveFilter: boolean;
clearFilters: () => void;
}
export function MobileSchedule({
filteredDays,
filterType,
setFilterType,
filterTrainer,
setFilterTrainer,
hasActiveFilter,
clearFilters,
}: MobileScheduleProps) {
return (
<div className="mt-6 px-4 sm:hidden">
{/* Active filter indicator */}
{hasActiveFilter && (
<div className="mb-3 flex items-center justify-between rounded-xl bg-gold/10 px-4 py-2.5 dark:bg-gold/5">
<div className="flex items-center gap-2 text-xs font-medium text-gold-dark dark:text-gold-light">
{filterTrainer && (
<span className="flex items-center gap-1">
<User size={11} />
{filterTrainer}
</span>
)}
{filterType && (
<span className="flex items-center gap-1">
<span className={`h-1.5 w-1.5 rounded-full ${TYPE_DOT[filterType] ?? "bg-white/30"}`} />
{filterType}
</span>
)}
</div>
<button
onClick={clearFilters}
className="flex items-center gap-1 rounded-full px-2 py-1 text-[11px] text-gold-dark dark:text-gold-light active:bg-gold/20"
>
<X size={12} />
Сбросить
</button>
</div>
)}
{filteredDays.length > 0 ? (
<div className="space-y-1">
{filteredDays.map((day) => (
<div key={day.day}>
{/* Day header */}
<div className="flex items-center gap-2.5 py-2.5">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gold/10 text-xs font-bold text-gold-dark dark:bg-gold/10 dark:text-gold-light">
{day.dayShort}
</span>
<span className="text-sm font-semibold text-neutral-900 dark:text-white/90">
{day.day}
</span>
</div>
{/* Class rows */}
<div className="ml-1 border-l-2 border-neutral-200 dark:border-white/[0.08]">
{day.classes.map((cls, i) => (
<div
key={i}
className={`ml-3 flex items-start gap-3 rounded-lg px-3 py-2 ${cls.hasSlots ? "bg-emerald-500/5" : cls.recruiting ? "bg-sky-500/5" : ""}`}
>
{/* Time */}
<span className="shrink-0 w-[72px] text-xs font-semibold tabular-nums text-neutral-500 dark:text-white/40 pt-0.5">
{cls.time}
</span>
{/* Info — tappable trainer & type */}
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<button
onClick={() => setFilterTrainer(filterTrainer === cls.trainer ? null : cls.trainer)}
className={`truncate text-sm font-medium text-left active:opacity-60 ${filterTrainer === cls.trainer ? "text-gold underline underline-offset-2" : "text-neutral-800 dark:text-white/80"}`}
>
{cls.trainer}
</button>
{cls.hasSlots && (
<span className="shrink-0 rounded-full bg-emerald-500/15 border border-emerald-500/25 px-1.5 py-px text-[9px] font-semibold text-emerald-600 dark:text-emerald-400">
места
</span>
)}
{cls.recruiting && (
<span className="shrink-0 rounded-full bg-sky-500/15 border border-sky-500/25 px-1.5 py-px text-[9px] font-semibold text-sky-600 dark:text-sky-400">
набор
</span>
)}
{cls.level && (
<span className="shrink-0 rounded-full bg-rose-500/15 border border-rose-500/25 px-1.5 py-px text-[9px] font-semibold text-rose-600 dark:text-rose-400">
{cls.level}
</span>
)}
</div>
<button
onClick={() => setFilterType(filterType === cls.type ? null : cls.type)}
className={`mt-0.5 flex items-center gap-1.5 active:opacity-60 ${filterType === cls.type ? "opacity-100" : ""}`}
>
<span className={`h-1.5 w-1.5 shrink-0 rounded-full ${TYPE_DOT[cls.type] ?? "bg-white/30"}`} />
<span className={`text-[11px] ${filterType === cls.type ? "text-gold underline underline-offset-2" : "text-neutral-400 dark:text-white/30"}`}>{cls.type}</span>
</button>
</div>
</div>
))}
</div>
</div>
))}
</div>
) : (
<div className="py-12 text-center text-sm text-neutral-400 dark:text-white/30">
Нет занятий по выбранным фильтрам
</div>
)}
</div>
);
}