diff --git a/frontends/nextjs/src/data/form/FieldGroup.tsx b/frontends/nextjs/src/data/form/FieldGroup.tsx new file mode 100644 index 000000000..67ad752ca --- /dev/null +++ b/frontends/nextjs/src/data/form/FieldGroup.tsx @@ -0,0 +1,50 @@ +import type { ReactNode } from 'react' +import { Box, Stack, Typography } from '@mui/material' + +interface FieldGroupProps { + title: string + description?: ReactNode + actions?: ReactNode + children: ReactNode + spacing?: number +} + +export function FieldGroup({ + title, + description, + actions, + children, + spacing = 2, +}: FieldGroupProps) { + return ( + + + + + {title} + + {description ? ( + + {description} + + ) : null} + + + {actions ? ( + {actions} + ) : null} + + + {children} + + ) +} diff --git a/frontends/nextjs/src/data/form/ValidationSummary.tsx b/frontends/nextjs/src/data/form/ValidationSummary.tsx new file mode 100644 index 000000000..57eb90278 --- /dev/null +++ b/frontends/nextjs/src/data/form/ValidationSummary.tsx @@ -0,0 +1,53 @@ +import type { ReactNode } from 'react' +import { Alert, AlertTitle, List, ListItem, ListItemText } from '@mui/material' + +/** + * Props for {@link ValidationSummary}. + * + * @property errors List of validation errors to display. Each entry can be a plain + * string message or a ReactNode for fully custom rendering (e.g. including links + * or emphasized text). The list is rendered as items in a bulleted list. + * @property title Optional title rendered inside the error alert header. This + * is only shown when {@link ValidationSummaryProps.showTitle | showTitle} is + * `true`. Defaults to `"Please fix the following"`. + * @property showTitle When `true` (default), renders an {@link AlertTitle} using + * the provided `title`. Set to `false` when you want to suppress the header and + * only show the list of errors. + */ +interface ValidationSummaryProps { + errors: Array + title?: string + showTitle?: boolean +} + +/** + * Displays a standardized validation error summary as an error {@link Alert} + * containing an optional title and a bulleted list of errors. + * + * Renders nothing when the `errors` array is empty. + */ +export function ValidationSummary({ + errors, + title = 'Please fix the following', + showTitle = true, +}: ValidationSummaryProps) { + if (!errors.length) return null + + return ( + + {showTitle ? {title} : null} + + {errors.map((error, index) => ( + + + + ))} + + + ) +} diff --git a/frontends/nextjs/src/data/table/Body.tsx b/frontends/nextjs/src/data/table/Body.tsx new file mode 100644 index 000000000..80225d390 --- /dev/null +++ b/frontends/nextjs/src/data/table/Body.tsx @@ -0,0 +1,59 @@ +import type { ReactNode } from 'react' +import { TableBody, TableCell, TableRow } from '@mui/material' + +import { EmptyState } from './EmptyState' +import type { DataTableColumn } from './types' + +interface BodyProps { + columns: Array> + rows: T[] + getRowId?: (row: T, rowIndex: number) => string | number + renderActions?: (row: T) => ReactNode + onRowClick?: (row: T) => void + emptyMessage?: string +} + +export function Body({ + columns, + rows, + getRowId, + renderActions, + onRowClick, + emptyMessage = 'No records found', +}: BodyProps) { + const colSpan = columns.length + (renderActions ? 1 : 0) + + return ( + + {rows.length === 0 ? ( + + ) : ( + rows.map((row, rowIndex) => { + const rowId = getRowId ? getRowId(row, rowIndex) : rowIndex + const handleClick = onRowClick ? () => onRowClick(row) : undefined + + return ( + + {columns.map((column) => { + const content = column.render ? column.render(row, rowIndex) : (row as Record)[column.key] + + return ( + + {content ?? '—'} + + ) + })} + + {renderActions ? {renderActions(row)} : null} + + ) + }) + )} + + ) +} diff --git a/frontends/nextjs/src/data/table/EmptyState.tsx b/frontends/nextjs/src/data/table/EmptyState.tsx new file mode 100644 index 000000000..b79247aaa --- /dev/null +++ b/frontends/nextjs/src/data/table/EmptyState.tsx @@ -0,0 +1,40 @@ +import type { ReactNode } from 'react' +import { Stack, TableCell, TableRow, Typography } from '@mui/material' + +/** + * Props for the {@link EmptyState} table row. + * + * This is typically used when a data-driven table has no rows to display and + * you want to show a friendly message, optionally with a follow-up action. + * + * @property colSpan - Number of table columns the empty-state cell should span. + * @property message - Optional message shown to explain that there is no data. + * @property action - Optional call-to-action content (for example, a "Create" + * button or "Reload" button) rendered below the message. + */ +interface EmptyStateProps { + colSpan: number + message?: string + action?: ReactNode +} + +/** + * Renders a full-width table row indicating that there is currently no data + * to display for the surrounding table. + * + * Use this component as the only row in a table body when the data source is + * empty. You can pass an optional `action` to surface primary actions such as + * creating a new item or retrying a failed load. + */ +export function EmptyState({ colSpan, message = 'No data to display', action }: EmptyStateProps) { + return ( + + + + {message} + {action ? {action} : null} + + + + ) +} diff --git a/frontends/nextjs/src/data/table/Header.tsx b/frontends/nextjs/src/data/table/Header.tsx new file mode 100644 index 000000000..a7c9f6602 --- /dev/null +++ b/frontends/nextjs/src/data/table/Header.tsx @@ -0,0 +1,52 @@ +import { TableCell, TableHead, TableRow, Typography } from '@mui/material' + +import type { DataTableColumn } from './types' + +interface HeaderProps { + columns: Array> + actionsHeader?: string +} + +export function Header({ columns, actionsHeader }: HeaderProps) { + return ( + + + {columns.map((column) => ( + + + {column.label} + + + ))} + + {actionsHeader ? ( + + + {actionsHeader} + + + ) : null} + + + ) +} diff --git a/frontends/nextjs/src/data/table/types.ts b/frontends/nextjs/src/data/table/types.ts new file mode 100644 index 000000000..d0b94318f --- /dev/null +++ b/frontends/nextjs/src/data/table/types.ts @@ -0,0 +1,11 @@ +import type { ReactNode } from 'react' +import type { TableCellProps } from '@mui/material' + +export interface DataTableColumn { + key: string + label: string + align?: TableCellProps['align'] + width?: number | string + render?: (row: T, rowIndex: number) => ReactNode + sx?: TableCellProps['sx'] +}