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:
2026-04-10 18:42:54 +03:00
parent bbe485d8fc
commit a587736dd3
74 changed files with 724 additions and 298 deletions
+11 -14
View File
@@ -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"