feat: mobile UX, admin polish, rate limiting, and media assets
- Mobile responsiveness improvements across admin and public sections - Admin: bookings modal, open-day page, team page, layout polish - Added rate limiting, CSRF hardening, auth-edge improvements - Scroll reveal, floating contact, back-to-top, Yandex map fixes - Schedule filters refactor, team profile/info component updates - New useTrainerPhotos hook - Added class, team, master-class, and news images
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useReducer, useMemo, useCallback } from "react";
|
||||
import { useTrainerPhotos } from "@/hooks/useTrainerPhotos";
|
||||
import { SignupModal } from "@/components/ui/SignupModal";
|
||||
import { CalendarDays, Users, LayoutGrid, SlidersHorizontal, MapPin } from "lucide-react";
|
||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||
@@ -135,15 +136,7 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
||||
|
||||
const typeDots = useMemo(() => buildTypeDots(classItems), [classItems]);
|
||||
|
||||
const trainerPhotos = useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
if (teamMembers) {
|
||||
for (const m of teamMembers) {
|
||||
if (m.image) map[m.name] = m.image;
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}, [teamMembers]);
|
||||
const trainerPhotos = useTrainerPhotos(teamMembers);
|
||||
|
||||
// Build days: either from one location or merged from all
|
||||
const activeDays: ScheduleDayMerged[] = useMemo(() => {
|
||||
@@ -251,14 +244,14 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
||||
classes: day.classes.filter(
|
||||
(cls) => {
|
||||
const clsStatus = cls.status || (cls.recruiting ? "recruiting" : cls.hasSlots ? "hasSlots" : "");
|
||||
const matchesTime = !activeTimeRange || (
|
||||
startTimeMinutes(cls.time) >= activeTimeRange[0] && startTimeMinutes(cls.time) < activeTimeRange[1]
|
||||
);
|
||||
return (filterTrainerSet.size === 0 || filterTrainerSet.has(cls.trainer)) &&
|
||||
(filterTypes.size === 0 || filterTypes.has(cls.type)) &&
|
||||
(filterStatusSet.size === 0 || (clsStatus && filterStatusSet.has(clsStatus))) &&
|
||||
(!filterLevel || cls.level === filterLevel) &&
|
||||
(!activeTimeRange || (() => {
|
||||
const m = startTimeMinutes(cls.time);
|
||||
return m >= activeTimeRange[0] && m < activeTimeRange[1];
|
||||
})());
|
||||
matchesTime;
|
||||
}),
|
||||
}))
|
||||
.filter((day) => day.classes.length > 0);
|
||||
@@ -384,8 +377,10 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
||||
{/* View mode toggle + filter button */}
|
||||
<Reveal>
|
||||
<div className="mt-4 hidden sm:flex items-center justify-center">
|
||||
<div className="inline-flex items-center rounded-xl border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.08] dark:bg-white/[0.04]">
|
||||
<div className="inline-flex items-center rounded-xl border border-neutral-200 bg-neutral-100 p-1 dark:border-white/[0.08] dark:bg-white/[0.04]" role="tablist" aria-label="Режим отображения">
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={viewMode === "days"}
|
||||
onClick={() => dispatch({ type: "SET_VIEW", mode: "days" })}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-xs font-medium transition-all duration-200 cursor-pointer ${
|
||||
viewMode === "days"
|
||||
@@ -397,6 +392,8 @@ export function Schedule({ data: schedule, scheduleConfig, classItems, teamMembe
|
||||
По дням
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
aria-selected={viewMode === "groups"}
|
||||
onClick={() => dispatch({ type: "SET_VIEW", mode: "groups" })}
|
||||
className={`inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-xs font-medium transition-all duration-200 cursor-pointer ${
|
||||
viewMode === "groups"
|
||||
|
||||
Reference in New Issue
Block a user