fix: widen classes container, wrap schedule tags on mobile
Classes section: replace section-container with wider max-w-[96rem] to reduce side gaps. Schedule DayCard: allow tags to flex-wrap so they don't clip on narrow screens.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Image from "next/image";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import {
|
||||
@@ -7,16 +8,13 @@ import {
|
||||
Dumbbell, Wind, Moon, Sun, Ribbon, Gem, Feather, CircleDot,
|
||||
Activity, Drama, PersonStanding, Footprints, PartyPopper, Flower2,
|
||||
Waves, Eye, Orbit, Brush, Palette, HandMetal, Theater,
|
||||
ArrowUpRight, X,
|
||||
} 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";
|
||||
import { formatMarkup } from "@/lib/markup";
|
||||
|
||||
/** Map of kebab-case icon keys to their components (curated for dance school) */
|
||||
const ICON_MAP: Record<string, LucideIcon> = {
|
||||
"flame": Flame, "heart": Heart, "heart-pulse": HeartPulse, "star": Star,
|
||||
"sparkles": Sparkles, "music": Music, "zap": Zap, "crown": Crown,
|
||||
@@ -30,7 +28,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
|
||||
|
||||
function getIcon(key: string) {
|
||||
const Icon = ICON_MAP[key];
|
||||
return Icon ? <Icon size={20} /> : null;
|
||||
return Icon ? <Icon size={16} /> : null;
|
||||
}
|
||||
|
||||
interface ClassesProps {
|
||||
@@ -38,53 +36,108 @@ interface ClassesProps {
|
||||
}
|
||||
|
||||
export function Classes({ data: classes }: ClassesProps) {
|
||||
const [selected, setSelected] = useState<ClassItem | null>(null);
|
||||
|
||||
if (!classes?.items?.length) return null;
|
||||
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-50 dark:bg-[#080808]">
|
||||
<div className="section-divider absolute top-0 left-0 right-0" />
|
||||
<div className="section-container">
|
||||
<div className="mx-auto max-w-[96rem] px-4 sm:px-6">
|
||||
<Reveal>
|
||||
<SectionHeading centered>{classes.title}</SectionHeading>
|
||||
</Reveal>
|
||||
|
||||
<div className="mt-14">
|
||||
<Reveal>
|
||||
<ShowcaseLayout<ClassItem>
|
||||
items={classes.items}
|
||||
activeIndex={activeIndex}
|
||||
onSelect={select}
|
||||
onHoverChange={setHovering}
|
||||
getItemLabel={(item) => item.name}
|
||||
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">
|
||||
<div className="mt-14 grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
{classes.items.map((item, i) => (
|
||||
<Reveal key={i}>
|
||||
<button
|
||||
onClick={() => setSelected(item)}
|
||||
className="group relative w-full text-left rounded-2xl border border-neutral-200 bg-white p-5 transition-all duration-300 hover:border-gold/30 hover:shadow-lg hover:shadow-gold/[0.08] cursor-pointer dark:border-white/[0.08] dark:bg-white/[0.03] dark:hover:border-gold/20 dark:hover:shadow-[0_0_30px_rgba(201,169,110,0.06)]"
|
||||
>
|
||||
{/* Header: icon + name + arrow */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-gold/10 text-gold-dark dark:text-gold-light">
|
||||
{getIcon(item.icon)}
|
||||
</span>
|
||||
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">
|
||||
{item.name}
|
||||
</h3>
|
||||
</div>
|
||||
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-neutral-200 text-neutral-400 transition-all group-hover:border-gold/40 group-hover:text-gold dark:border-white/10 dark:text-neutral-500">
|
||||
<ArrowUpRight size={14} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="mt-2.5 text-sm leading-relaxed text-neutral-500 dark:text-neutral-400 line-clamp-2">
|
||||
{item.description}
|
||||
</p>
|
||||
|
||||
{/* Photo */}
|
||||
{item.images?.[0] && (
|
||||
<div className="relative mt-4 aspect-[4/3] overflow-hidden rounded-xl">
|
||||
<Image
|
||||
src={item.images[0]}
|
||||
alt={item.name}
|
||||
fill
|
||||
loading="lazy"
|
||||
sizes="(min-width: 1024px) 60vw, 100vw"
|
||||
className="object-cover"
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||
style={{
|
||||
objectPosition: `${item.imageFocalX ?? 50}% ${item.imageFocalY ?? 50}%`,
|
||||
transform: item.imageZoom && item.imageZoom > 1 ? `scale(${item.imageZoom})` : undefined,
|
||||
}}
|
||||
/>
|
||||
{/* Gradient overlay */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
|
||||
|
||||
{/* Icon + name overlay */}
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 flex items-center gap-3">
|
||||
<div className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gold/20 text-gold-light backdrop-blur-sm">
|
||||
{getIcon(item.icon)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</Reveal>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Detail modal */}
|
||||
{selected && (
|
||||
<ClassDetailModal item={selected} onClose={() => setSelected(null)} />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function ClassDetailModal({ item, onClose }: { item: ClassItem; onClose: () => void }) {
|
||||
return (
|
||||
<div
|
||||
className="modal-overlay fixed inset-0 z-[60] flex items-center justify-center p-4"
|
||||
onClick={onClose}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={item.name}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" />
|
||||
<div
|
||||
className="modal-content relative w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-2xl border border-neutral-200 bg-white dark:border-white/[0.08] dark:bg-neutral-950 shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Hero image */}
|
||||
{item.images?.[0] && (
|
||||
<div className="relative aspect-[16/9] overflow-hidden rounded-t-2xl">
|
||||
<Image
|
||||
src={item.images[0]}
|
||||
alt={item.name}
|
||||
fill
|
||||
sizes="(min-width: 768px) 672px, 100vw"
|
||||
className="object-cover"
|
||||
style={{
|
||||
objectPosition: `${item.imageFocalX ?? 50}% ${item.imageFocalY ?? 50}%`,
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 flex items-center gap-3">
|
||||
<span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-gold/20 text-gold-light backdrop-blur-sm">
|
||||
{getIcon(item.icon)}
|
||||
</span>
|
||||
<h3 className="text-2xl font-bold text-white drop-shadow-[0_2px_8px_rgba(0,0,0,0.5)]">
|
||||
{item.name}
|
||||
</h3>
|
||||
@@ -92,46 +145,29 @@ export function Classes({ data: classes }: ClassesProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{/* Close button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
aria-label="Закрыть"
|
||||
className="absolute right-4 top-4 flex h-11 w-11 items-center justify-center rounded-full bg-black/40 text-white/70 backdrop-blur-sm transition-colors hover:bg-black/60 hover:text-white cursor-pointer"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 sm:p-8">
|
||||
{item.detailedDescription && (
|
||||
<div className="mt-5 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
<div className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{formatMarkup(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-600 dark:text-neutral-500 truncate">
|
||||
{!item.detailedDescription && item.description && (
|
||||
<p className="text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</Reveal>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,12 +33,12 @@ function ClassRow({
|
||||
}) {
|
||||
return (
|
||||
<div className="px-5 py-3.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-white/40">
|
||||
<Clock size={13} />
|
||||
<Clock size={13} className="shrink-0" />
|
||||
<span className="font-semibold">{cls.time}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex flex-wrap justify-end gap-1.5">
|
||||
{cls.status && (() => {
|
||||
const cfg = findStatusConfig(statuses, cls.status);
|
||||
return <ScheduleBadge>{cfg?.label || cls.status}</ScheduleBadge>;
|
||||
|
||||
Reference in New Issue
Block a user