Files
einfach-produktiv/app/components/VersandModal.tsx
T
Marco 7a3ae0ea53 Add /widerruf and /agb legal pages, fix newsletter modal scroll jank
- New /widerruf page: CMS-driven content via getLegalPage("widerruf"),
  sidebar TOC + "Nachhaltig handeln" callout, TrustRow, and a download
  card for the Muster-Widerrufsformular PDF attachment.
- New /agb page, same legal-page shell pattern as Impressum/Datenschutz.
- lib/payload.ts: getLegalPage now also resolves the optional `attachment`
  media relation (depth=1) for legal pages that have a downloadable file.
- NewsletterModal: replaced the body overflow/position scroll-lock hacks
  (which fought with the sticky Navbar and the global scroll-smooth CSS)
  with a wheel/touch event interceptor that never touches scroll position
  at all, plus `preventScroll: true` on all modal/drawer focus() calls to
  stop the browser's implicit scrollIntoView from nudging the page for
  elements near the sticky navbar's reserved scroll-padding-top zone.
  Same preventScroll fix applied to VersandModal and the Navbar mobile
  drawer for consistency.
- globals.css: scrollbar-gutter: stable, so a modal's overflow:hidden
  lock never causes a scrollbar-width layout shift.
- Divider: stagger its reveal animation slightly (delay 0.3s) behind the
  section above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 19:57:28 +00:00

109 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef } from "react";
import { AnimatePresence, motion } from "motion/react";
import { VersandSections } from "../versand/components/VersandSections";
/**
* Quick-reference version of /versand, opened from the cart's order
* summary "Versand" info link — a full page navigation would pull you out
* of checkout, which is exactly what the link is there to avoid. Reuses
* VersandSections so the two never carry different numbers/copy.
*/
export function VersandModal({ open, onClose }: { open: boolean; onClose: () => void }) {
const dialogRef = useRef<HTMLDivElement>(null);
const closeButtonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
if (!open) return;
const prevOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = prevOverflow;
};
}, [open]);
useEffect(() => {
if (!open) return;
closeButtonRef.current?.focus({ preventScroll: true });
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
e.preventDefault();
onClose();
return;
}
if (e.key !== "Tab") return;
const focusables = dialogRef.current?.querySelectorAll<HTMLElement>(
'a[href], button:not([disabled])'
);
if (!focusables || focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus({ preventScroll: true });
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus({ preventScroll: true });
}
};
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [open, onClose]);
return (
<AnimatePresence>
{open && (
<motion.div
className="fixed inset-0 z-[60] flex items-center justify-center p-4 md:p-8 bg-[rgba(134,134,134,0.9)]"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25, ease: "easeOut" }}
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<motion.div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby="versand-modal-heading"
initial={{ opacity: 0, y: 16, scale: 0.97 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 16, scale: 0.97 }}
transition={{ duration: 0.3, ease: [0.22, 1, 0.36, 1] }}
className="relative bg-bg-base rounded-md overflow-hidden w-full max-w-[40rem] max-h-[85vh] overflow-y-auto"
>
<div className="sticky top-0 z-10 flex items-center justify-between px-8 py-6 bg-bg-base border-b border-border">
<p
id="versand-modal-heading"
className="font-semibold text-h-small text-text-primary"
style={{ fontFamily: "var(--font-lora)" }}
>
Versand
</p>
<button
ref={closeButtonRef}
type="button"
onClick={onClose}
aria-label="Schließen"
className="size-6 flex items-center justify-center active:scale-90 transition-transform text-text-muted hover:text-text-primary"
>
<span aria-hidden className="text-2xl leading-none">×</span>
</button>
</div>
<div className="px-8 py-6 pb-8">
<VersandSections />
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}