feat: redesign news & master classes sections, migrate middleware to proxy

- News: magazine layout with featured hero article + compact list, click-to-open modal
- Master classes: fashion lookbook portrait cards with full-bleed images and overlay content
- Rename middleware.ts to proxy.ts (Next.js 16 convention)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 18:49:13 +03:00
parent 4a1a2d7512
commit 26cb9a9772
4 changed files with 336 additions and 136 deletions

View File

@@ -35,7 +35,6 @@ function formatSlots(slots: MasterClassSlot[]): string {
const dates = sorted.map((s) => parseDate(s.date)).filter((d) => !isNaN(d.getTime()));
if (dates.length === 0) return "";
// Time part from first slot
const timePart = sorted[0].startTime
? `, ${sorted[0].startTime}${sorted[0].endTime}`
: "";
@@ -80,6 +79,107 @@ function isUpcoming(item: MasterClassItem): boolean {
return lastDate >= today;
}
function MasterClassCard({
item,
onSignup,
}: {
item: MasterClassItem;
onSignup: () => void;
}) {
const duration = item.slots[0] ? calcDuration(item.slots[0]) : "";
const slotsDisplay = formatSlots(item.slots);
return (
<div className="group relative flex flex-col overflow-hidden rounded-2xl bg-black">
{/* Full-bleed image */}
{item.image && (
<div className="relative aspect-[3/4] sm:aspect-[2/3] w-full overflow-hidden">
<Image
src={item.image}
alt={item.title}
fill
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
className="object-cover transition-transform duration-700 group-hover:scale-110"
/>
{/* Dark overlay that intensifies on hover */}
<div className="absolute inset-0 bg-gradient-to-t from-black via-black/20 to-transparent opacity-80 transition-opacity duration-500 group-hover:opacity-90" />
</div>
)}
{/* Content overlay at bottom */}
<div className="absolute inset-x-0 bottom-0 flex flex-col p-5 sm:p-6">
{/* Tags row */}
<div className="flex flex-wrap items-center gap-2 mb-3">
<span className="inline-flex items-center gap-1 rounded-full border border-gold/40 bg-black/40 px-2.5 py-0.5 text-[11px] font-semibold uppercase tracking-wider text-gold backdrop-blur-md">
{item.style}
</span>
{duration && (
<span className="inline-flex items-center gap-1 rounded-full bg-white/10 px-2.5 py-0.5 text-[11px] text-white/60 backdrop-blur-md">
<Clock size={10} />
{duration}
</span>
)}
</div>
{/* Title */}
<h3 className="text-xl sm:text-2xl font-bold text-white leading-tight tracking-tight">
{item.title}
</h3>
{/* Trainer */}
<div className="mt-2 flex items-center gap-2 text-sm text-white/50">
<User size={13} className="shrink-0" />
<span>{item.trainer}</span>
</div>
{/* Divider */}
<div className="mt-4 mb-4 h-px bg-gradient-to-r from-gold/40 via-gold/20 to-transparent" />
{/* Date + Location */}
<div className="flex flex-col gap-1.5 text-sm text-white/60 mb-4">
<div className="flex items-center gap-2">
<Calendar size={13} className="shrink-0 text-gold/70" />
<span>{slotsDisplay}</span>
</div>
{item.location && (
<div className="flex items-center gap-2">
<MapPin size={13} className="shrink-0 text-gold/70" />
<span>{item.location}</span>
</div>
)}
</div>
{/* Price + Actions */}
<div className="flex items-center gap-3">
<button
onClick={onSignup}
className="flex-1 rounded-xl bg-gold py-3 text-sm font-bold text-black uppercase tracking-wide transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/25 cursor-pointer"
>
Записаться
</button>
{item.instagramUrl && (
<button
onClick={() =>
window.open(item.instagramUrl, "_blank", "noopener,noreferrer")
}
className="flex h-[46px] w-[46px] items-center justify-center rounded-xl border border-white/10 text-white/40 transition-all hover:border-gold/30 hover:text-gold cursor-pointer"
>
<Instagram size={18} />
</button>
)}
</div>
{/* Price floating tag */}
<div className="absolute top-0 right-0 -translate-y-full mr-5 sm:mr-6 mb-2">
<span className="inline-block rounded-full bg-white/10 px-3 py-1 text-sm font-bold text-white backdrop-blur-md">
{item.cost}
</span>
</div>
</div>
</div>
);
}
export function MasterClasses({ data }: MasterClassesProps) {
const [signupTitle, setSignupTitle] = useState<string | null>(null);
@@ -96,7 +196,7 @@ export function MasterClasses({ data }: MasterClassesProps) {
return (
<section
id="master-classes"
className="section-glow relative section-padding bg-neutral-100 dark:bg-[#080808] overflow-hidden"
className="section-glow relative section-padding overflow-hidden"
>
<div className="section-divider absolute top-0 left-0 right-0" />
@@ -122,94 +222,17 @@ export function MasterClasses({ data }: MasterClassesProps) {
</div>
</Reveal>
) : (
<Reveal>
<div className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{upcoming.map((item, i) => {
const duration = item.slots[0] ? calcDuration(item.slots[0]) : "";
const slotsDisplay = formatSlots(item.slots);
return (
<div
<Reveal>
<div className="mx-auto mt-10 grid max-w-5xl grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
{upcoming.map((item, i) => (
<MasterClassCard
key={i}
className="group rounded-2xl border border-neutral-200 bg-white overflow-hidden transition-colors dark:border-white/[0.06] dark:bg-[#0a0a0a]"
>
{/* Image */}
{item.image && (
<div className="relative aspect-[16/9] w-full overflow-hidden">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent" />
<div className="absolute bottom-3 left-3">
<span className="inline-flex items-center gap-1.5 rounded-full border border-gold/40 bg-black/60 px-3 py-1 text-xs font-semibold text-gold backdrop-blur-sm">
<Calendar size={12} />
{slotsDisplay}
</span>
</div>
</div>
)}
{/* Content */}
<div className="p-5 space-y-3">
<h3 className="text-lg font-bold text-neutral-900 dark:text-white/90 leading-tight">
{item.title}
</h3>
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-white/50">
<User size={14} className="shrink-0" />
<span>{item.trainer}</span>
</div>
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-white/50">
<span className="inline-block h-2 w-2 rounded-full bg-gold shrink-0" />
<span>{item.style}</span>
</div>
{duration && (
<div className="flex items-center gap-2 text-sm text-neutral-600 dark:text-white/50">
<Clock size={14} className="shrink-0" />
<span>{duration}</span>
</div>
)}
{item.location && (
<div className="flex items-center gap-2 text-sm text-neutral-400 dark:text-white/35">
<MapPin size={14} className="shrink-0" />
<span>{item.location}</span>
</div>
)}
</div>
<div className="pt-1">
<span className="text-lg font-bold text-neutral-900 dark:text-white/90">
{item.cost}
</span>
</div>
<div className="flex gap-2 pt-1">
<button
onClick={() => setSignupTitle(item.title)}
className="flex-1 rounded-xl bg-gold py-2.5 text-sm font-semibold text-black transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/20 cursor-pointer"
>
Записаться
</button>
{item.instagramUrl && (
<button
onClick={() => window.open(item.instagramUrl, "_blank", "noopener,noreferrer")}
className="flex items-center justify-center gap-1.5 rounded-xl border border-neutral-200 px-4 py-2.5 text-sm text-neutral-500 transition-colors hover:border-gold/30 hover:text-gold dark:border-white/[0.08] dark:text-white/40 dark:hover:text-gold cursor-pointer"
>
<Instagram size={16} />
Подробнее
</button>
)}
</div>
</div>
</div>
);
})}
</div>
</Reveal>
item={item}
onSignup={() => setSignupTitle(item.title)}
/>
))}
</div>
</Reveal>
)}
</div>