- 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>
192 lines
8.5 KiB
TypeScript
192 lines
8.5 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { CreditCard, Building2, ScrollText, Crown, Sparkles } from "lucide-react";
|
||
import { siteContent } from "@/data/content";
|
||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||
import { Reveal } from "@/components/ui/Reveal";
|
||
import { BookingModal } from "@/components/ui/BookingModal";
|
||
|
||
type Tab = "prices" | "rental" | "rules";
|
||
|
||
export function Pricing() {
|
||
const { pricing } = siteContent;
|
||
const [activeTab, setActiveTab] = useState<Tab>("prices");
|
||
const [bookingOpen, setBookingOpen] = useState(false);
|
||
|
||
const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
|
||
{ id: "prices", label: "Абонементы", icon: <CreditCard size={16} /> },
|
||
{ id: "rental", label: "Аренда зала", icon: <Building2 size={16} /> },
|
||
{ id: "rules", label: "Правила", icon: <ScrollText size={16} /> },
|
||
];
|
||
|
||
// Split items: regular + unlimited (last item)
|
||
const regularItems = pricing.items.slice(0, -1);
|
||
const unlimitedItem = pricing.items[pricing.items.length - 1];
|
||
|
||
return (
|
||
<section id="pricing" className="section-glow relative section-padding bg-neutral-50 dark:bg-[#050505]">
|
||
<div className="section-divider absolute top-0 left-0 right-0" />
|
||
<div className="section-container">
|
||
<Reveal>
|
||
<SectionHeading centered>{pricing.title}</SectionHeading>
|
||
</Reveal>
|
||
|
||
{/* Tabs */}
|
||
<Reveal>
|
||
<div className="mt-12 flex flex-wrap justify-center gap-2">
|
||
{tabs.map((tab) => (
|
||
<button
|
||
key={tab.id}
|
||
onClick={() => setActiveTab(tab.id)}
|
||
className={`inline-flex items-center gap-2 rounded-full px-6 py-2.5 text-sm font-medium transition-all duration-300 cursor-pointer ${
|
||
activeTab === tab.id
|
||
? "bg-gold text-black shadow-lg shadow-gold/25"
|
||
: "bg-neutral-100 text-neutral-600 hover:bg-neutral-200 dark:bg-white/[0.06] dark:text-neutral-300 dark:hover:bg-white/[0.1]"
|
||
}`}
|
||
>
|
||
{tab.icon}
|
||
{tab.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Reveal>
|
||
|
||
{/* Prices tab */}
|
||
{activeTab === "prices" && (
|
||
<Reveal>
|
||
<div className="mx-auto mt-10 max-w-4xl">
|
||
<p className="mb-8 text-center text-sm text-neutral-500 dark:text-neutral-400">
|
||
{pricing.subtitle}
|
||
</p>
|
||
|
||
{/* Cards grid */}
|
||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||
{regularItems.map((item, i) => {
|
||
const isPopular = i === 0;
|
||
return (
|
||
<button
|
||
key={i}
|
||
onClick={() => setBookingOpen(true)}
|
||
className={`group relative cursor-pointer rounded-2xl border p-5 transition-all duration-300 text-left ${
|
||
isPopular
|
||
? "border-gold/40 bg-gradient-to-br from-gold/10 via-transparent to-gold/5 dark:from-gold/[0.07] dark:to-gold/[0.02] shadow-lg shadow-gold/10 hover:shadow-xl hover:shadow-gold/20"
|
||
: "border-neutral-200 bg-white hover:border-neutral-300 dark:border-white/[0.06] dark:bg-[#0a0a0a] dark:hover:border-white/[0.12]"
|
||
}`}
|
||
>
|
||
{/* Popular badge */}
|
||
{isPopular && (
|
||
<div className="absolute -top-3 left-1/2 -translate-x-1/2">
|
||
<span className="inline-flex items-center gap-1 rounded-full bg-gold px-3 py-1 text-[10px] font-bold uppercase tracking-wider text-black shadow-md shadow-gold/30">
|
||
<Sparkles size={10} />
|
||
Популярный
|
||
</span>
|
||
</div>
|
||
)}
|
||
|
||
<div className={isPopular ? "mt-1" : ""}>
|
||
{/* Name */}
|
||
<p className={`text-sm font-medium ${isPopular ? "text-gold-dark dark:text-gold-light" : "text-neutral-700 dark:text-neutral-300"}`}>
|
||
{item.name}
|
||
</p>
|
||
|
||
{/* Note */}
|
||
{item.note && (
|
||
<p className="mt-1 text-xs text-neutral-400 dark:text-neutral-500">
|
||
{item.note}
|
||
</p>
|
||
)}
|
||
|
||
{/* Price */}
|
||
<p className={`mt-3 font-display text-2xl font-bold ${isPopular ? "text-gold" : "text-neutral-900 dark:text-white"}`}>
|
||
{item.price}
|
||
</p>
|
||
</div>
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{/* Unlimited — featured card */}
|
||
{unlimitedItem && (
|
||
<button onClick={() => setBookingOpen(true)} className="mt-6 w-full cursor-pointer text-left team-card-glitter rounded-2xl border border-gold/30 bg-gradient-to-r from-gold/10 via-gold/5 to-gold/10 dark:from-gold/[0.06] dark:via-transparent dark:to-gold/[0.06] p-6 sm:p-8 transition-shadow duration-300 hover:shadow-xl hover:shadow-gold/20">
|
||
<div className="flex flex-col items-center gap-4 sm:flex-row sm:justify-between">
|
||
<div className="text-center sm:text-left">
|
||
<div className="flex items-center justify-center gap-2 sm:justify-start">
|
||
<Crown size={18} className="text-gold" />
|
||
<p className="text-lg font-bold text-neutral-900 dark:text-white">
|
||
{unlimitedItem.name}
|
||
</p>
|
||
</div>
|
||
{unlimitedItem.note && (
|
||
<p className="mt-1 text-sm text-neutral-500 dark:text-neutral-400">
|
||
{unlimitedItem.note}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<p className="shrink-0 font-display text-3xl font-bold text-gold">
|
||
{unlimitedItem.price}
|
||
</p>
|
||
</div>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</Reveal>
|
||
)}
|
||
|
||
{/* Rental tab */}
|
||
{activeTab === "rental" && (
|
||
<Reveal>
|
||
<div className="mx-auto mt-10 max-w-2xl space-y-3">
|
||
{pricing.rentalItems.map((item, i) => (
|
||
<button
|
||
key={i}
|
||
onClick={() => setBookingOpen(true)}
|
||
className="w-full cursor-pointer text-left flex items-center justify-between gap-4 rounded-2xl border border-neutral-200 bg-white px-6 py-5 transition-colors hover:border-neutral-300 dark:border-white/[0.06] dark:bg-[#0a0a0a] dark:hover:border-white/[0.12]"
|
||
>
|
||
<div>
|
||
<p className="font-medium text-neutral-900 dark:text-white">
|
||
{item.name}
|
||
</p>
|
||
{item.note && (
|
||
<p className="mt-0.5 text-sm text-neutral-500 dark:text-neutral-400">
|
||
{item.note}
|
||
</p>
|
||
)}
|
||
</div>
|
||
<span className="shrink-0 font-display text-xl font-bold text-gold-dark dark:text-gold-light">
|
||
{item.price}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</Reveal>
|
||
)}
|
||
|
||
{/* Rules tab */}
|
||
{activeTab === "rules" && (
|
||
<Reveal>
|
||
<div className="mx-auto mt-10 max-w-2xl space-y-3">
|
||
{pricing.rules.map((rule, i) => (
|
||
<div
|
||
key={i}
|
||
className="flex gap-4 rounded-2xl border border-neutral-200 bg-white px-5 py-4 dark:border-white/[0.06] dark:bg-[#0a0a0a]"
|
||
>
|
||
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-gold/10 text-xs font-bold text-gold-dark dark:bg-gold/10 dark:text-gold-light">
|
||
{i + 1}
|
||
</span>
|
||
<p className="text-sm leading-relaxed text-neutral-700 dark:text-neutral-300">
|
||
{rule}
|
||
</p>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Reveal>
|
||
)}
|
||
</div>
|
||
|
||
<BookingModal open={bookingOpen} onClose={() => setBookingOpen(false)} />
|
||
</section>
|
||
);
|
||
}
|