- Add "Все студии" tab merging all locations by weekday with location sub-headers - Location tabs show hall name + address subtitle for clarity - Add day multi-select and time-of-day preset filters (Утро/День/Вечер) behind collapsible "Когда" button - Make trainer and type names clickable in day cards for inline filtering - Add group view clustering classes by trainer+type+location - Remove trainer dropdown from filter bar — filter by clicking names in schedule - Add searchable icon picker and lucide-react icon rendering for classes admin/section Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
4.4 KiB
TypeScript
116 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import Image from "next/image";
|
|
import { icons } from "lucide-react";
|
|
import { SectionHeading } from "@/components/ui/SectionHeading";
|
|
import { Reveal } from "@/components/ui/Reveal";
|
|
import { ShowcaseLayout } from "@/components/ui/ShowcaseLayout";
|
|
import { useShowcaseRotation } from "@/hooks/useShowcaseRotation";
|
|
import type { ClassItem, SiteContent } from "@/types";
|
|
import { UI_CONFIG } from "@/lib/config";
|
|
|
|
// kebab "heart-pulse" → PascalCase "HeartPulse"
|
|
function toPascal(kebab: string) {
|
|
return kebab.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
|
|
}
|
|
|
|
function getIcon(key: string) {
|
|
const Icon = icons[toPascal(key) as keyof typeof icons];
|
|
return Icon ? <Icon size={20} /> : null;
|
|
}
|
|
|
|
interface ClassesProps {
|
|
data: SiteContent["classes"];
|
|
}
|
|
|
|
export function Classes({ data: classes }: ClassesProps) {
|
|
const { activeIndex, select, setHovering } = useShowcaseRotation({
|
|
totalItems: classes.items.length,
|
|
autoPlayInterval: UI_CONFIG.showcase.autoPlayInterval,
|
|
});
|
|
|
|
return (
|
|
<section id="classes" className="section-glow relative section-padding bg-neutral-100 dark:bg-[#080808]">
|
|
<div className="section-divider absolute top-0 left-0 right-0" />
|
|
<div className="section-container">
|
|
<Reveal>
|
|
<SectionHeading centered>{classes.title}</SectionHeading>
|
|
</Reveal>
|
|
|
|
<div className="mt-14">
|
|
<Reveal>
|
|
<ShowcaseLayout<ClassItem>
|
|
items={classes.items}
|
|
activeIndex={activeIndex}
|
|
onSelect={select}
|
|
onHoverChange={setHovering}
|
|
renderDetail={(item) => (
|
|
<div>
|
|
{/* Hero image */}
|
|
{item.images && item.images[0] && (
|
|
<div className="team-card-glitter relative aspect-[16/9] w-full overflow-hidden rounded-2xl">
|
|
<Image
|
|
src={item.images[0]}
|
|
alt={item.name}
|
|
fill
|
|
className="object-cover"
|
|
/>
|
|
{/* Gradient overlay */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
|
|
|
|
{/* Icon + name overlay */}
|
|
<div className="absolute bottom-0 left-0 right-0 p-6">
|
|
<div className="mb-2 inline-flex h-9 w-9 items-center justify-center rounded-lg bg-gold/20 text-gold-light backdrop-blur-sm">
|
|
{getIcon(item.icon)}
|
|
</div>
|
|
<h3 className="text-2xl font-bold text-white">
|
|
{item.name}
|
|
</h3>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{item.detailedDescription && (
|
|
<div className="mt-5 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 whitespace-pre-line">
|
|
{item.detailedDescription}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
renderSelectorItem={(item, _i, isActive) => (
|
|
<div className="flex items-center gap-2 px-3 py-2 lg:gap-3 lg:p-3">
|
|
{/* Icon */}
|
|
<div
|
|
className={`flex h-7 w-7 lg:h-9 lg:w-9 shrink-0 items-center justify-center rounded-lg transition-colors ${
|
|
isActive
|
|
? "bg-gold/20 text-gold-light"
|
|
: "bg-neutral-200/50 text-neutral-500 dark:bg-white/[0.06] dark:text-neutral-400"
|
|
}`}
|
|
>
|
|
{getIcon(item.icon)}
|
|
</div>
|
|
<div className="min-w-0">
|
|
<p
|
|
className={`text-xs lg:text-sm font-semibold truncate transition-colors ${
|
|
isActive
|
|
? "text-gold"
|
|
: "text-neutral-700 dark:text-neutral-300"
|
|
}`}
|
|
>
|
|
{item.name}
|
|
</p>
|
|
<p className="hidden lg:block text-xs text-neutral-500 dark:text-neutral-500 truncate">
|
|
{item.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
</Reveal>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|