From 66dce3f8f5c848f796316eaa9bee3c7e8b0098e5 Mon Sep 17 00:00:00 2001 From: "diana.dolgolyova" Date: Thu, 19 Mar 2026 14:01:21 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20HIGH=20priority=20=E2=80=94=20scroll=20d?= =?UTF-8?q?ebounce,=20timing-safe=20auth,=20a11y,=20error=20logging,=20cle?= =?UTF-8?q?anup=20dead=20modals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header: throttle scroll handler via requestAnimationFrame (was firing 60+/sec) - Auth: use crypto.timingSafeEqual for password and token signature comparison - A11y: add role="dialog", aria-modal, aria-label to all modals (SignupModal, NewsModal, TeamProfile lightbox) - A11y: add aria-label to close buttons, menu toggle (with aria-expanded), floating CTA - A11y: add aria-label to MC Instagram buttons - Error logging: add console.error with route names to all API catch blocks (admin + public) - Fix open-day-register error leak (was returning raw DB error to client) - Fix MasterClasses key={index} → key={item.title} - Delete 3 unused modal components (BookingModal, MasterClassSignupModal, OpenDaySignupModal) — replaced by unified SignupModal Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/admin/group-bookings/route.ts | 3 +- src/app/api/admin/mc-registrations/route.ts | 6 +- src/app/api/admin/open-day/bookings/route.ts | 3 +- src/app/api/admin/open-day/classes/route.ts | 3 +- src/app/api/admin/open-day/route.ts | 6 +- src/app/api/admin/reminders/route.ts | 3 +- src/app/api/admin/validate-instagram/route.ts | 3 +- src/app/api/group-booking/route.ts | 3 +- src/app/api/master-class-register/route.ts | 3 +- src/app/api/open-day-register/route.ts | 3 +- src/components/layout/Header.tsx | 13 +- src/components/sections/MasterClasses.tsx | 5 +- src/components/sections/team/TeamProfile.tsx | 4 + src/components/ui/BookingModal.tsx | 214 ------------------ src/components/ui/MasterClassSignupModal.tsx | 195 ---------------- src/components/ui/NewsModal.tsx | 4 + src/components/ui/OpenDaySignupModal.tsx | 210 ----------------- src/components/ui/SignupModal.tsx | 3 +- src/lib/auth.ts | 10 +- 19 files changed, 56 insertions(+), 638 deletions(-) delete mode 100644 src/components/ui/BookingModal.tsx delete mode 100644 src/components/ui/MasterClassSignupModal.tsx delete mode 100644 src/components/ui/OpenDaySignupModal.tsx diff --git a/src/app/api/admin/group-bookings/route.ts b/src/app/api/admin/group-bookings/route.ts index 5e52739..6fa5fb4 100644 --- a/src/app/api/admin/group-bookings/route.ts +++ b/src/app/api/admin/group-bookings/route.ts @@ -21,7 +21,8 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ ok: true }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); - } catch { + } catch (err) { + console.error("[admin/group-bookings] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/mc-registrations/route.ts b/src/app/api/admin/mc-registrations/route.ts index 91f3a95..7ac3267 100644 --- a/src/app/api/admin/mc-registrations/route.ts +++ b/src/app/api/admin/mc-registrations/route.ts @@ -19,7 +19,8 @@ export async function POST(request: NextRequest) { } const id = addMcRegistration(masterClassTitle.trim(), name.trim(), instagram.trim(), telegram?.trim() || undefined); return NextResponse.json({ ok: true, id }); - } catch { + } catch (err) { + console.error("[admin/mc-registrations] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } @@ -48,7 +49,8 @@ export async function PUT(request: NextRequest) { } updateMcRegistration(id, name.trim(), instagram.trim(), telegram?.trim() || undefined); return NextResponse.json({ ok: true }); - } catch { + } catch (err) { + console.error("[admin/mc-registrations] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/open-day/bookings/route.ts b/src/app/api/admin/open-day/bookings/route.ts index ee7055e..3aaf08f 100644 --- a/src/app/api/admin/open-day/bookings/route.ts +++ b/src/app/api/admin/open-day/bookings/route.ts @@ -28,7 +28,8 @@ export async function PUT(request: NextRequest) { return NextResponse.json({ ok: true }); } return NextResponse.json({ error: "Unknown action" }, { status: 400 }); - } catch { + } catch (err) { + console.error("[admin/open-day/bookings] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/open-day/classes/route.ts b/src/app/api/admin/open-day/classes/route.ts index 08a1344..f920510 100644 --- a/src/app/api/admin/open-day/classes/route.ts +++ b/src/app/api/admin/open-day/classes/route.ts @@ -39,7 +39,8 @@ export async function PUT(request: NextRequest) { const { id, ...data } = body; updateOpenDayClass(id, data); return NextResponse.json({ ok: true }); - } catch { + } catch (err) { + console.error("[admin/open-day/classes] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/open-day/route.ts b/src/app/api/admin/open-day/route.ts index 04ced9b..ff89cf2 100644 --- a/src/app/api/admin/open-day/route.ts +++ b/src/app/api/admin/open-day/route.ts @@ -27,7 +27,8 @@ export async function POST(request: NextRequest) { } const id = createOpenDayEvent(body); return NextResponse.json({ ok: true, id }); - } catch { + } catch (err) { + console.error("[admin/open-day] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } @@ -39,7 +40,8 @@ export async function PUT(request: NextRequest) { const { id, ...data } = body; updateOpenDayEvent(id, data); return NextResponse.json({ ok: true }); - } catch { + } catch (err) { + console.error("[admin/open-day] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/reminders/route.ts b/src/app/api/admin/reminders/route.ts index 7437a6c..4abddb8 100644 --- a/src/app/api/admin/reminders/route.ts +++ b/src/app/api/admin/reminders/route.ts @@ -30,7 +30,8 @@ export async function PUT(request: NextRequest) { status as ReminderStatus | null ); return NextResponse.json({ ok: true }); - } catch { + } catch (err) { + console.error("[admin/reminders] error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/admin/validate-instagram/route.ts b/src/app/api/admin/validate-instagram/route.ts index 8cfd534..8278d2a 100644 --- a/src/app/api/admin/validate-instagram/route.ts +++ b/src/app/api/admin/validate-instagram/route.ts @@ -20,7 +20,8 @@ export async function GET(request: NextRequest) { // Instagram returns 200 for existing profiles, 404 for non-existing const valid = res.ok; return NextResponse.json({ valid }); - } catch { + } catch (err) { + console.error("[admin/validate-instagram] error:", err); // Network error or timeout — don't block the user return NextResponse.json({ valid: true, uncertain: true }); } diff --git a/src/app/api/group-booking/route.ts b/src/app/api/group-booking/route.ts index f409fcf..a31ce7f 100644 --- a/src/app/api/group-booking/route.ts +++ b/src/app/api/group-booking/route.ts @@ -27,7 +27,8 @@ export async function POST(request: NextRequest) { const id = addGroupBooking(cleanName, cleanPhone, cleanGroup, cleanIg, cleanTg); return NextResponse.json({ ok: true, id }); - } catch { + } catch (err) { + console.error("[group-booking] POST error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/master-class-register/route.ts b/src/app/api/master-class-register/route.ts index d43eb2e..2c461d8 100644 --- a/src/app/api/master-class-register/route.ts +++ b/src/app/api/master-class-register/route.ts @@ -38,7 +38,8 @@ export async function POST(request: Request) { ); return NextResponse.json({ ok: true, id }); - } catch { + } catch (err) { + console.error("[master-class-register] POST error:", err); return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/app/api/open-day-register/route.ts b/src/app/api/open-day-register/route.ts index 64ae02d..66a2a22 100644 --- a/src/app/api/open-day-register/route.ts +++ b/src/app/api/open-day-register/route.ts @@ -58,6 +58,7 @@ export async function POST(request: NextRequest) { if (msg.includes("UNIQUE")) { return NextResponse.json({ error: "Вы уже записаны на это занятие" }, { status: 409 }); } - return NextResponse.json({ error: msg }, { status: 500 }); + console.error("[open-day-register] POST error:", e); + return NextResponse.json({ error: "Internal error" }, { status: 500 }); } } diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index f4860ef..d5c89d2 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -15,8 +15,15 @@ export function Header() { const [bookingOpen, setBookingOpen] = useState(false); useEffect(() => { + let ticking = false; function handleScroll() { - setScrolled(window.scrollY > UI_CONFIG.scrollThresholds.header); + if (!ticking) { + ticking = true; + requestAnimationFrame(() => { + setScrolled(window.scrollY > UI_CONFIG.scrollThresholds.header); + ticking = false; + }); + } } window.addEventListener("scroll", handleScroll, { passive: true }); return () => window.removeEventListener("scroll", handleScroll); @@ -128,7 +135,8 @@ export function Header() {
- - {submitted ? ( - /* Success state */ -
-
- -
-

Отлично!

-

- Сообщение отправлено в Instagram. Мы свяжемся с вами в ближайшее время! -

- -
- ) : ( - <> - {/* Header */} -
-

Записаться

-

- Оставьте данные и мы свяжемся с вами, или напишите нам напрямую -

-
- - {/* Form */} -
-
- setName(e.target.value)} - placeholder="Ваше имя" - required - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
-
- handlePhoneChange(e.target.value)} - placeholder="+375 (__) ___-__-__" - required - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
- - -
- - {/* Divider */} -
- - или напрямую - -
- - {/* Direct links */} -
- - - Instagram - - - - Позвонить - -
- - )} -
- , - document.body - ); -} diff --git a/src/components/ui/MasterClassSignupModal.tsx b/src/components/ui/MasterClassSignupModal.tsx deleted file mode 100644 index deb1f50..0000000 --- a/src/components/ui/MasterClassSignupModal.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback } from "react"; -import { createPortal } from "react-dom"; -import { X, Instagram, Send, CheckCircle } from "lucide-react"; - -interface MasterClassSignupModalProps { - open: boolean; - onClose: () => void; - masterClassTitle: string; - successMessage?: string; -} - -export function MasterClassSignupModal({ - open, - onClose, - masterClassTitle, - successMessage, -}: MasterClassSignupModalProps) { - const [name, setName] = useState(""); - const [instagram, setInstagram] = useState(""); - const [telegram, setTelegram] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [submitted, setSubmitted] = useState(false); - const [error, setError] = useState(""); - - // Close on Escape - useEffect(() => { - if (!open) return; - function onKey(e: KeyboardEvent) { - if (e.key === "Escape") onClose(); - } - document.addEventListener("keydown", onKey); - return () => document.removeEventListener("keydown", onKey); - }, [open, onClose]); - - // Lock body scroll - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - - const handleSubmit = useCallback( - async (e: React.FormEvent) => { - e.preventDefault(); - setError(""); - setSubmitting(true); - - try { - const res = await fetch("/api/master-class-register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - masterClassTitle, - name: name.trim(), - instagram: `@${instagram.trim()}`, - telegram: telegram.trim() ? `@${telegram.trim()}` : undefined, - }), - }); - - if (!res.ok) { - const data = await res.json(); - throw new Error(data.error || "Ошибка регистрации"); - } - - setSubmitted(true); - } catch (err) { - setError(err instanceof Error ? err.message : "Ошибка регистрации"); - } finally { - setSubmitting(false); - } - }, - [masterClassTitle, name, instagram, telegram] - ); - - const handleClose = useCallback(() => { - onClose(); - setTimeout(() => { - setName(""); - setInstagram(""); - setTelegram(""); - setSubmitted(false); - setError(""); - }, 300); - }, [onClose]); - - if (!open) return null; - - return createPortal( -
-
- -
e.stopPropagation()} - > - - - {submitted ? ( -
-
- -
-

Отлично!

-

- {successMessage || "Вы записаны! Мы свяжемся с вами"} -

- -
- ) : ( - <> -
-

Записаться

-

{masterClassTitle}

-
- -
-
- setName(e.target.value)} - placeholder="Ваше имя" - required - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
- -
- - - @ - - setInstagram(e.target.value.replace(/^@/, ""))} - placeholder="username" - required - className="flex-1 bg-transparent px-2 py-3 text-sm text-white placeholder-neutral-500 outline-none" - /> -
- -
- - - @ - - setTelegram(e.target.value.replace(/^@/, ""))} - placeholder="username (необязательно)" - className="flex-1 bg-transparent px-2 py-3 text-sm text-white placeholder-neutral-500 outline-none" - /> -
- - {error && ( -

{error}

- )} - - -
- - )} -
-
, - document.body - ); -} diff --git a/src/components/ui/NewsModal.tsx b/src/components/ui/NewsModal.tsx index 1bd02f2..45d76ed 100644 --- a/src/components/ui/NewsModal.tsx +++ b/src/components/ui/NewsModal.tsx @@ -49,6 +49,9 @@ export function NewsModal({ item, onClose }: NewsModalProps) { return createPortal(
@@ -59,6 +62,7 @@ export function NewsModal({ item, onClose }: NewsModalProps) { > - - {result ? ( -
-
- -
-

Вы записаны!

-

{classLabel}

-

- Вы записаны на {result.totalBookings} занятий. -
- Стоимость: {result.pricePerClass} BYN за занятие -

- -
- ) : ( - <> -
-

Записаться

-

{classLabel}

-
- -
- setName(e.target.value)} - placeholder="Ваше имя" - required - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] px-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
- - handlePhoneChange(e.target.value)} - placeholder="+375 (__) ___-__-__" - required - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-9 pr-4 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
-
-
- @ - setInstagram(e.target.value.replace(/^@/, ""))} - placeholder="Instagram" - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-7 pr-3 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
-
- @ - setTelegram(e.target.value.replace(/^@/, ""))} - placeholder="Telegram" - className="w-full rounded-xl border border-white/[0.08] bg-white/[0.04] pl-7 pr-3 py-3 text-sm text-white placeholder-neutral-500 outline-none transition-colors focus:border-gold/40 focus:bg-white/[0.06]" - /> -
-
- - {error && ( -

{error}

- )} - - -
- - )} -
-
, - document.body - ); -} diff --git a/src/components/ui/SignupModal.tsx b/src/components/ui/SignupModal.tsx index f74e93a..4950dae 100644 --- a/src/components/ui/SignupModal.tsx +++ b/src/components/ui/SignupModal.tsx @@ -132,7 +132,7 @@ export function SignupModal({ if (!open) return null; return createPortal( -
+