import { useCallback, useMemo, useRef, useState } from 'react' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { DialogueContext } from '@/lib/dialogues/useDialogue' import type { FC, ReactNode } from 'react' import type { AlertOptions, ChoiceOptions, ConfirmOptions, DialogueAPI, DialogueFormAction, DialogueFormControls, DialogueFormOptions } from '@/lib/dialogues/useDialogue' type DialogueRequest = | { id: number kind: 'confirm' options: ConfirmOptions resolve: (value: boolean) => void } | { id: number kind: 'alert' options: AlertOptions resolve: () => void } | { id: number kind: 'choice' options: ChoiceOptions resolve: (value: string | null) => void } | { id: number kind: 'form' options: DialogueFormOptions resolve: () => void } let nextDialogueId = 1 type Props = { children: ReactNode } const DialogueProvider: FC = ({ children }) => { const [queue, setQueue] = useState ([]) const [pendingIds, setPendingIds] = useState ([]) const [formActions, setFormActions] = useState> ({ }) const formControls = useRef> ({ }) const [nestedConfirm, setNestedConfirm] = useState<{ parentId: number options: ConfirmOptions resolve: (value: boolean) => void } | null> (null) const push = useCallback ((request: Omit) => { const id = nextDialogueId ++nextDialogueId setQueue (current => [...current, { ...request, id } as DialogueRequest]) }, []) const closeRequest = useCallback ((id: number, result?: unknown) => { setQueue (current => { const active = current.find (request => request.id === id) if (active == null) return current switch (active.kind) { case 'confirm': active.resolve (Boolean (result)) break case 'alert': active.resolve () break case 'choice': active.resolve ((result ?? null) as string | null) break case 'form': active.resolve () break } return current.filter (request => request.id !== id) }) setPendingIds (current => current.filter (_1 => _1 !== id)) setFormActions (current => { const { [id]: _, ...rest } = current return rest }) delete formControls.current[id] }, []) const setRequestActions = useCallback ( (id: number, actions: DialogueFormAction[]) => { setFormActions (current => ({ ...current, [id]: actions })) }, []) const openNestedConfirm = useCallback ( (parentId: number, options: ConfirmOptions) => new Promise (resolve => { setNestedConfirm ({ parentId, options, resolve }) }), []) const closeNestedConfirm = useCallback ((result: boolean) => { setNestedConfirm (current => { if (current == null) return current current.resolve (result) return null }) }, []) const api = useMemo (() => ({ confirm: options => new Promise (resolve => { push ({ kind: 'confirm', options, resolve }) }), alert: options => new Promise (resolve => { push ({ kind: 'alert', options, resolve }) }), choice: options => new Promise (resolve => { push ({ kind: 'choice', options: options as ChoiceOptions, resolve: resolve as (value: string | null) => void }) }), form: options => new Promise (resolve => { push ({ kind: 'form', options, resolve }) }) }), [push]) const handleFormAction = useCallback ( async (id: number, action: DialogueFormAction) => { if (pendingIds.includes (id)) return setPendingIds (current => [...current, id]) try { const shouldClose = await action.onSelect () if (shouldClose !== false) closeRequest (id) } finally { setPendingIds (current => current.filter (_1 => _1 !== id)) } }, [closeRequest, pendingIds]) const active = queue[0] const startActions = active?.kind === 'form' ? (formActions[active.id] ?? []).filter (action => action.placement === 'start') : [] const endActions = active?.kind === 'form' ? (formActions[active.id] ?? []).filter (action => action.placement == null || action.placement === 'end') : [] return ( {children} {active && ( { const blocked = nestedConfirm?.parentId === active.id || pendingIds.includes (active.id) if (!(open) && !(blocked)) closeRequest (active.id, active.kind !== 'confirm' && null) }}> { if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id)) event.preventDefault () }} onPointerDownOutside={event => { if (nestedConfirm?.parentId === active.id || pendingIds.includes (active.id)) event.preventDefault () }}> {active.kind === 'form' ? ( <> {active.options.title} {active.options.description && (
{active.options.description}
)}
{active.options.body ( formControls.current[active.id] ??= { close: () => closeRequest (active.id), setActions: actions => setRequestActions (active.id, actions), confirm: options => openNestedConfirm (active.id, options) })}
{startActions.map (action => ( ))}
{endActions.map (action => ( ))}
) : ( <> {active.options.title} {active.options.description && (
{active.options.description}
)}
{active.kind === 'confirm' && ( <> )} {active.kind === 'alert' && ( )} {active.kind === 'choice' && ( <> {active.options.choices.map (choice => ( ))} )} )}
)} {nestedConfirm && ( { if (!(open)) closeNestedConfirm (false) }}> {nestedConfirm.options.title} {nestedConfirm.options.description && (
{nestedConfirm.options.description}
)}
)}
) } export { useDialogue } from '@/lib/dialogues/useDialogue' export default DialogueProvider