mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-07 21:48:18 -08:00
chore: initial public commit
This commit is contained in:
commit
fc9054b78c
250 changed files with 32809 additions and 0 deletions
84
client/app/components/modals/Modal.tsx
Normal file
84
client/app/components/modals/Modal.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
export function Modal({
|
||||
isOpen,
|
||||
onClose,
|
||||
children,
|
||||
maxW,
|
||||
h
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
maxW?: number;
|
||||
h?: number;
|
||||
}) {
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const [shouldRender, setShouldRender] = useState(isOpen);
|
||||
const [isClosing, setIsClosing] = useState(false);
|
||||
|
||||
// Show/hide logic
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setShouldRender(true);
|
||||
setIsClosing(false);
|
||||
} else if (shouldRender) {
|
||||
setIsClosing(true);
|
||||
const timeout = setTimeout(() => {
|
||||
setShouldRender(false);
|
||||
}, 100); // Match fade-out duration
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [isOpen, shouldRender]);
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
if (isOpen) document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
// Close on outside click
|
||||
useEffect(() => {
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
if (
|
||||
modalRef.current &&
|
||||
!modalRef.current.contains(e.target as Node)
|
||||
) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
if (isOpen) document.addEventListener('mousedown', handleClick);
|
||||
return () => document.removeEventListener('mousedown', handleClick);
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
if (!shouldRender) return null;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={`fixed inset-0 z-50 flex items-center justify-center bg-black/50 transition-opacity duration-100 ${
|
||||
isClosing ? 'animate-fade-out' : 'animate-fade-in'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
ref={modalRef}
|
||||
className={`bg-secondary rounded-lg shadow-md p-6 w-full relative max-h-3/4 overflow-y-auto transition-all duration-100 ${
|
||||
isClosing ? 'animate-fade-out-scale' : 'animate-fade-in-scale'
|
||||
}`}
|
||||
style={{ maxWidth: maxW ?? 600, height: h ?? '' }}
|
||||
>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-2 right-2 color-fg-tertiary hover:cursor-pointer"
|
||||
>
|
||||
🞪
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue