From 5384332b019ccd348ae9d083409f4563d4ae6ebe Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Sat, 27 Dec 2025 22:59:13 +0000 Subject: [PATCH] refactor: split dialog components into modules --- .../ui/organisms/dialogs/AlertDialog.tsx | 223 +-------------- .../ui/organisms/dialogs/Command.tsx | 3 + .../ui/organisms/dialogs/Command/Palette.tsx | 38 +++ .../ui/organisms/dialogs/Command/Results.tsx | 56 ++++ .../dialogs/Command/useCommandState.ts | 59 ++++ .../components/ui/organisms/dialogs/Sheet.tsx | 255 +----------------- .../ui/organisms/dialogs/Sheet/Drawer.tsx | 129 +++++++++ .../ui/organisms/dialogs/Sheet/Header.tsx | 108 ++++++++ .../ui/organisms/dialogs/alert/Actions.tsx | 69 +++++ .../ui/organisms/dialogs/alert/Content.tsx | 141 ++++++++++ 10 files changed, 619 insertions(+), 462 deletions(-) create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/Command/Palette.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/Command/Results.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/Command/useCommandState.ts create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Drawer.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Header.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/alert/Actions.tsx create mode 100644 frontends/nextjs/src/components/ui/organisms/dialogs/alert/Content.tsx diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/AlertDialog.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/AlertDialog.tsx index 270072d29..e2a904553 100644 --- a/frontends/nextjs/src/components/ui/organisms/dialogs/AlertDialog.tsx +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/AlertDialog.tsx @@ -1,24 +1,16 @@ -// TODO: Split this file (268 LOC) into smaller organisms (<150 LOC each) 'use client' import { forwardRef, ReactNode } from 'react' -import { - Dialog, - DialogTitle, - DialogContent, - DialogContentText, - DialogActions, - Button, - IconButton, - Typography, -} from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' -import WarningAmberIcon from '@mui/icons-material/WarningAmber' -import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' -import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import { Dialog } from '@mui/material' + +import { + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +} from './alert/Content' +import { AlertDialogAction, AlertDialogCancel, AlertDialogFooter } from './alert/Actions' -// AlertDialog Root interface AlertDialogProps { open: boolean onClose?: () => void @@ -52,7 +44,6 @@ const AlertDialog = forwardRef( ) AlertDialog.displayName = 'AlertDialog' -// AlertDialogTrigger - element that opens dialog interface AlertDialogTriggerProps { children: ReactNode onClick?: () => void @@ -70,200 +61,14 @@ const AlertDialogTrigger = forwardRef( ) AlertDialogTrigger.displayName = 'AlertDialogTrigger' -// AlertDialogContent - wrapper for dialog content -interface AlertDialogContentProps { - children: ReactNode - showCloseButton?: boolean - onClose?: () => void - className?: string -} - -const AlertDialogContent = forwardRef( - ({ children, showCloseButton = false, onClose, className, ...props }, ref) => { - return ( - <> - {showCloseButton && onClose && ( - - - - )} - {children} - - ) - } -) -AlertDialogContent.displayName = 'AlertDialogContent' - -// AlertDialogHeader -interface AlertDialogHeaderProps { - children: ReactNode - icon?: 'warning' | 'error' | 'info' | 'success' | ReactNode -} - -const AlertDialogHeader = forwardRef( - ({ children, icon, ...props }, ref) => { - const getIcon = () => { - if (!icon) return null - if (typeof icon !== 'string') return icon - - const iconMap = { - warning: , - error: , - info: , - success: , - } - return iconMap[icon] - } - - const iconElement = getIcon() - - return ( - - {iconElement} - {children} - - ) - } -) -AlertDialogHeader.displayName = 'AlertDialogHeader' - -// AlertDialogTitle -interface AlertDialogTitleProps { - children: ReactNode - className?: string -} - -const AlertDialogTitle = forwardRef( - ({ children, className, ...props }, ref) => { - return ( - - {children} - - ) - } -) -AlertDialogTitle.displayName = 'AlertDialogTitle' - -// AlertDialogDescription -interface AlertDialogDescriptionProps { - children: ReactNode - className?: string -} - -const AlertDialogDescription = forwardRef( - ({ children, className, ...props }, ref) => { - return ( - - - {children} - - - ) - } -) -AlertDialogDescription.displayName = 'AlertDialogDescription' - -// AlertDialogFooter -interface AlertDialogFooterProps { - children: ReactNode -} - -const AlertDialogFooter = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -AlertDialogFooter.displayName = 'AlertDialogFooter' - -// AlertDialogCancel -interface AlertDialogCancelProps { - children?: ReactNode - onClick?: () => void - className?: string -} - -const AlertDialogCancel = forwardRef( - ({ children = 'Cancel', onClick, className, ...props }, ref) => { - return ( - - ) - } -) -AlertDialogCancel.displayName = 'AlertDialogCancel' - -// AlertDialogAction -interface AlertDialogActionProps { - children?: ReactNode - onClick?: () => void - color?: 'primary' | 'error' | 'warning' | 'success' | 'info' - variant?: 'text' | 'outlined' | 'contained' - autoFocus?: boolean - className?: string -} - -const AlertDialogAction = forwardRef( - ({ children = 'Confirm', onClick, color = 'primary', variant = 'contained', autoFocus = true, className, ...props }, ref) => { - return ( - - ) - } -) -AlertDialogAction.displayName = 'AlertDialogAction' - export { AlertDialog, - AlertDialogTrigger, + AlertDialogAction, + AlertDialogCancel, AlertDialogContent, - AlertDialogHeader, - AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, - AlertDialogCancel, - AlertDialogAction, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx index 7005d4e05..a812c86f2 100644 --- a/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Command.tsx @@ -1,3 +1,6 @@ 'use client' export * from './command' +export * from './Command/Palette' +export * from './Command/Results' +export * from './Command/useCommandState' diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Palette.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Palette.tsx new file mode 100644 index 000000000..d275cb4ab --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Palette.tsx @@ -0,0 +1,38 @@ +'use client' + +import { ReactNode } from 'react' + +import { CommandDialog } from '../command' + +interface CommandPaletteProps { + children: ReactNode + open: boolean + onOpenChange: (open: boolean) => void + placeholder?: string + search: string + onSearchChange: (value: string) => void +} + +const CommandPalette = ({ + children, + open, + onOpenChange, + placeholder = 'Type a command or search...', + search, + onSearchChange, +}: CommandPaletteProps) => { + return ( + onOpenChange(false)}> + + {children} + + ) +} + +export { CommandPalette } +export type { CommandPaletteProps } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Results.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Results.tsx new file mode 100644 index 000000000..4e72f24b6 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/Results.tsx @@ -0,0 +1,56 @@ +'use client' + +import { ReactNode } from 'react' +import { Box, Typography } from '@mui/material' + +import { CommandDialog } from '../command' +import type { CommandGroup, CommandItem } from '../command/command.types' + +interface CommandResultsProps { + groups: CommandGroup[] + emptyMessage?: ReactNode + onSelect?: (item: CommandItem) => void +} + +const CommandResults = ({ groups, emptyMessage = 'No results found.', onSelect }: CommandResultsProps) => { + const hasResults = groups.some((group) => group.items.length > 0) + + return ( + + {hasResults ? ( + groups.map((group, index) => ( + + {group.items.map((item) => ( + { + item.onSelect?.() + onSelect?.(item) + }} + > + + + {item.label} + + {item.description && ( + + {item.description} + + )} + + + ))} + + )) + ) : ( + {emptyMessage} + )} + + ) +} + +export { CommandResults } +export type { CommandResultsProps } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Command/useCommandState.ts b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/useCommandState.ts new file mode 100644 index 000000000..796bfa395 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Command/useCommandState.ts @@ -0,0 +1,59 @@ +'use client' + +import { useMemo, useState } from 'react' + +import type { CommandGroup, CommandItem } from '../command/command.types' + +interface UseCommandStateOptions { + groups: CommandGroup[] + defaultOpen?: boolean +} + +interface UseCommandStateResult { + filteredGroups: CommandGroup[] + open: boolean + search: string + setOpen: (open: boolean) => void + setSearch: (value: string) => void + close: () => void +} + +const filterItems = (items: CommandItem[], query: string) => { + if (!query) return items + const lowered = query.toLowerCase() + return items.filter((item) => { + const labelMatch = item.label.toLowerCase().includes(lowered) + const keywordsMatch = item.keywords?.some((keyword) => keyword.toLowerCase().includes(lowered)) + const descriptionMatch = item.description?.toLowerCase().includes(lowered) + + return labelMatch || keywordsMatch || descriptionMatch + }) +} + +const useCommandState = ({ groups, defaultOpen = false }: UseCommandStateOptions): UseCommandStateResult => { + const [open, setOpen] = useState(defaultOpen) + const [search, setSearch] = useState('') + + const filteredGroups = useMemo(() => { + if (!search) return groups + + return groups + .map((group) => ({ + ...group, + items: filterItems(group.items, search), + })) + .filter((group) => group.items.length > 0) + }, [groups, search]) + + return { + filteredGroups, + open, + search, + setOpen, + setSearch, + close: () => setOpen(false), + } +} + +export { useCommandState } +export type { UseCommandStateOptions, UseCommandStateResult } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet.tsx index 0ea3c093e..cf4bdeba3 100644 --- a/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet.tsx +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet.tsx @@ -1,255 +1,4 @@ -// TODO: Split this file (254 LOC) into smaller organisms (<150 LOC each) 'use client' -import { forwardRef, ReactNode } from 'react' -import { - Drawer, - DrawerProps, - Box, - IconButton, - Typography, -} from '@mui/material' -import CloseIcon from '@mui/icons-material/Close' - -// Sheet (Drawer) Root Component -interface SheetProps extends Omit { - children: ReactNode - side?: 'left' | 'right' | 'top' | 'bottom' - onOpenChange?: (open: boolean) => void -} - -const Sheet = forwardRef( - ({ children, side = 'right', open, onClose, onOpenChange, ...props }, ref) => { - const handleClose = (_event: React.SyntheticEvent | object, reason: 'backdropClick' | 'escapeKeyDown') => { - if (typeof onClose === 'function') { - onClose(_event, reason) - } - onOpenChange?.(false) - } - return ( - - {children} - - ) - } -) -Sheet.displayName = 'Sheet' - -// SheetTrigger - returns children with onClick handler -interface SheetTriggerProps { - children: ReactNode - onClick?: () => void - asChild?: boolean -} - -const SheetTrigger = forwardRef( - ({ children, onClick, asChild, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetTrigger.displayName = 'SheetTrigger' - -// SheetContent -interface SheetContentProps { - children: ReactNode - side?: 'left' | 'right' | 'top' | 'bottom' - onClose?: () => void - showCloseButton?: boolean - className?: string -} - -const SheetContent = forwardRef( - ({ children, side = 'right', onClose, showCloseButton = true, ...props }, ref) => { - const isHorizontal = side === 'left' || side === 'right' - - return ( - - {showCloseButton && ( - - - - )} - {children} - - ) - } -) -SheetContent.displayName = 'SheetContent' - -// SheetHeader -interface SheetHeaderProps { - children: ReactNode - className?: string -} - -const SheetHeader = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetHeader.displayName = 'SheetHeader' - -// SheetFooter -interface SheetFooterProps { - children: ReactNode - className?: string -} - -const SheetFooter = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetFooter.displayName = 'SheetFooter' - -// SheetTitle -interface SheetTitleProps { - children: ReactNode - className?: string -} - -const SheetTitle = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetTitle.displayName = 'SheetTitle' - -// SheetDescription -interface SheetDescriptionProps { - children: ReactNode - className?: string -} - -const SheetDescription = forwardRef( - ({ children, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetDescription.displayName = 'SheetDescription' - -// SheetClose - button to close sheet -interface SheetCloseProps { - children: ReactNode - onClick?: () => void - asChild?: boolean -} - -const SheetClose = forwardRef( - ({ children, onClick, ...props }, ref) => { - return ( - - {children} - - ) - } -) -SheetClose.displayName = 'SheetClose' - -export { - Sheet, - SheetTrigger, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, - SheetClose, -} +export { Sheet, SheetClose, SheetContent, SheetTrigger } from './Sheet/Drawer' +export { SheetDescription, SheetFooter, SheetHeader, SheetTitle } from './Sheet/Header' diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Drawer.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Drawer.tsx new file mode 100644 index 000000000..0f2f83f09 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Drawer.tsx @@ -0,0 +1,129 @@ +'use client' + +import { forwardRef, ReactNode, SyntheticEvent } from 'react' +import { Box, Drawer, DrawerProps, IconButton } from '@mui/material' +import CloseIcon from '@mui/icons-material/Close' + +interface SheetProps extends Omit { + children: ReactNode + side?: 'left' | 'right' | 'top' | 'bottom' + onOpenChange?: (open: boolean) => void +} + +const Sheet = forwardRef( + ({ children, side = 'right', open, onClose, onOpenChange, ...props }, ref) => { + const handleClose = (_event: SyntheticEvent | object, reason: 'backdropClick' | 'escapeKeyDown') => { + if (typeof onClose === 'function') { + onClose(_event, reason) + } + onOpenChange?.(false) + } + return ( + + {children} + + ) + } +) +Sheet.displayName = 'Sheet' + +interface SheetTriggerProps { + children: ReactNode + onClick?: () => void + asChild?: boolean +} + +const SheetTrigger = forwardRef( + ({ children, onClick, asChild, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetTrigger.displayName = 'SheetTrigger' + +interface SheetContentProps { + children: ReactNode + side?: 'left' | 'right' | 'top' | 'bottom' + onClose?: () => void + showCloseButton?: boolean + className?: string +} + +const SheetContent = forwardRef( + ({ children, side = 'right', onClose, showCloseButton = true, ...props }, ref) => { + const isHorizontal = side === 'left' || side === 'right' + + return ( + + {showCloseButton && ( + + + + )} + {children} + + ) + } +) +SheetContent.displayName = 'SheetContent' + +interface SheetCloseProps { + children: ReactNode + onClick?: () => void + asChild?: boolean +} + +const SheetClose = forwardRef( + ({ children, onClick, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetClose.displayName = 'SheetClose' + +export { Sheet, SheetClose, SheetContent, SheetTrigger } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Header.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Header.tsx new file mode 100644 index 000000000..358164d5b --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/Sheet/Header.tsx @@ -0,0 +1,108 @@ +'use client' + +import { forwardRef, ReactNode } from 'react' +import { Box, Typography } from '@mui/material' + +interface SheetHeaderProps { + children: ReactNode + className?: string +} + +const SheetHeader = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetHeader.displayName = 'SheetHeader' + +interface SheetFooterProps { + children: ReactNode + className?: string +} + +const SheetFooter = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetFooter.displayName = 'SheetFooter' + +interface SheetTitleProps { + children: ReactNode + className?: string +} + +const SheetTitle = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetTitle.displayName = 'SheetTitle' + +interface SheetDescriptionProps { + children: ReactNode + className?: string +} + +const SheetDescription = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +SheetDescription.displayName = 'SheetDescription' + +export { SheetDescription, SheetFooter, SheetHeader, SheetTitle } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Actions.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Actions.tsx new file mode 100644 index 000000000..6670952cb --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Actions.tsx @@ -0,0 +1,69 @@ +'use client' + +import { forwardRef, ReactNode } from 'react' +import { Button, DialogActions } from '@mui/material' + +interface AlertDialogFooterProps { + children: ReactNode +} + +const AlertDialogFooter = forwardRef( + ({ children, ...props }, ref) => { + return ( + + {children} + + ) + } +) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +interface AlertDialogCancelProps { + children?: ReactNode + onClick?: () => void + className?: string +} + +const AlertDialogCancel = forwardRef( + ({ children = 'Cancel', onClick, className, ...props }, ref) => { + return ( + + ) + } +) +AlertDialogCancel.displayName = 'AlertDialogCancel' + +interface AlertDialogActionProps { + children?: ReactNode + onClick?: () => void + color?: 'primary' | 'error' | 'warning' | 'success' | 'info' + variant?: 'text' | 'outlined' | 'contained' + autoFocus?: boolean + className?: string +} + +const AlertDialogAction = forwardRef( + ( + { children = 'Confirm', onClick, color = 'primary', variant = 'contained', autoFocus = true, className, ...props }, + ref + ) => { + return ( + + ) + } +) +AlertDialogAction.displayName = 'AlertDialogAction' + +export { AlertDialogAction, AlertDialogCancel, AlertDialogFooter } diff --git a/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Content.tsx b/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Content.tsx new file mode 100644 index 000000000..16842add6 --- /dev/null +++ b/frontends/nextjs/src/components/ui/organisms/dialogs/alert/Content.tsx @@ -0,0 +1,141 @@ +'use client' + +import { forwardRef, ReactNode } from 'react' +import { + DialogContent, + DialogContentText, + DialogTitle, + IconButton, + Typography, +} from '@mui/material' +import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline' +import CloseIcon from '@mui/icons-material/Close' +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline' +import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined' +import WarningAmberIcon from '@mui/icons-material/WarningAmber' + +interface AlertDialogContentProps { + children: ReactNode + showCloseButton?: boolean + onClose?: () => void + className?: string +} + +const AlertDialogContent = forwardRef( + ({ children, showCloseButton = false, onClose, className, ...props }, ref) => { + return ( + <> + {showCloseButton && onClose && ( + + + + )} + {children} + + ) + } +) +AlertDialogContent.displayName = 'AlertDialogContent' + +interface AlertDialogHeaderProps { + children: ReactNode + icon?: 'warning' | 'error' | 'info' | 'success' | ReactNode +} + +const AlertDialogHeader = forwardRef( + ({ children, icon, ...props }, ref) => { + const getIcon = () => { + if (!icon) return null + if (typeof icon !== 'string') return icon + + const iconMap = { + warning: , + error: , + info: , + success: , + } + return iconMap[icon] + } + + const iconElement = getIcon() + + return ( + + {iconElement} + {children} + + ) + } +) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +interface AlertDialogTitleProps { + children: ReactNode + className?: string +} + +const AlertDialogTitle = forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + {children} + + ) + } +) +AlertDialogTitle.displayName = 'AlertDialogTitle' + +interface AlertDialogDescriptionProps { + children: ReactNode + className?: string +} + +const AlertDialogDescription = forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + + {children} + + + ) + } +) +AlertDialogDescription.displayName = 'AlertDialogDescription' + +export { + AlertDialogContent, + AlertDialogDescription, + AlertDialogHeader, + AlertDialogTitle, +}