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:
@@ -35,7 +35,6 @@ function formatSlots(slots: MasterClassSlot[]): string {
|
|||||||
const dates = sorted.map((s) => parseDate(s.date)).filter((d) => !isNaN(d.getTime()));
|
const dates = sorted.map((s) => parseDate(s.date)).filter((d) => !isNaN(d.getTime()));
|
||||||
if (dates.length === 0) return "";
|
if (dates.length === 0) return "";
|
||||||
|
|
||||||
// Time part from first slot
|
|
||||||
const timePart = sorted[0].startTime
|
const timePart = sorted[0].startTime
|
||||||
? `, ${sorted[0].startTime}–${sorted[0].endTime}`
|
? `, ${sorted[0].startTime}–${sorted[0].endTime}`
|
||||||
: "";
|
: "";
|
||||||
@@ -80,6 +79,107 @@ function isUpcoming(item: MasterClassItem): boolean {
|
|||||||
return lastDate >= today;
|
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) {
|
export function MasterClasses({ data }: MasterClassesProps) {
|
||||||
const [signupTitle, setSignupTitle] = useState<string | null>(null);
|
const [signupTitle, setSignupTitle] = useState<string | null>(null);
|
||||||
|
|
||||||
@@ -96,7 +196,7 @@ export function MasterClasses({ data }: MasterClassesProps) {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="master-classes"
|
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" />
|
<div className="section-divider absolute top-0 left-0 right-0" />
|
||||||
|
|
||||||
@@ -123,91 +223,14 @@ export function MasterClasses({ data }: MasterClassesProps) {
|
|||||||
</Reveal>
|
</Reveal>
|
||||||
) : (
|
) : (
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="mt-10 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<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) => {
|
{upcoming.map((item, i) => (
|
||||||
const duration = item.slots[0] ? calcDuration(item.slots[0]) : "";
|
<MasterClassCard
|
||||||
const slotsDisplay = formatSlots(item.slots);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={i}
|
key={i}
|
||||||
className="group rounded-2xl border border-neutral-200 bg-white overflow-hidden transition-colors dark:border-white/[0.06] dark:bg-[#0a0a0a]"
|
item={item}
|
||||||
>
|
onSignup={() => setSignupTitle(item.title)}
|
||||||
{/* 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>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Calendar, ExternalLink } from "lucide-react";
|
import { Calendar, ExternalLink } from "lucide-react";
|
||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||||
import { Reveal } from "@/components/ui/Reveal";
|
import { Reveal } from "@/components/ui/Reveal";
|
||||||
import type { SiteContent } from "@/types/content";
|
import { NewsModal } from "@/components/ui/NewsModal";
|
||||||
|
import type { SiteContent, NewsItem } from "@/types/content";
|
||||||
|
|
||||||
interface NewsProps {
|
interface NewsProps {
|
||||||
data: SiteContent["news"];
|
data: SiteContent["news"];
|
||||||
@@ -20,9 +24,93 @@ function formatDate(iso: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FeaturedArticle({
|
||||||
|
item,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
item: NewsItem;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className="group relative overflow-hidden rounded-3xl cursor-pointer"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{item.image && (
|
||||||
|
<div className="relative aspect-[21/9] sm:aspect-[2/1] overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={item.image}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 768px) 80vw, 100vw"
|
||||||
|
className="object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/30 to-transparent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${item.image ? "absolute bottom-0 left-0 right-0 p-6 sm:p-8" : "p-6 sm:p-8 bg-neutral-900 rounded-3xl"}`}
|
||||||
|
>
|
||||||
|
<span className="inline-flex items-center gap-1.5 rounded-full bg-white/15 px-3 py-1 text-xs font-medium text-white/80 backdrop-blur-sm">
|
||||||
|
<Calendar size={12} />
|
||||||
|
{formatDate(item.date)}
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-3 text-xl sm:text-2xl font-bold text-white leading-tight">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 max-w-2xl text-sm leading-relaxed text-white/70 line-clamp-3">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CompactArticle({
|
||||||
|
item,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
item: NewsItem;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className="group flex gap-4 items-start py-5 border-b border-neutral-200/60 last:border-0 dark:border-white/[0.06] cursor-pointer"
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{item.image && (
|
||||||
|
<div className="relative w-24 h-24 sm:w-28 sm:h-28 shrink-0 overflow-hidden rounded-xl">
|
||||||
|
<Image
|
||||||
|
src={item.image}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
sizes="112px"
|
||||||
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<span className="text-xs text-neutral-400 dark:text-white/30">
|
||||||
|
{formatDate(item.date)}
|
||||||
|
</span>
|
||||||
|
<h3 className="mt-1 text-sm sm:text-base font-bold text-neutral-900 dark:text-white leading-snug line-clamp-2 group-hover:text-gold transition-colors">
|
||||||
|
{item.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-sm leading-relaxed text-neutral-500 dark:text-neutral-400 line-clamp-2">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function News({ data }: NewsProps) {
|
export function News({ data }: NewsProps) {
|
||||||
|
const [selected, setSelected] = useState<NewsItem | null>(null);
|
||||||
|
|
||||||
if (!data.items || data.items.length === 0) return null;
|
if (!data.items || data.items.length === 0) return null;
|
||||||
|
|
||||||
|
const [featured, ...rest] = data.items;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="news" className="section-glow relative section-padding">
|
<section id="news" className="section-glow relative section-padding">
|
||||||
<div className="section-divider absolute top-0 left-0 right-0" />
|
<div className="section-divider absolute top-0 left-0 right-0" />
|
||||||
@@ -31,52 +119,31 @@ export function News({ data }: NewsProps) {
|
|||||||
<SectionHeading centered>{data.title}</SectionHeading>
|
<SectionHeading centered>{data.title}</SectionHeading>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
|
||||||
|
<div className="mx-auto mt-10 max-w-4xl space-y-6">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="mx-auto mt-10 max-w-4xl grid gap-4 sm:grid-cols-2">
|
<FeaturedArticle
|
||||||
{data.items.map((item, i) => (
|
item={featured}
|
||||||
<article
|
onClick={() => setSelected(featured)}
|
||||||
key={i}
|
/>
|
||||||
className="group rounded-2xl border border-neutral-200 bg-white overflow-hidden transition-all duration-300 hover:shadow-lg dark:border-white/[0.06] dark:bg-[#0a0a0a] dark:hover:border-white/[0.12]"
|
</Reveal>
|
||||||
>
|
|
||||||
{item.image && (
|
{rest.length > 0 && (
|
||||||
<div className="relative aspect-[2/1] overflow-hidden">
|
<Reveal>
|
||||||
<Image
|
<div className="rounded-2xl bg-neutral-50/80 px-5 sm:px-6 dark:bg-white/[0.02]">
|
||||||
src={item.image}
|
{rest.map((item, i) => (
|
||||||
alt={item.title}
|
<CompactArticle
|
||||||
fill
|
key={i}
|
||||||
sizes="(min-width: 640px) 50vw, 100vw"
|
item={item}
|
||||||
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
onClick={() => setSelected(item)}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="flex items-center gap-1.5 text-xs text-neutral-400 dark:text-white/30">
|
|
||||||
<Calendar size={12} />
|
|
||||||
{formatDate(item.date)}
|
|
||||||
</div>
|
|
||||||
<h3 className="mt-2 text-base font-bold text-neutral-900 dark:text-white">
|
|
||||||
{item.title}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-2 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400 line-clamp-4">
|
|
||||||
{item.text}
|
|
||||||
</p>
|
|
||||||
{item.link && (
|
|
||||||
<a
|
|
||||||
href={item.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="mt-3 inline-flex items-center gap-1.5 text-sm font-medium text-gold-dark hover:text-gold transition-colors dark:text-gold-light"
|
|
||||||
>
|
|
||||||
<ExternalLink size={14} />
|
|
||||||
Подробнее
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NewsModal item={selected} onClose={() => setSelected(null)} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
110
src/components/ui/NewsModal.tsx
Normal file
110
src/components/ui/NewsModal.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { X, Calendar, ExternalLink } from "lucide-react";
|
||||||
|
import type { NewsItem } from "@/types/content";
|
||||||
|
|
||||||
|
interface NewsModalProps {
|
||||||
|
item: NewsItem | null;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(iso: string): string {
|
||||||
|
try {
|
||||||
|
return new Date(iso).toLocaleDateString("ru-RU", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return iso;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewsModal({ item, onClose }: NewsModalProps) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!item) return;
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
|
}
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
return () => document.removeEventListener("keydown", onKey);
|
||||||
|
}, [item, onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (item) {
|
||||||
|
document.body.style.overflow = "hidden";
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = "";
|
||||||
|
};
|
||||||
|
}, [item]);
|
||||||
|
|
||||||
|
if (!item) return null;
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className="modal-overlay fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<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-white/[0.08] bg-[#0a0a0a] shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute right-4 top-4 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-black/50 text-neutral-400 backdrop-blur-sm transition-colors hover:bg-white/[0.1] hover:text-white cursor-pointer"
|
||||||
|
>
|
||||||
|
<X size={18} />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{item.image && (
|
||||||
|
<div className="relative aspect-[2/1] w-full overflow-hidden rounded-t-2xl">
|
||||||
|
<Image
|
||||||
|
src={item.image}
|
||||||
|
alt={item.title}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 768px) 672px, 100vw"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-[#0a0a0a] via-transparent to-transparent" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={`p-6 sm:p-8 ${item.image ? "-mt-12 relative" : ""}`}>
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs text-neutral-400">
|
||||||
|
<Calendar size={12} />
|
||||||
|
{formatDate(item.date)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h2 className="mt-2 text-xl sm:text-2xl font-bold text-white leading-tight">
|
||||||
|
{item.title}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="mt-4 text-sm sm:text-base leading-relaxed text-neutral-300 whitespace-pre-line">
|
||||||
|
{item.text}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{item.link && (
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="mt-6 inline-flex items-center gap-2 rounded-xl bg-gold px-5 py-2.5 text-sm font-semibold text-black transition-all hover:bg-gold-light hover:shadow-lg hover:shadow-gold/20"
|
||||||
|
>
|
||||||
|
Подробнее
|
||||||
|
<ExternalLink size={14} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { verifyToken, COOKIE_NAME } from "@/lib/auth-edge";
|
import { verifyToken, COOKIE_NAME } from "@/lib/auth-edge";
|
||||||
|
|
||||||
export async function middleware(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const { pathname } = request.nextUrl;
|
const { pathname } = request.nextUrl;
|
||||||
|
|
||||||
// Allow login page and login API
|
// Allow login page and login API
|
||||||
Reference in New Issue
Block a user