diff --git a/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515538630.jpg b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515538630.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515538630.jpg differ diff --git a/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515545926.jpg b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515545926.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774515545926.jpg differ diff --git a/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516098008.jpg b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516098008.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516098008.jpg differ diff --git a/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516126607.jpg b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516126607.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/news/saveclip-app-605404066-17852939781600031-474959415-1774516126607.jpg differ diff --git a/src/app/globals.css b/src/app/globals.css index 3c4c57f..0bc4b5c 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -83,3 +83,27 @@ body { .admin-scrollbar::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); } + +/* ===== Global page scrollbar ===== */ + +html { + scrollbar-width: thin; + scrollbar-color: rgba(201, 169, 110, 0.3) #0a0a0a; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: #0a0a0a; +} + +::-webkit-scrollbar-thumb { + background: rgba(201, 169, 110, 0.3); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(201, 169, 110, 0.5); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index c8bae17..5b0b142 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -124,12 +124,6 @@ export function Header() { ); })} -
diff --git a/src/components/sections/OpenDay.tsx b/src/components/sections/OpenDay.tsx index 5b9e6e1..4763932 100644 --- a/src/components/sections/OpenDay.tsx +++ b/src/components/sections/OpenDay.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useMemo } from "react"; -import { Calendar, Users, Sparkles } from "lucide-react"; +import { Calendar, Sparkles } from "lucide-react"; import { SectionHeading } from "@/components/ui/SectionHeading"; import { Reveal } from "@/components/ui/Reveal"; import { SignupModal } from "@/components/ui/SignupModal"; @@ -48,7 +48,8 @@ export function OpenDay({ data, popups }: OpenDayProps) { if (classes.length === 0) return null; return ( -
+
+
{event.title} @@ -179,11 +180,8 @@ function ClassCard({
{cls.startTime}–{cls.endTime} -

{cls.style}

-

- - {cls.trainer} -

+

{cls.trainer}

+

{cls.style}

{maxParticipants > 0 && (

{cls.bookingCount}/{maxParticipants} мест diff --git a/src/components/sections/Pricing.tsx b/src/components/sections/Pricing.tsx index 4ec1f5e..4fff60a 100644 --- a/src/components/sections/Pricing.tsx +++ b/src/components/sections/Pricing.tsx @@ -53,136 +53,128 @@ export function Pricing({ data: pricing }: PricingProps) { {/* Prices tab */} - {activeTab === "prices" && ( - -

-

- {pricing.subtitle} -

+
+
+

+ {pricing.subtitle} +

- {/* Cards grid */} -
- {regularItems.map((item, i) => { - const isPopular = item.popular ?? false; - return ( -
- {/* Popular badge */} - {isPopular && ( -
- - - Популярный - -
+ {/* Cards grid */} +
+ {regularItems.map((item, i) => { + const isPopular = item.popular ?? false; + return ( +
+ {/* Popular badge */} + {isPopular && ( +
+ + + Популярный + +
+ )} + +
+ {/* Name */} +

+ {item.name} +

+ + {/* Note */} + {item.note && ( +

+ {item.note} +

)} -
- {/* Name */} -

- {item.name} -

- - {/* Note */} - {item.note && ( -

- {item.note} -

- )} - - {/* Price */} -

- {item.price} -

-
+ {/* Price */} +

+ {item.price} +

- ); - })} -
- - {/* Featured — big card */} - {featuredItem && ( -
-
-
-
- -

- {featuredItem.name} -

-
- {featuredItem.note && ( -

- {featuredItem.note} -

- )} -
-

- {featuredItem.price} -

-
- )} + ); + })} +
-
- - )} - - {/* Rental tab */} - {activeTab === "rental" && ( - -
- {pricing.rentalItems.map((item, i) => ( -
-
-

- {item.name} -

- {item.note && ( -

- {item.note} + {/* Featured — big card */} + {featuredItem && ( +

+
+
+
+ +

+ {featuredItem.name} +

+
+ {featuredItem.note && ( +

+ {featuredItem.note}

)}
- - {item.price} - -
- ))} - -
- - )} - - {/* Rules tab */} - {activeTab === "rules" && ( - -
- {pricing.rules.map((rule, i) => ( -
- - {i + 1} - -

- {rule} +

+ {featuredItem.price}

- ))} -
-
- )} +
+ )} +
+
+ + {/* Rental tab */} +
+
+ {pricing.rentalItems.map((item, i) => ( +
+
+

+ {item.name} +

+ {item.note && ( +

+ {item.note} +

+ )} +
+ + {item.price} + +
+ ))} +
+
+ + {/* Rules tab */} +
+
+ {pricing.rules.map((rule, i) => ( +
+ + {i + 1} + +

+ {rule} +

+
+ ))} +
+
); diff --git a/src/components/sections/Schedule.tsx b/src/components/sections/Schedule.tsx index 00b195b..78b8fbb 100644 --- a/src/components/sections/Schedule.tsx +++ b/src/components/sections/Schedule.tsx @@ -10,7 +10,7 @@ import { ScheduleFilters } from "./schedule/ScheduleFilters"; import { MobileSchedule } from "./schedule/MobileSchedule"; import { GroupView } from "./schedule/GroupView"; import { buildTypeDots, shortAddress, startTimeMinutes, TIME_PRESETS } from "./schedule/constants"; -import type { StatusFilter, TimeFilter, ScheduleDayMerged, ScheduleClassWithLocation } from "./schedule/constants"; +import type { StatusTag, TimeFilter, ScheduleDayMerged, ScheduleClassWithLocation } from "./schedule/constants"; import type { SiteContent } from "@/types/content"; type ViewMode = "days" | "groups"; @@ -20,8 +20,8 @@ interface ScheduleState { locationMode: LocationMode; viewMode: ViewMode; filterTrainer: string | null; - filterType: string | null; - filterStatus: StatusFilter; + filterTypes: Set; + filterStatusSet: Set; filterTime: TimeFilter; filterDaySet: Set; bookingGroup: string | null; @@ -31,8 +31,8 @@ type ScheduleAction = | { type: "SET_LOCATION"; mode: LocationMode } | { type: "SET_VIEW"; mode: ViewMode } | { type: "SET_TRAINER"; value: string | null } - | { type: "SET_TYPE"; value: string | null } - | { type: "SET_STATUS"; value: StatusFilter } + | { type: "TOGGLE_TYPE"; value: string } + | { type: "TOGGLE_STATUS"; value: StatusTag } | { type: "SET_TIME"; value: TimeFilter } | { type: "TOGGLE_DAY"; day: string } | { type: "SET_BOOKING"; value: string | null } @@ -42,8 +42,8 @@ const initialState: ScheduleState = { locationMode: "all", viewMode: "groups", filterTrainer: null, - filterType: null, - filterStatus: "all", + filterTypes: new Set(), + filterStatusSet: new Set(), filterTime: "all", filterDaySet: new Set(), bookingGroup: null, @@ -57,10 +57,18 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule return { ...state, viewMode: action.mode }; case "SET_TRAINER": return { ...state, filterTrainer: action.value }; - case "SET_TYPE": - return { ...state, filterType: action.value }; - case "SET_STATUS": - return { ...state, filterStatus: action.value }; + case "TOGGLE_TYPE": { + const next = new Set(state.filterTypes); + if (next.has(action.value)) next.delete(action.value); + else next.add(action.value); + return { ...state, filterTypes: next }; + } + case "TOGGLE_STATUS": { + const next = new Set(state.filterStatusSet); + if (next.has(action.value)) next.delete(action.value); + else next.add(action.value); + return { ...state, filterStatusSet: next }; + } case "SET_TIME": return { ...state, filterTime: action.value }; case "TOGGLE_DAY": { @@ -72,7 +80,7 @@ function scheduleReducer(state: ScheduleState, action: ScheduleAction): Schedule case "SET_BOOKING": return { ...state, bookingGroup: action.value }; case "CLEAR_FILTERS": - return { ...state, filterTrainer: null, filterType: null, filterStatus: "all", filterTime: "all", filterDaySet: new Set() }; + return { ...state, filterTrainer: null, filterTypes: new Set(), filterStatusSet: new Set(), filterTime: "all", filterDaySet: new Set() }; } } @@ -84,7 +92,7 @@ interface ScheduleProps { export function Schedule({ data: schedule, classItems, teamMembers }: ScheduleProps) { const [state, dispatch] = useReducer(scheduleReducer, initialState); - const { locationMode, viewMode, filterTrainer, filterType, filterStatus, filterTime, filterDaySet, bookingGroup } = state; + const { locationMode, viewMode, filterTrainer, filterTypes, filterStatusSet, filterTime, filterDaySet, bookingGroup } = state; const isAllMode = locationMode === "all"; @@ -94,8 +102,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr }, []); const setFilterTrainer = useCallback((value: string | null) => dispatch({ type: "SET_TRAINER", value }), []); - const setFilterType = useCallback((value: string | null) => dispatch({ type: "SET_TYPE", value }), []); - const setFilterStatus = useCallback((value: StatusFilter) => dispatch({ type: "SET_STATUS", value }), []); + const toggleFilterType = useCallback((value: string) => dispatch({ type: "TOGGLE_TYPE", value }), []); + const toggleFilterStatus = useCallback((value: StatusTag) => dispatch({ type: "TOGGLE_STATUS", value }), []); const setFilterTime = useCallback((value: TimeFilter) => dispatch({ type: "SET_TIME", value }), []); const setFilterTrainerFromCard = useCallback((trainer: string | null) => { @@ -103,9 +111,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr if (trainer) scrollToSchedule(); }, [scrollToSchedule]); - const setFilterTypeFromCard = useCallback((type: string | null) => { - dispatch({ type: "SET_TYPE", value: type }); - if (type) scrollToSchedule(); + const toggleFilterTypeFromCard = useCallback((type: string) => { + dispatch({ type: "TOGGLE_TYPE", value: type }); + scrollToSchedule(); }, [scrollToSchedule]); const typeDots = useMemo(() => buildTypeDots(classItems), [classItems]); @@ -186,7 +194,7 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr : null; const filteredDays: ScheduleDayMerged[] = useMemo(() => { - const noFilter = !filterTrainer && !filterType && filterStatus === "all" && filterTime === "all" && filterDaySet.size === 0; + const noFilter = !filterTrainer && filterTypes.size === 0 && filterStatusSet.size === 0 && filterTime === "all" && filterDaySet.size === 0; if (noFilter) return activeDays; // First filter by day names if any selected @@ -200,10 +208,10 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr classes: day.classes.filter( (cls) => (!filterTrainer || cls.trainer === filterTrainer) && - (!filterType || cls.type === filterType) && - (filterStatus === "all" || - (filterStatus === "hasSlots" && cls.hasSlots) || - (filterStatus === "recruiting" && cls.recruiting)) && + (filterTypes.size === 0 || filterTypes.has(cls.type)) && + (filterStatusSet.size === 0 || + (filterStatusSet.has("hasSlots") && cls.hasSlots) || + (filterStatusSet.has("recruiting") && cls.recruiting)) && (!activeTimeRange || (() => { const m = startTimeMinutes(cls.time); return m >= activeTimeRange[0] && m < activeTimeRange[1]; @@ -211,9 +219,9 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr ), })) .filter((day) => day.classes.length > 0); - }, [activeDays, filterTrainer, filterType, filterStatus, filterTime, activeTimeRange, filterDaySet]); + }, [activeDays, filterTrainer, filterTypes, filterStatusSet, filterTime, activeTimeRange, filterDaySet]); - const hasActiveFilter = !!(filterTrainer || filterType || filterStatus !== "all" || filterTime !== "all" || filterDaySet.size > 0); + const hasActiveFilter = !!(filterTrainer || filterTypes.size > 0 || filterStatusSet.size > 0 || filterTime !== "all" || filterDaySet.size > 0); function clearFilters() { dispatch({ type: "CLEAR_FILTERS" }); @@ -338,11 +346,11 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr types={types} hasAnySlots={hasAnySlots} hasAnyRecruiting={hasAnyRecruiting} - filterType={filterType} - setFilterType={setFilterType} + filterTypes={filterTypes} + toggleFilterType={toggleFilterType} filterTrainer={filterTrainer} - filterStatus={filterStatus} - setFilterStatus={setFilterStatus} + filterStatusSet={filterStatusSet} + toggleFilterStatus={toggleFilterStatus} filterTime={filterTime} setFilterTime={setFilterTime} availableDays={availableDays} @@ -361,8 +369,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr - +
))} @@ -400,8 +408,8 @@ export function Schedule({ data: schedule, classItems, teamMembers }: SchedulePr { + setActiveIndex(index); + setShowProfile(true); + history.pushState({ trainerProfile: true }, ""); + }, []); + + const closeProfile = useCallback(() => { + setShowProfile(false); + }, []); + const openTrainerByName = useCallback((name: string) => { const idx = team.members.findIndex((m) => m.name === name); if (idx >= 0) { - setActiveIndex(idx); - setShowProfile(true); + openProfile(idx); setTimeout(() => { const el = document.getElementById("team"); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50); } - }, [team.members]); + }, [team.members, openProfile]); + + // Handle browser back button + useEffect(() => { + function onPopState(e: PopStateEvent) { + if (showProfile) { + e.preventDefault(); + setShowProfile(false); + } + } + window.addEventListener("popstate", onPopState); + return () => window.removeEventListener("popstate", onPopState); + }, [showProfile]); useEffect(() => { function handler(e: Event) { @@ -75,14 +96,14 @@ export function Team({ data: team, schedule }: TeamProps) { members={team.members} activeIndex={activeIndex} onSelect={setActiveIndex} - onOpenBio={() => setShowProfile(true)} + onOpenBio={() => openProfile(activeIndex)} /> ) : ( setShowProfile(false)} + onBack={() => { history.back(); }} schedule={schedule} /> )} diff --git a/src/components/sections/schedule/DayCard.tsx b/src/components/sections/schedule/DayCard.tsx index 9918b1b..9f650ca 100644 --- a/src/components/sections/schedule/DayCard.tsx +++ b/src/components/sections/schedule/DayCard.tsx @@ -8,8 +8,8 @@ interface DayCardProps { showLocation?: boolean; filterTrainer: string | null; setFilterTrainer: (trainer: string | null) => void; - filterType: string | null; - setFilterType: (type: string | null) => void; + filterTypes: Set; + toggleFilterType: (type: string) => void; } function ClassRow({ @@ -17,15 +17,15 @@ function ClassRow({ typeDots, filterTrainer, setFilterTrainer, - filterType, - setFilterType, + filterTypes, + toggleFilterType, }: { cls: ScheduleClassWithLocation; typeDots: Record; filterTrainer: string | null; setFilterTrainer: (trainer: string | null) => void; - filterType: string | null; - setFilterType: (type: string | null) => void; + filterTypes: Set; + toggleFilterType: (type: string) => void; }) { return (
@@ -58,12 +58,12 @@ function ClassRow({
{classes.map((cls, i) => ( - + ))}
@@ -133,7 +133,7 @@ export function DayCard({ day, typeDots, showLocation, filterTrainer, setFilterT // Single location — no sub-headers
{day.classes.map((cls, i) => ( - + ))}
)} diff --git a/src/components/sections/schedule/GroupView.tsx b/src/components/sections/schedule/GroupView.tsx index 52e931f..9824af5 100644 --- a/src/components/sections/schedule/GroupView.tsx +++ b/src/components/sections/schedule/GroupView.tsx @@ -119,8 +119,8 @@ function groupByType(groups: ScheduleGroup[]): { type: string; groups: ScheduleG interface GroupViewProps { typeDots: Record; filteredDays: ScheduleDayMerged[]; - filterType: string | null; - setFilterType: (type: string | null) => void; + filterTypes: Set; + toggleFilterType: (type: string) => void; filterTrainer: string | null; setFilterTrainer: (trainer: string | null) => void; showLocation?: boolean; @@ -133,8 +133,8 @@ const WEEKDAY_NAMES = ["Воскресенье", "Понедельник", "Вт export function GroupView({ typeDots, filteredDays, - filterType, - setFilterType, + filterTypes, + toggleFilterType, filterTrainer, setFilterTrainer, showLocation, @@ -226,11 +226,11 @@ export function GroupView({ {/* Type name */}
{group.level && ( diff --git a/src/components/sections/schedule/MobileSchedule.tsx b/src/components/sections/schedule/MobileSchedule.tsx index 5fd971d..62359aa 100644 --- a/src/components/sections/schedule/MobileSchedule.tsx +++ b/src/components/sections/schedule/MobileSchedule.tsx @@ -7,8 +7,8 @@ import type { ScheduleDayMerged, ScheduleClassWithLocation } from "./constants"; interface MobileScheduleProps { typeDots: Record; filteredDays: ScheduleDayMerged[]; - filterType: string | null; - setFilterType: (type: string | null) => void; + filterTypes: Set; + toggleFilterType: (type: string) => void; filterTrainer: string | null; setFilterTrainer: (trainer: string | null) => void; hasActiveFilter: boolean; @@ -19,16 +19,16 @@ interface MobileScheduleProps { function ClassRow({ cls, typeDots, - filterType, - setFilterType, + filterTypes, + toggleFilterType, filterTrainer, setFilterTrainer, showLocation, }: { cls: ScheduleClassWithLocation; typeDots: Record; - filterType: string | null; - setFilterType: (type: string | null) => void; + filterTypes: Set; + toggleFilterType: (type: string) => void; filterTrainer: string | null; setFilterTrainer: (trainer: string | null) => void; showLocation?: boolean; @@ -69,11 +69,11 @@ function ClassRow({
{showLocation && cls.locationName && ( @@ -90,8 +90,8 @@ function ClassRow({ export function MobileSchedule({ typeDots, filteredDays, - filterType, - setFilterType, + filterTypes, + toggleFilterType, filterTrainer, setFilterTrainer, hasActiveFilter, @@ -110,12 +110,12 @@ export function MobileSchedule({ {filterTrainer} )} - {filterType && ( - - - {filterType} + {filterTypes.size > 0 && Array.from(filterTypes).map((type) => ( + + + {type} - )} + ))}