diff --git a/src/components/sections/MasterClasses.tsx b/src/components/sections/MasterClasses.tsx index 808b60f..609bba5 100644 --- a/src/components/sections/MasterClasses.tsx +++ b/src/components/sections/MasterClasses.tsx @@ -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 ( +
+ {/* Full-bleed image */} + {item.image && ( +
+ {item.title} + {/* Dark overlay that intensifies on hover */} +
+
+ )} + + {/* Content overlay at bottom */} +
+ {/* Tags row */} +
+ + {item.style} + + {duration && ( + + + {duration} + + )} +
+ + {/* Title */} +

+ {item.title} +

+ + {/* Trainer */} +
+ + {item.trainer} +
+ + {/* Divider */} +
+ + {/* Date + Location */} +
+
+ + {slotsDisplay} +
+ {item.location && ( +
+ + {item.location} +
+ )} +
+ + {/* Price + Actions */} +
+ + {item.instagramUrl && ( + + )} +
+ + {/* Price floating tag */} +
+ + {item.cost} + +
+
+
+ ); +} + export function MasterClasses({ data }: MasterClassesProps) { const [signupTitle, setSignupTitle] = useState(null); @@ -96,7 +196,7 @@ export function MasterClasses({ data }: MasterClassesProps) { return (
@@ -122,94 +222,17 @@ export function MasterClasses({ data }: MasterClassesProps) {
) : ( - -
- {upcoming.map((item, i) => { - const duration = item.slots[0] ? calcDuration(item.slots[0]) : ""; - const slotsDisplay = formatSlots(item.slots); - - return ( -
+
+ {upcoming.map((item, i) => ( + - {/* Image */} - {item.image && ( -
- {item.title} -
-
- - - {slotsDisplay} - -
-
- )} - - {/* Content */} -
-

- {item.title} -

- -
-
- - {item.trainer} -
-
- - {item.style} -
- {duration && ( -
- - {duration} -
- )} - {item.location && ( -
- - {item.location} -
- )} -
- -
- - {item.cost} - -
- -
- - {item.instagramUrl && ( - - )} -
-
-
- ); - })} -
- + item={item} + onSignup={() => setSignupTitle(item.title)} + /> + ))} +
+ )}
diff --git a/src/components/sections/News.tsx b/src/components/sections/News.tsx index bcb81fa..21b498f 100644 --- a/src/components/sections/News.tsx +++ b/src/components/sections/News.tsx @@ -1,8 +1,12 @@ +"use client"; + +import { useState } from "react"; import Image from "next/image"; import { Calendar, ExternalLink } from "lucide-react"; import { SectionHeading } from "@/components/ui/SectionHeading"; 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 { data: SiteContent["news"]; @@ -20,9 +24,93 @@ function formatDate(iso: string): string { } } +function FeaturedArticle({ + item, + onClick, +}: { + item: NewsItem; + onClick: () => void; +}) { + return ( +
+ {item.image && ( +
+ {item.title} +
+
+ )} +
+ + + {formatDate(item.date)} + +

+ {item.title} +

+

+ {item.text} +

+
+
+ ); +} + +function CompactArticle({ + item, + onClick, +}: { + item: NewsItem; + onClick: () => void; +}) { + return ( +
+ {item.image && ( +
+ {item.title} +
+ )} +
+ + {formatDate(item.date)} + +

+ {item.title} +

+

+ {item.text} +

+
+
+ ); +} + export function News({ data }: NewsProps) { + const [selected, setSelected] = useState(null); + if (!data.items || data.items.length === 0) return null; + const [featured, ...rest] = data.items; + return (
@@ -31,52 +119,31 @@ export function News({ data }: NewsProps) { {data.title} - -
- {data.items.map((item, i) => ( -
- {item.image && ( -
- {item.title} -
- )} -
-
- - {formatDate(item.date)} -
-

- {item.title} -

-

- {item.text} -

- {item.link && ( - - - Подробнее - - )} -
-
- ))} -
-
+
+ + setSelected(featured)} + /> + + + {rest.length > 0 && ( + +
+ {rest.map((item, i) => ( + setSelected(item)} + /> + ))} +
+
+ )} +
+ + setSelected(null)} />
); } diff --git a/src/components/ui/NewsModal.tsx b/src/components/ui/NewsModal.tsx new file mode 100644 index 0000000..1bd02f2 --- /dev/null +++ b/src/components/ui/NewsModal.tsx @@ -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( +
+
+ +
e.stopPropagation()} + > + + + {item.image && ( +
+ {item.title} +
+
+ )} + +
+ + + {formatDate(item.date)} + + +

+ {item.title} +

+ +

+ {item.text} +

+ + {item.link && ( + + Подробнее + + + )} +
+
+
, + document.body + ); +} diff --git a/src/middleware.ts b/src/proxy.ts similarity index 93% rename from src/middleware.ts rename to src/proxy.ts index f36f490..75edf39 100644 --- a/src/middleware.ts +++ b/src/proxy.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { verifyToken, COOKIE_NAME } from "@/lib/auth-edge"; -export async function middleware(request: NextRequest) { +export async function proxy(request: NextRequest) { const { pathname } = request.nextUrl; // Allow login page and login API