From e23f1255feaaa2dbf7bc600df43cae85fc456a3c Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sat, 28 Feb 2026 20:06:38 +0100 Subject: [PATCH] feat: add reusable DataTable and EntityForm components --- frontend/src/components/DataTable.tsx | 61 +++++++++++++++++++ frontend/src/components/EntityForm.tsx | 83 ++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 frontend/src/components/DataTable.tsx create mode 100644 frontend/src/components/EntityForm.tsx diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx new file mode 100644 index 0000000..4f44177 --- /dev/null +++ b/frontend/src/components/DataTable.tsx @@ -0,0 +1,61 @@ +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import IconButton from '@mui/material/IconButton' +import EditIcon from '@mui/icons-material/Edit' +import DeleteIcon from '@mui/icons-material/Delete' +import Typography from '@mui/material/Typography' + +export interface Column { + key: keyof T + label: string + render?: (row: T) => React.ReactNode +} + +interface Props { + columns: Column[] + rows: T[] + onEdit: (row: T) => void + onDelete: (row: T) => void +} + +export default function DataTable({ columns, rows, onEdit, onDelete }: Props) { + if (rows.length === 0) { + return No entries yet. + } + return ( + + + + + {columns.map((col) => ( + + {col.label} + + ))} + Actions + + + + {rows.map((row) => ( + + {columns.map((col) => ( + + {col.render ? col.render(row) : String(row[col.key] ?? '')} + + ))} + + onEdit(row)}> + onDelete(row)} color="error"> + + + ))} + +
+
+ ) +} diff --git a/frontend/src/components/EntityForm.tsx b/frontend/src/components/EntityForm.tsx new file mode 100644 index 0000000..3d73d1a --- /dev/null +++ b/frontend/src/components/EntityForm.tsx @@ -0,0 +1,83 @@ +import { useState, useEffect } from 'react' +import Dialog from '@mui/material/Dialog' +import DialogTitle from '@mui/material/DialogTitle' +import DialogContent from '@mui/material/DialogContent' +import DialogActions from '@mui/material/DialogActions' +import Button from '@mui/material/Button' +import TextField from '@mui/material/TextField' +import MenuItem from '@mui/material/MenuItem' +import Stack from '@mui/material/Stack' + +export interface FieldDef { + name: string + label: string + required?: boolean + type?: 'text' | 'select' | 'number' + options?: { value: string | number; label: string }[] +} + +interface Props { + open: boolean + title: string + fields: FieldDef[] + initialValues?: Record + onClose: () => void + onSubmit: (values: Record) => Promise +} + +export default function EntityForm({ open, title, fields, initialValues, onClose, onSubmit }: Props) { + const [values, setValues] = useState>({}) + const [submitting, setSubmitting] = useState(false) + + useEffect(() => { + if (open) setValues(initialValues ?? {}) + }, [open, initialValues]) + + const handleChange = (name: string, value: unknown) => setValues((v) => ({ ...v, [name]: value })) + + const handleSubmit = async () => { + setSubmitting(true) + try { await onSubmit(values) } finally { setSubmitting(false) } + } + + return ( + + {title} + + + {fields.map((f) => + f.type === 'select' ? ( + handleChange(f.name, e.target.value)} + size="small" + > + {f.options?.map((o) => {o.label})} + + ) : ( + handleChange(f.name, e.target.value)} + size="small" + /> + ) + )} + + + + + + + + ) +}