2025-08-05 23:39:54 +02:00

168 lines
5.2 KiB
TypeScript

'use client'
import { useEffect } from 'react'
type Width =
| 'sm:max-w-sm'
| 'sm:max-w-md'
| 'sm:max-w-lg'
| 'sm:max-w-xl'
| 'sm:max-w-2xl'
| string
type ModalProps = {
id: string
title: string
children?: React.ReactNode
show: boolean
onClose?: () => void
onSave?: () => void
hideCloseButton?: boolean
closeButtonColor?: 'blue' | 'red' | 'green' | 'teal'
closeButtonTitle?: string
disableSave?: boolean
maxWidth?: Width
}
export default function Modal({
id,
title,
children,
show,
onClose,
onSave,
hideCloseButton = false,
closeButtonColor = 'blue',
closeButtonTitle = 'Speichern',
disableSave,
maxWidth = 'sm:max-w-lg',
}: ModalProps) {
/* ───────── Overlay-Lifecycle ───────── */
useEffect(() => {
const modalEl = document.getElementById(id)
const hs = (window as any).HSOverlay
if (!modalEl || !hs) return
/* ► Collection kann undefined oder ein Objekt sein.
► Wir sichern uns ab und behandeln nur echte Arrays. */
const getCollection = (): any[] =>
Array.isArray(hs.collection) ? hs.collection : []
const destroyIfExists = () => {
const inst = getCollection().find((i) => i.element === modalEl)
inst?.destroy?.()
if (inst) {
hs.collection = getCollection().filter((i) => i !== inst)
}
}
const handleClose = () => onClose?.()
modalEl.addEventListener('hsOverlay:close', handleClose)
try {
if (show) {
destroyIfExists()
hs.autoInit?.()
hs.open?.(modalEl)
} else {
hs.close?.(modalEl)
destroyIfExists()
}
} catch (err) {
// eslint-disable-next-line no-console
console.error('[Modal] HSOverlay Fehler:', err)
}
return () => {
modalEl.removeEventListener('hsOverlay:close', handleClose)
destroyIfExists()
}
}, [show, id, onClose])
/* ───────── Render ───────── */
return (
<div
id={id}
data-hs-overlay="true"
role="dialog"
tabIndex={-1}
aria-labelledby={`${id}-label`}
onMouseDown={(e) => {
if (e.target === e.currentTarget) onClose?.()
}}
className="hs-overlay hidden fixed inset-0 z-80 overflow-y-auto overflow-x-hidden"
>
{/* Backdrop */}
<div className="fixed inset-0 -z-10 bg-black/50 dark:bg-neutral-900/70" />
{/* Dialog */}
<div className={`hs-overlay-open:mt-7 hs-overlay-open:opacity-100 hs-overlay-open:duration-500
mt-0 opacity-0 transition-all ease-out
${maxWidth} sm:w-full m-3 sm:mx-auto
min-h-[calc(100%-56px)] flex items-center`}
>
<div className="w-full flex flex-col bg-white dark:bg-neutral-800 border border-gray-200 dark:border-neutral-700 shadow-2xs dark:shadow-neutral-700/70 rounded-xl">
{/* Header */}
<div className="flex justify-between items-center py-3 px-4 border-b border-gray-200 dark:border-neutral-700">
<h3 id={`${id}-label`} className="font-bold text-gray-800 dark:text-white">
{title}
</h3>
{!hideCloseButton && (
<button
type="button"
aria-label="Close"
data-hs-overlay={`#${id}`}
onClick={onClose}
className="size-8 inline-flex justify-center items-center rounded-full bg-gray-100 hover:bg-gray-200 dark:bg-neutral-700 dark:hover:bg-neutral-600 text-gray-800 dark:text-neutral-400"
>
<svg
className="size-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
)}
</div>
{/* Body */}
<div className="p-4 overflow-y-auto max-h-[70vh] sm:max-h-[60vh]">{children}</div>
{/* Footer */}
<div className="flex justify-end items-center gap-x-2 py-3 px-4 border-t border-gray-200 dark:border-neutral-700">
{!hideCloseButton && (
<button
type="button"
data-hs-overlay={`#${id}`}
onClick={onClose}
className="py-2 px-3 text-sm font-medium rounded-lg border border-gray-200 dark:border-neutral-700 bg-white dark:bg-neutral-800 text-gray-800 dark:text-white shadow-2xs hover:bg-gray-50 dark:hover:bg-neutral-700"
>
Schließen
</button>
)}
{onSave && (
<button
type="button"
onClick={onSave}
disabled={disableSave}
className={`py-2 px-3 text-sm font-medium rounded-lg border border-transparent bg-${closeButtonColor}-600 hover:bg-${closeButtonColor}-700 focus:bg-${closeButtonColor}-700 text-white`}
>
{closeButtonTitle}
</button>
)}
</div>
</div>
</div>
</div>
)
}