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:
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
count?: number;
|
||||
defaultOpen?: boolean;
|
||||
isOpen?: boolean;
|
||||
onToggle?: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared collapsible section for admin pages.
|
||||
* Supports both controlled (isOpen/onToggle) and uncontrolled (defaultOpen) modes.
|
||||
*/
|
||||
export function CollapsibleSection({
|
||||
title,
|
||||
count,
|
||||
defaultOpen = true,
|
||||
isOpen: controlledOpen,
|
||||
onToggle,
|
||||
children,
|
||||
}: CollapsibleSectionProps) {
|
||||
const [internalOpen, setInternalOpen] = useState(defaultOpen);
|
||||
const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
|
||||
const toggle = onToggle ?? (() => setInternalOpen((v) => !v));
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-white/10 bg-neutral-900/30 overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
aria-expanded={open}
|
||||
className="flex items-center justify-between w-full px-5 py-3.5 text-left cursor-pointer group hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-sm font-semibold text-neutral-200 group-hover:text-white transition-colors">
|
||||
{title}
|
||||
</h3>
|
||||
{count !== undefined && (
|
||||
<span className="text-xs text-neutral-500">{count}</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronDown
|
||||
size={16}
|
||||
className={`text-neutral-500 transition-transform duration-200 ${open ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className="grid transition-[grid-template-rows] duration-300 ease-out"
|
||||
style={{ gridTemplateRows: open ? "1fr" : "0fr" }}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="px-5 pb-5 space-y-4">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user