diff --git a/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx b/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx index 334ccb21..d73d5489 100644 --- a/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx +++ b/packages/web/src/components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx @@ -1,16 +1,7 @@ -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { useMessages } from "@core/stores"; +import { useMessages } from "@app/core/stores/index.ts"; import { AlertTriangleIcon } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { DialogWrapper } from "../DialogWrapper.tsx"; export interface DeleteMessagesDialogProps { open: boolean; @@ -22,40 +13,25 @@ export const DeleteMessagesDialog = ({ onOpenChange, }: DeleteMessagesDialogProps) => { const { t } = useTranslation("dialog"); - const { deleteAllMessages } = useMessages(); - const handleCloseDialog = () => { + const messageStore = useMessages(); + + const handleConfirm = () => { + messageStore.deleteAllMessages(); onOpenChange(false); }; return ( - - - - - - - {t("deleteMessages.title")} - - - {t("deleteMessages.description")} - - - - - - - - + } + variant="destructive" + confirmText={t("button.clearMessages")} + cancelText={t("button.dismiss")} + onConfirm={handleConfirm} + /> ); }; diff --git a/packages/web/src/components/Dialog/DialogWrapper.tsx b/packages/web/src/components/Dialog/DialogWrapper.tsx new file mode 100644 index 00000000..ca69df21 --- /dev/null +++ b/packages/web/src/components/Dialog/DialogWrapper.tsx @@ -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; + 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 {customFooter}; + } + + switch (type) { + case "confirm": + return ( + + + + + ); + + case "alert": + case "info": + return ( + + + + ); + + default: + return null; + } + }; + + return ( + + + + + + {icon && icon} + {title} + + {type !== "custom" && } + {description && {description}} + + {children} + {renderFooter()} + + + ); +}; diff --git a/packages/web/src/components/Dialog/RemoveNodeDialog.tsx b/packages/web/src/components/Dialog/RemoveNodeDialog.tsx index 9574fa16..c51a5e46 100644 --- a/packages/web/src/components/Dialog/RemoveNodeDialog.tsx +++ b/packages/web/src/components/Dialog/RemoveNodeDialog.tsx @@ -1,16 +1,7 @@ -import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; import { Label } from "@components/UI/Label.tsx"; import { useAppStore, useDevice, useNodeDB } from "@core/stores"; import { useTranslation } from "react-i18next"; +import { DialogWrapper } from "./DialogWrapper.tsx"; export interface RemoveNodeDialogProps { open: boolean; @@ -26,35 +17,25 @@ export const RemoveNodeDialog = ({ const { getNode, removeNode } = useNodeDB(); const { nodeNumToBeRemoved } = useAppStore(); - const onSubmit = () => { + const handleConfirm = () => { connection?.removeNodeByNum(nodeNumToBeRemoved); removeNode(nodeNumToBeRemoved); - onOpenChange(false); }; return ( - - - - - {t("removeNode.title")} - {t("removeNode.description")} - -
-
- -
-
- - - -
-
+ +
+ +
+
); }; diff --git a/packages/web/src/components/Dialog/ShutdownDialog.tsx b/packages/web/src/components/Dialog/ShutdownDialog.tsx index 00ef46ad..b53da064 100644 --- a/packages/web/src/components/Dialog/ShutdownDialog.tsx +++ b/packages/web/src/components/Dialog/ShutdownDialog.tsx @@ -1,17 +1,10 @@ import { Button } from "@components/UI/Button.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; import { Input } from "@components/UI/Input.tsx"; import { useDevice } from "@core/stores"; import { ClockIcon, PowerIcon } from "lucide-react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { DialogWrapper } from "./DialogWrapper.tsx"; export interface ShutdownDialogProps { open: boolean; @@ -21,45 +14,40 @@ export interface ShutdownDialogProps { export const ShutdownDialog = ({ open, onOpenChange }: ShutdownDialogProps) => { const { t } = useTranslation("dialog"); const { connection } = useDevice(); - const [time, setTime] = useState(5); - return ( - - - - - {t("shutdown.title")} - {t("shutdown.description")} - + const handleScheduledShutdown = () => { + connection?.shutdown(time * 60).then(() => onOpenChange(false)); + }; -
- setTime(Number.parseInt(e.target.value))} - suffix={t("unit.minute.plural")} - /> - - -
-
-
+ const handleImmediateShutdown = () => { + connection?.shutdown(2).then(() => onOpenChange(false)); + }; + + return ( + +
+ setTime(Number.parseInt(e.target.value))} + suffix={t("unit.minute.plural")} + /> + + +
+
); }; diff --git a/packages/web/src/components/Dialog/useDialog.ts b/packages/web/src/components/Dialog/useDialog.ts new file mode 100644 index 00000000..e45e9eee --- /dev/null +++ b/packages/web/src/components/Dialog/useDialog.ts @@ -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; + 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; +} + +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( + 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["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 {}; + } +}; diff --git a/packages/web/src/core/stores/appStore/index.ts b/packages/web/src/core/stores/appStore/index.ts index 2c59979d..1f342c29 100644 --- a/packages/web/src/core/stores/appStore/index.ts +++ b/packages/web/src/core/stores/appStore/index.ts @@ -32,7 +32,7 @@ interface AppState { setNodeNumDetails: (nodeNum: number) => void; } -export const useAppStore = create()((set, get) => ({ +export const useAppStore = create()((set, _get) => ({ selectedDeviceId: 0, devices: [], currentPage: "messages",