generic src for react admin

This commit is contained in:
2026-04-01 14:22:14 +05:30
parent 4d06859cb0
commit 14dcd19b17
13 changed files with 1137 additions and 7 deletions

View File

@@ -0,0 +1,103 @@
import * as React from 'react';
import {
Box,
Drawer,
AppBar,
Toolbar,
List,
Typography,
Divider,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
CssBaseline,
IconButton
} from '@mui/material';
import TableViewIcon from '@mui/icons-material/TableView';
import DashboardIcon from '@mui/icons-material/Dashboard';
import LogoutIcon from '@mui/icons-material/Logout';
import { config } from '../config';
const drawerWidth = 240;
interface AdminLayoutProps {
children: React.ReactNode;
onSelectResource: (resourceName: string | null) => void;
selectedResourceName: string | null;
onLogout: () => void;
username?: string;
}
export default function AdminLayout({
children,
onSelectResource,
selectedResourceName,
onLogout,
username
}: AdminLayoutProps) {
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
<AppBar position="fixed" sx={{ zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar>
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
Admin Panel
</Typography>
<Typography variant="body1" sx={{ mr: 2 }}>
{username}
</Typography>
<IconButton color="inherit" onClick={onLogout}>
<LogoutIcon />
</IconButton>
</Toolbar>
</AppBar>
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
[`& .MuiDrawer-paper`]: { width: drawerWidth, boxSizing: 'border-box' },
}}
>
<Toolbar />
<Box sx={{ overflow: 'auto' }}>
<List>
<ListItem disablePadding>
<ListItemButton
selected={selectedResourceName === null}
onClick={() => onSelectResource(null)}
>
<ListItemIcon>
<DashboardIcon />
</ListItemIcon>
<ListItemText primary="Dashboard" />
</ListItemButton>
</ListItem>
</List>
<Divider />
<List>
{config.resources.map((res) => (
<ListItem key={res.name} disablePadding>
<ListItemButton
selected={selectedResourceName === res.name}
onClick={() => onSelectResource(res.name)}
>
<ListItemIcon>
<TableViewIcon />
</ListItemIcon>
<ListItemText primary={res.pluralLabel} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
</Box>
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
{children}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,163 @@
import * as React from 'react';
import {
Box,
TextField,
Button,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Checkbox,
Typography,
Divider,
} from '@mui/material';
import { ResourceConfig, ResourceField } from '../types/config';
interface GenericFormProps {
config: ResourceConfig;
initialData?: any;
onSave: (data: any) => Promise<void>;
onCancel: () => void;
loading?: boolean;
}
export default function GenericForm({
config,
initialData = {},
onSave,
onCancel,
loading,
}: GenericFormProps) {
const [formData, setFormData] = React.useState(initialData);
const handleChange = (key: string, value: any) => {
setFormData((prev: any) => ({ ...prev, [key]: value }));
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSave(formData);
};
return (
<Box component="form" onSubmit={handleSubmit} sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Typography variant="h5">
{initialData[config.primaryKey] ? `Edit ${config.label}` : `New ${config.label}`}
</Typography>
<Divider />
{Object.entries(config.fields).map(([key, field]) => (
<FormField
key={key}
name={key}
field={field}
value={formData[key]}
onChange={(val) => handleChange(key, val)}
disabled={field.readOnly}
/>
))}
<Box sx={{ display: 'flex', justifyContent: 'flex-end', gap: 2, mt: 4 }}>
<Button variant="outlined" onClick={onCancel} disabled={loading}>
Cancel
</Button>
<Button variant="contained" type="submit" loading={loading} disabled={loading}>
Save {config.label}
</Button>
</Box>
</Box>
);
}
function FormField({ name, field, value, onChange, disabled }: any) {
const label = field.label;
if (field.type === 'boolean') {
return (
<FormControlLabel
control={
<Checkbox
checked={!!value}
onChange={(e) => onChange(e.target.checked)}
disabled={disabled}
/>
}
label={label}
/>
);
}
if (field.type === 'enum' && field.options) {
return (
<FormControl fullWidth>
<InputLabel>{label}</InputLabel>
<Select
value={value || ''}
label={label}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
>
{field.options.map((opt: string) => (
<MenuItem key={opt} value={opt}>
{opt}
</MenuItem>
))}
</Select>
</FormControl>
);
}
if (field.type === 'markdown' || field.type === 'string') {
return (
<TextField
fullWidth
label={label}
value={value || ''}
multiline={field.type === 'markdown'}
rows={field.type === 'markdown' ? 4 : 1}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
required={field.required}
/>
);
}
if (field.type === 'number') {
return (
<TextField
fullWidth
label={label}
type="number"
value={value || 0}
onChange={(e) => onChange(Number(e.target.value))}
disabled={disabled}
required={field.required}
/>
);
}
if (field.type === 'date') {
return (
<TextField
fullWidth
label={label}
type="date"
InputLabelProps={{ shrink: true }}
value={value ? new Date(value).toISOString().split('T')[0] : ''}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
required={field.required}
/>
);
}
return (
<TextField
fullWidth
label={label}
value={JSON.stringify(value)}
disabled
/>
);
}

View File

@@ -0,0 +1,95 @@
import * as React from 'react';
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Paper,
IconButton,
Typography,
Box,
Button
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import { ResourceConfig } from '../types/config';
interface GenericTableProps {
config: ResourceConfig;
data: any[];
onEdit: (item: any) => void;
onDelete: (id: string) => void;
onCreate: () => void;
}
export default function GenericTable({
config,
data,
onEdit,
onDelete,
onCreate,
}: GenericTableProps) {
const fields = Object.entries(config.fields);
return (
<Box>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 3, alignItems: 'center' }}>
<Typography variant="h5">{config.pluralLabel}</Typography>
<Button variant="contained" color="primary" onClick={onCreate}>
Add {config.label}
</Button>
</Box>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
{fields.map(([key, field]) => (
<TableCell key={key}>{field.label}</TableCell>
))}
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.map((item) => (
<TableRow key={item[config.primaryKey]}>
{fields.map(([key, field]) => (
<TableCell key={key}>
{renderCellValue(item[key], field)}
</TableCell>
))}
<TableCell align="right">
<IconButton onClick={() => onEdit(item)}>
<EditIcon />
</IconButton>
<IconButton onClick={() => onDelete(item[config.primaryKey])}>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}
function renderCellValue(value: any, field: any) {
if (value === null || value === undefined) return '-';
switch (field.type) {
case 'boolean':
return value ? 'Yes' : 'No';
case 'date':
return new Date(value).toLocaleDateString();
case 'object':
return JSON.stringify(value);
case 'array':
return `${value.length} items`;
default:
return String(value);
}
}

View File

@@ -0,0 +1,79 @@
import * as React from 'react';
import { Box, Typography, Button, Paper, CircularProgress } from '@mui/material';
import { ResourceConfig } from '../types/config';
import { useResource } from '../hooks/useResource';
import GenericTable from './GenericTable';
import GenericForm from './GenericForm';
interface ResourceViewProps {
config: ResourceConfig;
}
export default function ResourceView({ config }: ResourceViewProps) {
const [view, setView] = React.useState<'list' | 'create' | 'edit'>('list');
const [selectedItem, setSelectedItem] = React.useState<any>(null);
const { useList, useCreate, useUpdate, useDelete } = useResource(config);
const { data, isLoading, error } = useList();
const createMutation = useCreate();
const updateMutation = useUpdate();
const deleteMutation = useDelete();
const handleEdit = (item: any) => {
setSelectedItem(item);
setView('edit');
};
const handleCreate = () => {
setSelectedItem(null);
setView('create');
};
const handleSave = async (formData: any) => {
try {
if (view === 'edit') {
const id = formData[config.primaryKey];
await updateMutation.mutateAsync({ id, data: formData });
} else {
await createMutation.mutateAsync(formData);
}
setView('list');
} catch (err) {
console.error('Save failed:', err);
}
};
const handleDelete = async (id: string) => {
if (window.confirm('Are you sure you want to delete this item?')) {
await deleteMutation.mutateAsync(id);
}
};
if (isLoading) return <CircularProgress />;
if (error) return <Typography color="error">Error loading {config.pluralLabel}</Typography>;
return (
<Box>
{view === 'list' ? (
<GenericTable
config={config}
data={data || []}
onEdit={handleEdit}
onDelete={handleDelete}
onCreate={handleCreate}
/>
) : (
<Paper sx={{ p: 4 }}>
<GenericForm
config={config}
initialData={selectedItem}
onSave={handleSave}
onCancel={() => setView('list')}
loading={createMutation.isPending || updateMutation.isPending}
/>
</Paper>
)}
</Box>
);
}