import { Suspense, useEffect, useRef, useState } from 'react'; import { Loader } from 'lucide-react'; import mitt from 'mitt'; import dynamic from 'next/dynamic'; import { useOnClickOutside } from 'usehooks-ts'; import type { ConfirmProps } from './Confirm'; const Loading = () => (
); const modals = { EditProject: dynamic(() => import('./EditProject'), { loading: Loading, }), EditClient: dynamic(() => import('./EditClient'), { loading: Loading, }), AddProject: dynamic(() => import('./AddProject'), { loading: Loading, }), AddClient: dynamic(() => import('./AddClient'), { loading: Loading, }), Confirm: dynamic(() => import('./Confirm'), { loading: Loading, }), SaveReport: dynamic(() => import('./SaveReport'), { loading: Loading, }), }; const emitter = mitt<{ push: { name: ModalRoutes; props: Record; }; replace: { name: ModalRoutes; props: Record; }; pop: { name?: ModalRoutes }; unshift: { name: ModalRoutes }; }>(); type ModalRoutes = keyof typeof modals; interface StateItem { key: string; name: ModalRoutes; props: Record; } interface ModalWrapperProps { children: React.ReactNode; name: ModalRoutes; isOnTop: boolean; } function ModalWrapper({ children, name, isOnTop }: ModalWrapperProps) { const ref = useRef(null); useOnClickOutside(ref, (event) => { const target = event.target as HTMLElement; const isPortal = typeof target.closest === 'function' ? !!target.closest('[data-radix-popper-content-wrapper]') : false; if (isOnTop && !isPortal) { emitter.emit('pop', { name, }); } }); return (
{children}
); } export function ModalProvider() { const [state, setState] = useState([]); useEffect(() => { document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && state.length > 0) { setState((p) => { p.pop(); return [...p]; }); } }); }, [state]); useEffect(() => { emitter.on('push', ({ name, props }) => { setState((p) => [ ...p, { key: Math.random().toString(), name, props, }, ]); }); emitter.on('replace', ({ name, props }) => { setState([ { key: Math.random().toString(), name, props, }, ]); }); emitter.on('pop', ({ name }) => { setState((items) => { const match = name === undefined ? // Pick last item if no name is provided items.length - 1 : items.findLastIndex((item) => item.name === name); return items.filter((_, index) => index !== match); }); }); emitter.on('unshift', ({ name }) => { setState((items) => { const match = items.findIndex((item) => item.name === name); return items.filter((_, index) => index !== match); }); }); return () => emitter.all.clear(); }); return ( <> {!!state.length && (
)} {state.map((item, index) => { const Modal = modals[item.name]; return ( {/* @ts-expect-error */} ); })} ); } type GetComponentProps = T extends | React.ComponentType | React.Component ? P : never; type OrUndefined = T extends Record ? undefined : T; export const pushModal = < T extends StateItem['name'], B extends OrUndefined>, >( name: T, ...rest: B extends undefined ? [] : [B] ) => emitter.emit('push', { name, props: Array.isArray(rest) && rest[0] ? rest[0] : {}, }); export const replaceModal = < T extends StateItem['name'], B extends OrUndefined>, >( name: T, ...rest: B extends undefined ? [] : [B] ) => emitter.emit('replace', { name, props: Array.isArray(rest) && rest[0] ? rest[0] : {}, }); export const popModal = (name?: StateItem['name']) => emitter.emit('pop', { name, }); export const unshiftModal = (name: StateItem['name']) => emitter.emit('unshift', { name, }); export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props);