feat: comprehensive CMS improvements
- Fix DOMPurify SSR crash (use isomorphic-dompurify) - Fix SanitizedHtml to sanitize by default - Add auth guards to studio/catalog maintenance pages - Add update/edit to vouchers CRUD - Add update/edit to rare-values CRUD - Add approve workflow to applications page - Add edit form to guilds detail page - Add SEO metadata to all public pages (21 pages) - Fix mobile nav accessibility (focus trap, aria attributes) - Fix missing labels and table accessibility - Add dynamic imports for heavy client components (6 components) - Fix silent error swallowing (40+ locations) - Add content scheduling for articles (publishAt, status) - Wire up 12 missing webhook notification triggers - Add global search to admin panel - Add bulk actions to admin users table - Fix JSON formatting and a11y issues
This commit is contained in:
1 parent
b810b16672
commit
30c95b1a5c
78 files changed
+2252
-134
No files matched your search
@@ -2,7 +2,13 @@
|
||||
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import Image from "next/image";
|
||||
import { type ReactNode, useRef, useState } from "react";
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { mobileMenuVariants } from "@/lib/motion";
|
||||
|
||||
interface MobileNavProps {
|
||||
@@ -20,6 +26,57 @@ export function MobileNav({
|
||||
}: MobileNavProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const detailsRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const MENU_ID = "mobile-nav-menu";
|
||||
|
||||
const handleEscape = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "Escape" && open) {
|
||||
setOpen(false);
|
||||
detailsRef.current?.querySelector<HTMLButtonElement>("button")?.focus();
|
||||
}
|
||||
},
|
||||
[open],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
return () => document.removeEventListener("keydown", handleEscape);
|
||||
}, [handleEscape]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const menu = menuRef.current;
|
||||
if (!menu) return;
|
||||
|
||||
const focusableSelector =
|
||||
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
|
||||
|
||||
function handleTab(e: KeyboardEvent) {
|
||||
if (e.key !== "Tab" || !menu) return;
|
||||
const focusable = Array.from(
|
||||
menu.querySelectorAll<HTMLElement>(focusableSelector),
|
||||
);
|
||||
if (focusable.length === 0) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
menu.addEventListener("keydown", handleTab);
|
||||
return () => menu.removeEventListener("keydown", handleTab);
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div ref={detailsRef} className="group relative md:hidden">
|
||||
@@ -33,6 +90,8 @@ export function MobileNav({
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-primary-readable,var(--color-primary))] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-navbar)]"
|
||||
aria-label={open ? closeLabel : menuLabel}
|
||||
aria-expanded={open}
|
||||
aria-haspopup="true"
|
||||
aria-controls={MENU_ID}
|
||||
>
|
||||
<motion.svg
|
||||
className="w-6 h-6"
|
||||
@@ -63,6 +122,9 @@ export function MobileNav({
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
id={MENU_ID}
|
||||
role="menu"
|
||||
aria-label={brandLabel || "Navigation menu"}
|
||||
className="absolute left-0 top-full mt-2 w-[min(88vw,360px)] z-50 origin-top-left"
|
||||
style={{ backgroundColor: "transparent" }}
|
||||
variants={mobileMenuVariants}
|
||||
@@ -71,8 +133,9 @@ export function MobileNav({
|
||||
exit="exit"
|
||||
>
|
||||
<div
|
||||
ref={menuRef}
|
||||
className="flex flex-col gap-y-1 p-2 rounded-2xl border shadow-xl max-h-[calc(100dvh-5rem)] overflow-y-auto overscroll-contain
|
||||
bg-[var(--color-dropdown,var(--color-surface,#fff))]"
|
||||
bg-[var(--color-dropdown,var(--color-surface,#fff))]"
|
||||
style={{
|
||||
borderColor:
|
||||
"color-mix(in srgb, var(--color-text-muted) 12%, transparent)",
|
||||
|
||||
Reference in new issue
Block a user