121 lines
3.8 KiB
TypeScript
121 lines
3.8 KiB
TypeScript
"use client"
|
|
|
|
import { create } from "zustand"
|
|
import { Trash2 } from "lucide-react"
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogMedia,
|
|
AlertDialogTitle,
|
|
} from "@/shared/components/ui/alert-dialog"
|
|
|
|
// ── Types ──
|
|
|
|
export type ConfirmOptions = {
|
|
title?: string
|
|
description?: string
|
|
confirmLabel?: string
|
|
cancelLabel?: string
|
|
variant?: "destructive" | "default"
|
|
}
|
|
|
|
type ConfirmStore = {
|
|
open: boolean
|
|
options: ConfirmOptions
|
|
resolve: ((value: boolean) => void) | null
|
|
_show: (options: ConfirmOptions) => Promise<boolean>
|
|
_close: (confirmed: boolean) => void
|
|
}
|
|
|
|
// ── Store ──
|
|
|
|
const useConfirmStore = create<ConfirmStore>((set, get) => ({
|
|
open: false,
|
|
options: {},
|
|
resolve: null,
|
|
_show: (options) =>
|
|
new Promise<boolean>((resolve) => {
|
|
set({ open: true, options, resolve })
|
|
}),
|
|
_close: (confirmed) => {
|
|
const { resolve } = get()
|
|
resolve?.(confirmed)
|
|
set({ open: false, resolve: null })
|
|
},
|
|
}))
|
|
|
|
// ── Imperative API (usage: `await confirm({ ... })`) ──
|
|
|
|
export function confirm(options: ConfirmOptions = {}) {
|
|
if (process.env.NODE_ENV === "development") {
|
|
const state = useConfirmStore.getState()
|
|
if (state.open) {
|
|
console.warn("[ConfirmDialog] A confirm dialog is already open. Nested confirms are not supported.")
|
|
}
|
|
// Detect missing <ConfirmDialog /> mount: if `resolve` is never set after a tick, the dialog component is not mounted.
|
|
const result = state._show(options)
|
|
setTimeout(() => {
|
|
const current = useConfirmStore.getState()
|
|
if (current.open && current.resolve === null) {
|
|
console.warn(
|
|
"[ConfirmDialog] confirm() was called but <ConfirmDialog /> does not appear to be mounted. " +
|
|
"Make sure <ConfirmDialog /> is rendered in your root layout.",
|
|
)
|
|
}
|
|
}, 100)
|
|
return result
|
|
}
|
|
return useConfirmStore.getState()._show(options)
|
|
}
|
|
|
|
// ── Dialog component (mount once in the root layout) ──
|
|
|
|
export function ConfirmDialog() {
|
|
const { open, options, _close } = useConfirmStore()
|
|
|
|
const isDestructive = options.variant === "destructive"
|
|
|
|
return (
|
|
<AlertDialog
|
|
open={open}
|
|
onOpenChange={(v) => {
|
|
if (!v) _close(false)
|
|
}}
|
|
>
|
|
<AlertDialogContent size="sm">
|
|
<AlertDialogHeader>
|
|
{isDestructive && (
|
|
<AlertDialogMedia>
|
|
<Trash2 className="text-destructive" />
|
|
</AlertDialogMedia>
|
|
)}
|
|
<AlertDialogTitle>
|
|
{options.title ?? "Are you sure?"}
|
|
</AlertDialogTitle>
|
|
{options.description && (
|
|
<AlertDialogDescription>
|
|
{options.description}
|
|
</AlertDialogDescription>
|
|
)}
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => _close(false)}>
|
|
{options.cancelLabel ?? "Cancel"}
|
|
</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
variant={isDestructive ? "destructive" : "default"}
|
|
onClick={() => _close(true)}
|
|
>
|
|
{options.confirmLabel ?? "Confirm"}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
)
|
|
}
|