diff --git a/src/components/ui/AppDialog.tsx b/src/components/ui/AppDialog.tsx index bb110360..ff06379a 100644 --- a/src/components/ui/AppDialog.tsx +++ b/src/components/ui/AppDialog.tsx @@ -1,77 +1,84 @@ - import { - Dialog, - DialogTitle, - DialogContent, - DialogActions, - IconButton, - Typography, - Button -} from "@mui/material"; -import CloseIcon from "@mui/icons-material/Close"; + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { X } from "lucide-react"; import { ReactNode } from "react"; type AppDialogProps = { - open: boolean; - onClose: () => void; - title: string; - children: ReactNode; - maxWidth?: "xs" | "sm" | "md" | "lg" | "xl"; - actions?: ReactNode; - submitLabel?: string; - onSubmit?: () => void; - isSubmitting?: boolean; + open: boolean; + onClose: () => void; + title: string; + children: ReactNode; + maxWidth?: "xs" | "sm" | "md" | "lg" | "xl"; + actions?: ReactNode; + submitLabel?: string; + onSubmit?: () => void; + isSubmitting?: boolean; +}; + +const MAX_WIDTH_CLASS: Record, string> = { + xs: "max-w-xs", + sm: "max-w-sm", + md: "max-w-md", + lg: "max-w-2xl", + xl: "max-w-4xl", }; export function AppDialog({ - open, - onClose, - title, - children, - maxWidth = "sm", - actions, - submitLabel = "Save", - onSubmit, - isSubmitting = false + open, + onClose, + title, + children, + maxWidth = "sm", + actions, + submitLabel = "Save", + onSubmit, + isSubmitting = false, }: AppDialogProps) { - return ( - - - {title} - - - - - - {children} - - - {actions ? actions : ( + return ( + !o && onClose()}> + + + {title} + + + +
{children}
+ + + {actions ?? ( + <> + + {onSubmit && ( + - {onSubmit && ( - - )} + + Saving… - )} -
-
- ); + ) : ( + submitLabel + )} + + )} + + )} + + + + ); } diff --git a/src/components/ui/DataTable.tsx b/src/components/ui/DataTable.tsx index 8e0502e7..c6883fa7 100644 --- a/src/components/ui/DataTable.tsx +++ b/src/components/ui/DataTable.tsx @@ -1,20 +1,16 @@ "use client"; import { - Box, - Card, - Pagination, - Stack, Table, TableBody, TableCell, - TableContainer, TableHead, + TableHeader, TableRow, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material"; +} from "@/components/ui/table"; +import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { ChevronLeft, ChevronRight } from "lucide-react"; import { ReactNode } from "react"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -38,7 +34,6 @@ type DataTableProps = { page: number; perPage: number; }; - /** If provided, renders this instead of the table on xs/sm screens */ mobileCard?: (row: T) => ReactNode; }; @@ -50,22 +45,91 @@ function PaginationBar({ page, perPage, total }: { page: number; perPage: number if (pageCount <= 1) return null; - function handlePageChange(_: React.ChangeEvent, newPage: number) { + function goTo(newPage: number) { const params = new URLSearchParams(searchParams.toString()); params.set("page", String(newPage)); router.push(`${pathname}?${params.toString()}`); } return ( - - - +
+ + + Page {page} of {pageCount} + + +
+ ); +} + +function DesktopTable({ + columns, data, keyField, emptyMessage, onRowClick, isEmpty, +}: { + columns: Column[]; + data: T[]; + keyField: keyof T; + emptyMessage: string; + onRowClick?: (row: T) => void; + isEmpty: boolean; +}) { + return ( +
+ + + + {columns.map((col) => ( + + {col.label} + + ))} + + + + {isEmpty ? ( + + + {emptyMessage} + + + ) : ( + data.map((row) => ( + onRowClick(row) : undefined} + className={onRowClick ? "cursor-pointer hover:bg-muted/50" : ""} + > + {columns.map((col) => ( + + {col.render ? col.render(row) : (row as Record)[col.id] as ReactNode} + + ))} + + )) + )} + +
+
); } @@ -79,76 +143,47 @@ export function DataTable({ pagination, mobileCard, }: DataTableProps) { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); - const isEmpty = data.length === 0 && !loading; - if (isMobile && mobileCard) { + if (mobileCard) { return ( - - {isEmpty ? ( - - {emptyMessage} - - ) : ( - - {data.map((row) => ( - - {mobileCard(row)} - - ))} - - )} - {pagination && } - +
+
+ {isEmpty ? ( + + + {emptyMessage} + + + ) : ( +
+ {data.map((row) => ( +
{mobileCard(row)}
+ ))} +
+ )} + {pagination && } +
+
+ + {pagination && } +
+
); } return ( - - - - - - {columns.map((col) => ( - - {col.label} - - ))} - - - - {isEmpty ? ( - - - {emptyMessage} - - - ) : ( - data.map((row) => ( - onRowClick(row) : undefined} - sx={onRowClick ? { cursor: "pointer", "&:hover": { bgcolor: "action.hover" } } : undefined} - > - {columns.map((col) => ( - - {col.render ? col.render(row) : (row as Record)[col.id] as ReactNode} - - ))} - - )) - )} - -
-
- +
+ {pagination && } - +
); } diff --git a/src/components/ui/PageHeader.tsx b/src/components/ui/PageHeader.tsx index c559a2d3..eb19075c 100644 --- a/src/components/ui/PageHeader.tsx +++ b/src/components/ui/PageHeader.tsx @@ -1,47 +1,32 @@ -import { Button, Stack, Typography } from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; +import { Plus } from "lucide-react"; import { ReactNode } from "react"; +import { Button } from "@/components/ui/button"; type PageHeaderProps = { - title: string; - description?: string; - action?: { - label: string; - onClick: () => void; - icon?: ReactNode; - }; + title: string; + description?: string; + action?: { + label: string; + onClick: () => void; + icon?: ReactNode; + }; }; export function PageHeader({ title, description, action }: PageHeaderProps) { - return ( - - - - {title} - - {description && ( - - {description} - - )} - - {action && ( - - )} - - ); + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {action && ( + + )} +
+ ); } diff --git a/src/components/ui/SearchField.tsx b/src/components/ui/SearchField.tsx index fd052fc0..3ecb0eeb 100644 --- a/src/components/ui/SearchField.tsx +++ b/src/components/ui/SearchField.tsx @@ -1,37 +1,21 @@ +import { Search } from "lucide-react"; +import { Input } from "@/components/ui/input"; +import { cn } from "@/lib/utils"; +import { InputHTMLAttributes } from "react"; -import { InputAdornment, TextField, TextFieldProps } from "@mui/material"; -import SearchIcon from "@mui/icons-material/Search"; +type SearchFieldProps = InputHTMLAttributes & { + className?: string; +}; -export function SearchField(props: TextFieldProps) { - return ( - - - - ) - } - }} - sx={{ - maxWidth: 400, - "& .MuiOutlinedInput-root": { - bgcolor: "background.paper", - transition: "all 0.2s", - "&:hover": { - bgcolor: "action.hover" - }, - "&.Mui-focused": { - bgcolor: "background.paper", - boxShadow: "0 4px 20px rgba(0,0,0,0.2)" - } - } - }} - {...props} - /> - ); +export function SearchField({ className, ...props }: SearchFieldProps) { + return ( +
+ + +
+ ); } diff --git a/src/components/ui/StatusChip.tsx b/src/components/ui/StatusChip.tsx index daa54e06..4588517a 100644 --- a/src/components/ui/StatusChip.tsx +++ b/src/components/ui/StatusChip.tsx @@ -1,58 +1,34 @@ -import { Box, Typography } from "@mui/material"; -import type { SxProps, Theme } from "@mui/material"; +import { cn } from "@/lib/utils"; type StatusType = "active" | "inactive" | "error" | "warning"; type StatusChipProps = { - status: StatusType; - label?: string; - sx?: SxProps; + status: StatusType; + label?: string; + className?: string; }; -const STATUS_CONFIG: Record = { - active: { color: "#22c55e", label: "Active" }, // Green-500 - inactive: { color: "#71717a", label: "Paused" }, // Zinc-500 - error: { color: "#ef4444", label: "Error" }, // Red-500 - warning: { color: "#f59e0b", label: "Warning" } // Amber-500 +const STATUS_CONFIG: Record = { + active: { dot: "bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.4)]", text: "text-green-500", label: "Active" }, + inactive: { dot: "bg-zinc-500", text: "text-zinc-400", label: "Paused" }, + error: { dot: "bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.4)]", text: "text-red-500", label: "Error" }, + warning: { dot: "bg-amber-500 shadow-[0_0_8px_rgba(245,158,11,0.4)]", text: "text-amber-500", label: "Warning" }, }; -export function StatusChip({ status, label, sx }: StatusChipProps) { - const config = STATUS_CONFIG[status]; - const displayLabel = label || config.label; +export function StatusChip({ status, label, className }: StatusChipProps) { + const config = STATUS_CONFIG[status]; + const displayLabel = label ?? config.label; - return ( - - - - {displayLabel} - - - ); + return ( + + + + {displayLabel} + + + ); }