Browse Source
* feat: add dialog abstraction system * removed unneeded file * fix formatting * linting fixespull/841/head
committed by
GitHub
6 changed files with 327 additions and 123 deletions
@ -0,0 +1,141 @@ |
|||||
|
import { Button } from "@components/UI/Button.tsx"; |
||||
|
import { |
||||
|
Dialog, |
||||
|
DialogClose, |
||||
|
DialogContent, |
||||
|
DialogDescription, |
||||
|
DialogFooter, |
||||
|
DialogHeader, |
||||
|
DialogTitle, |
||||
|
} from "@components/UI/Dialog.tsx"; |
||||
|
import { Separator } from "@components/UI/Separator.tsx"; |
||||
|
import type { ReactNode } from "react"; |
||||
|
import { useTranslation } from "react-i18next"; |
||||
|
|
||||
|
export type DialogVariant = "default" | "destructive"; |
||||
|
export type DialogType = "confirm" | "alert" | "info" | "custom"; |
||||
|
|
||||
|
export interface DialogWrapperProps { |
||||
|
open: boolean; |
||||
|
onOpenChange: (open: boolean) => void; |
||||
|
title: string; |
||||
|
description?: string; |
||||
|
type?: DialogType; |
||||
|
icon?: ReactNode; |
||||
|
children?: ReactNode; |
||||
|
|
||||
|
showFooter?: boolean; |
||||
|
confirmText?: string; |
||||
|
cancelText?: string; |
||||
|
dismissText?: string; |
||||
|
variant?: DialogVariant; |
||||
|
confirmIcon?: ReactNode; |
||||
|
customFooter?: ReactNode; |
||||
|
|
||||
|
onConfirm?: () => void | Promise<void>; |
||||
|
onCancel?: () => void; |
||||
|
onDismiss?: () => void; |
||||
|
} |
||||
|
|
||||
|
export const DialogWrapper = ({ |
||||
|
open, |
||||
|
onOpenChange, |
||||
|
title, |
||||
|
description, |
||||
|
type = "custom", |
||||
|
icon, |
||||
|
children, |
||||
|
showFooter = true, |
||||
|
confirmText, |
||||
|
cancelText, |
||||
|
dismissText, |
||||
|
variant = "default", |
||||
|
confirmIcon, |
||||
|
customFooter, |
||||
|
onConfirm, |
||||
|
onCancel, |
||||
|
onDismiss, |
||||
|
}: DialogWrapperProps) => { |
||||
|
const { t } = useTranslation("dialog"); |
||||
|
|
||||
|
const handleClose = () => { |
||||
|
onOpenChange(false); |
||||
|
}; |
||||
|
|
||||
|
const handleConfirm = async () => { |
||||
|
if (onConfirm) { |
||||
|
await onConfirm(); |
||||
|
} |
||||
|
handleClose(); |
||||
|
}; |
||||
|
|
||||
|
const handleCancel = () => { |
||||
|
if (onCancel) { |
||||
|
onCancel(); |
||||
|
} |
||||
|
handleClose(); |
||||
|
}; |
||||
|
|
||||
|
const handleDismiss = () => { |
||||
|
if (onDismiss) { |
||||
|
onDismiss(); |
||||
|
} |
||||
|
handleClose(); |
||||
|
}; |
||||
|
|
||||
|
const renderFooter = () => { |
||||
|
if (!showFooter) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
if (customFooter) { |
||||
|
return <DialogFooter className="mt-4">{customFooter}</DialogFooter>; |
||||
|
} |
||||
|
|
||||
|
switch (type) { |
||||
|
case "confirm": |
||||
|
return ( |
||||
|
<DialogFooter className="mt-4"> |
||||
|
<Button variant="outline" onClick={handleCancel} name="cancel"> |
||||
|
{cancelText || t("button.cancel")} |
||||
|
</Button> |
||||
|
<Button variant={variant} onClick={handleConfirm} name="confirm"> |
||||
|
{confirmIcon && <span className="mr-2">{confirmIcon}</span>} |
||||
|
{confirmText || t("button.confirm")} |
||||
|
</Button> |
||||
|
</DialogFooter> |
||||
|
); |
||||
|
|
||||
|
case "alert": |
||||
|
case "info": |
||||
|
return ( |
||||
|
<DialogFooter className="mt-4"> |
||||
|
<Button variant="outline" onClick={handleDismiss} name="dismiss"> |
||||
|
{dismissText || t("button.dismiss")} |
||||
|
</Button> |
||||
|
</DialogFooter> |
||||
|
); |
||||
|
|
||||
|
default: |
||||
|
return null; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return ( |
||||
|
<Dialog open={open} onOpenChange={onOpenChange}> |
||||
|
<DialogContent> |
||||
|
<DialogClose /> |
||||
|
<DialogHeader> |
||||
|
<DialogTitle className="flex items-center gap-2"> |
||||
|
{icon && icon} |
||||
|
{title} |
||||
|
</DialogTitle> |
||||
|
{type !== "custom" && <Separator />} |
||||
|
{description && <DialogDescription>{description}</DialogDescription>} |
||||
|
</DialogHeader> |
||||
|
{children} |
||||
|
{renderFooter()} |
||||
|
</DialogContent> |
||||
|
</Dialog> |
||||
|
); |
||||
|
}; |
||||
@ -0,0 +1,118 @@ |
|||||
|
import { type ReactNode, useCallback, useState } from "react"; |
||||
|
import type { useTranslation } from "react-i18next"; |
||||
|
|
||||
|
export type DialogType = "confirm" | "alert" | "info" | "custom"; |
||||
|
|
||||
|
export interface BaseDialogConfig { |
||||
|
type: DialogType; |
||||
|
title: string; |
||||
|
description?: string; |
||||
|
onConfirm?: () => void | Promise<void>; |
||||
|
onCancel?: () => void; |
||||
|
onClose?: () => void; |
||||
|
} |
||||
|
|
||||
|
export interface ConfirmDialogConfig extends BaseDialogConfig { |
||||
|
type: "confirm"; |
||||
|
confirmText?: string; |
||||
|
cancelText?: string; |
||||
|
variant?: "default" | "destructive"; |
||||
|
icon?: ReactNode; |
||||
|
confirmIcon?: ReactNode; |
||||
|
onConfirm: () => void | Promise<void>; |
||||
|
} |
||||
|
|
||||
|
export interface AlertDialogConfig extends BaseDialogConfig { |
||||
|
type: "alert"; |
||||
|
dismissText?: string; |
||||
|
icon?: ReactNode; |
||||
|
} |
||||
|
|
||||
|
export interface InfoDialogConfig extends BaseDialogConfig { |
||||
|
type: "info"; |
||||
|
dismissText?: string; |
||||
|
icon?: ReactNode; |
||||
|
} |
||||
|
|
||||
|
export interface CustomDialogConfig extends BaseDialogConfig { |
||||
|
type: "custom"; |
||||
|
icon?: React.ReactNode; |
||||
|
content: ReactNode; |
||||
|
footerActions?: ReactNode; |
||||
|
} |
||||
|
|
||||
|
export type DialogConfig = |
||||
|
| ConfirmDialogConfig |
||||
|
| AlertDialogConfig |
||||
|
| InfoDialogConfig |
||||
|
| CustomDialogConfig; |
||||
|
|
||||
|
export interface DialogState { |
||||
|
isOpen: boolean; |
||||
|
config?: DialogConfig; |
||||
|
} |
||||
|
|
||||
|
export interface UseDialogReturn { |
||||
|
isOpen: boolean; |
||||
|
config?: DialogConfig; |
||||
|
openDialog: (config: DialogConfig) => void; |
||||
|
closeDialog: () => void; |
||||
|
handleConfirm: () => void; |
||||
|
handleCancel: () => void; |
||||
|
} |
||||
|
|
||||
|
export const useDialog = (initialState?: DialogState): UseDialogReturn => { |
||||
|
const [state, setState] = useState<DialogState>( |
||||
|
initialState ?? { isOpen: false }, |
||||
|
); |
||||
|
|
||||
|
const openDialog = useCallback((config: DialogConfig) => { |
||||
|
setState({ isOpen: true, config }); |
||||
|
}, []); |
||||
|
|
||||
|
const closeDialog = useCallback(() => { |
||||
|
state.config?.onClose?.(); |
||||
|
setState({ isOpen: false, config: undefined }); |
||||
|
}, [state.config]); |
||||
|
|
||||
|
const handleConfirm = useCallback(async () => { |
||||
|
if (state.config?.onConfirm) { |
||||
|
await state.config.onConfirm(); |
||||
|
} |
||||
|
closeDialog(); |
||||
|
}, [state.config, closeDialog]); |
||||
|
|
||||
|
const handleCancel = useCallback(() => { |
||||
|
state.config?.onCancel?.(); |
||||
|
closeDialog(); |
||||
|
}, [state.config, closeDialog]); |
||||
|
|
||||
|
return { |
||||
|
isOpen: state.isOpen, |
||||
|
config: state.config, |
||||
|
openDialog, |
||||
|
closeDialog, |
||||
|
handleConfirm, |
||||
|
handleCancel, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export const getDefaultTexts = ( |
||||
|
type: DialogType, |
||||
|
t: ReturnType<typeof useTranslation>["t"], |
||||
|
) => { |
||||
|
switch (type) { |
||||
|
case "confirm": |
||||
|
return { |
||||
|
confirmText: t("button.confirm"), |
||||
|
cancelText: t("button.cancel"), |
||||
|
}; |
||||
|
case "alert": |
||||
|
case "info": |
||||
|
return { |
||||
|
dismissText: t("button.dismiss"), |
||||
|
}; |
||||
|
default: |
||||
|
return {}; |
||||
|
} |
||||
|
}; |
||||
Loading…
Reference in new issue