refactor: comprehensive frontend review — consistency, a11y, code quality

- Replace event dispatchers with BookingContext (Hero, Header, FloatingContact)
- Add focus trap hook for modals (SignupModal, NewsModal)
- Extract shared components: CollapsibleSection, ConfirmDialog, PriceField, AdminSkeleton
- Add delete confirmation dialog to ArrayEditor
- Replace hardcoded colors (#050505, #0a0a0a, #c9a96e, #2ecc71) with theme tokens
- Add CSS variables --color-surface-deep/dark for consistent dark surfaces
- Improve contrast: muted text neutral-500 → neutral-400 in dark mode
- Fix modal z-index hierarchy (modals z-60, header z-50, floats z-40)
- Consolidate duplicate formatDate → shared formatting.ts
- Add useMemo to TeamProfile groupMap computation
- Fix typography: responsive price text in Pricing section
- Add ARIA labels/expanded to FAQ, OpenDay, ArrayEditor grip handles
- Hide number input spinners globally
- Reorder admin sidebar: Dashboard → SEO → Bookings → site section order
- Use shared PriceField in Open Day editor
- Fix schedule grid first time slot (09:00) clipped by container
- Fix pre-existing type errors (bookings, hero, db interfaces)
This commit is contained in:
2026-03-26 19:45:37 +03:00
parent ec08f8e8d5
commit 76307e298b
32 changed files with 613 additions and 319 deletions

View File

@@ -3,6 +3,7 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { createPortal } from "react-dom";
import { Plus, Trash2, GripVertical, ChevronDown, ChevronsUpDown } from "lucide-react";
import { ConfirmDialog } from "./ConfirmDialog";
interface ArrayEditorProps<T> {
items: T[];
@@ -31,6 +32,7 @@ export function ArrayEditor<T>({
hiddenItems,
addPosition = "bottom",
}: ArrayEditorProps<T>) {
const [confirmDelete, setConfirmDelete] = useState<number | null>(null);
const [dragIndex, setDragIndex] = useState<number | null>(null);
const [insertAt, setInsertAt] = useState<number | null>(null);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
@@ -170,6 +172,8 @@ export function ArrayEditor<T>({
<div
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
onMouseDown={(e) => handleGripMouseDown(e, i)}
aria-label="Перетащить для сортировки"
role="button"
>
<GripVertical size={16} />
</div>
@@ -187,7 +191,8 @@ export function ArrayEditor<T>({
</div>
<button
type="button"
onClick={() => removeItem(i)}
onClick={() => setConfirmDelete(i)}
aria-label="Удалить элемент"
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors shrink-0"
>
<Trash2 size={16} />
@@ -257,12 +262,15 @@ export function ArrayEditor<T>({
<div
className="cursor-grab active:cursor-grabbing rounded p-1 text-neutral-500 hover:text-white transition-colors select-none"
onMouseDown={(e) => handleGripMouseDown(e, i)}
aria-label="Перетащить для сортировки"
role="button"
>
<GripVertical size={16} />
</div>
<button
type="button"
onClick={() => removeItem(i)}
aria-label="Удалить элемент"
className="rounded p-1 text-neutral-500 hover:text-red-400 transition-colors"
>
<Trash2 size={16} />
@@ -370,6 +378,14 @@ export function ArrayEditor<T>({
</div>,
document.body
)}
<ConfirmDialog
open={confirmDelete !== null}
title="Удалить элемент?"
message="Это действие нельзя отменить."
onConfirm={() => { if (confirmDelete !== null) removeItem(confirmDelete); setConfirmDelete(null); }}
onCancel={() => setConfirmDelete(null)}
/>
</div>
);
}