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:
2026-04-12 21:01:10 +03:00
parent 03d3cad0a7
commit 8c84da279e
2 changed files with 124 additions and 88 deletions
+100 -64
View File
@@ -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>
);
}
+3 -3
View File
@@ -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>;