diff --git a/src_generic/components/AdminLayout.tsx b/src_generic/components/AdminLayout.tsx
index 414cdaf..bc717d8 100644
--- a/src_generic/components/AdminLayout.tsx
+++ b/src_generic/components/AdminLayout.tsx
@@ -14,10 +14,13 @@ import {
CssBaseline,
IconButton,
Tooltip,
+ useMediaQuery,
+ useTheme,
} 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 MenuIcon from '@mui/icons-material/Menu';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { ResourceConfig } from '../types/config';
@@ -41,26 +44,108 @@ export default function AdminLayout({
username,
resources,
}: AdminLayoutProps) {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const location = useLocation();
const navigate = useNavigate();
+
const [isCollapsed, setIsCollapsed] = React.useState(false);
+ const [mobileOpen, setMobileOpen] = React.useState(false);
+
const activeResourceName = location.pathname.split('/')[1] || null;
// AUTO-TOGGLE LOGIC
React.useEffect(() => {
- if (location.pathname === '/' || location.pathname === '') {
- setIsCollapsed(false);
+ if (isMobile) {
+ setIsCollapsed(false); // Mobile drawer is never "mini"
+ setMobileOpen(false); // Close on navigation
} else {
- setIsCollapsed(true);
+ if (location.pathname === '/' || location.pathname === '') {
+ setIsCollapsed(false);
+ } else {
+ setIsCollapsed(true);
+ }
}
- }, [location.pathname]);
+ }, [location.pathname, isMobile]);
- const currentWidth = isCollapsed ? collapsedWidth : drawerWidth;
+ const currentWidth = isMobile ? drawerWidth : (isCollapsed ? collapsedWidth : drawerWidth);
- const handleToggle = () => {
+ const handleDrawerToggle = () => {
+ setMobileOpen(!mobileOpen);
+ };
+
+ const handleSidebarToggle = () => {
setIsCollapsed(!isCollapsed);
};
+ const drawerContent = (
+
+ {!isMobile && (
+ <>
+
+
+ {isCollapsed ? : }
+
+
+
+ >
+ )}
+ {isMobile && }
+
+
+
+
+ navigate('/')}
+ sx={{
+ minHeight: 48,
+ justifyContent: (isCollapsed && !isMobile) ? 'center' : 'initial',
+ px: 2.5,
+ }}
+ >
+
+
+
+ {(!isCollapsed || isMobile) && }
+
+
+
+
+
+
+ {resources.map((res) => (
+
+
+ onSelectResource(res.name)}
+ sx={{
+ minHeight: 48,
+ justifyContent: (isCollapsed && !isMobile) ? 'center' : 'initial',
+ px: 2.5,
+ }}
+ >
+
+
+
+ {(!isCollapsed || isMobile) && }
+
+
+
+ ))}
+
+
+ );
+
return (
@@ -69,7 +154,7 @@ export default function AdminLayout({
sx={{
zIndex: (theme) => theme.zIndex.drawer + 1,
backdropFilter: 'blur(8px)',
- backgroundColor: 'rgba(255, 255, 255, 0.8)', // Adjust based on theme in real app
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
color: 'text.primary',
boxShadow: 'none',
borderBottom: '1px solid',
@@ -77,10 +162,21 @@ export default function AdminLayout({
}}
>
+ {isMobile && (
+
+
+
+ )}
Admin Panel
-
+
{username}
@@ -90,95 +186,60 @@ export default function AdminLayout({
- theme.transitions.create('width', {
- easing: theme.transitions.easing.sharp,
- duration: theme.transitions.duration.enteringScreen,
- }),
- [`& .MuiDrawer-paper`]: {
- width: currentWidth,
- boxSizing: 'border-box',
- overflowX: 'hidden',
- transition: (theme) => theme.transitions.create('width', {
+
+
+ {isMobile ? (
+
+ {drawerContent}
+
+ ) : (
+ theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
- }),
- },
- }}
- >
-
-
-
-
- {isCollapsed ? : }
-
-
-
-
-
-
- navigate('/')}
- sx={{
- minHeight: 48,
- justifyContent: isCollapsed ? 'center' : 'initial',
- px: 2.5,
- }}
- >
-
-
-
- {!isCollapsed && }
-
-
-
-
-
-
- {resources.map((res) => (
-
-
- onSelectResource(res.name)}
- sx={{
- minHeight: 48,
- justifyContent: isCollapsed ? 'center' : 'initial',
- px: 2.5,
- }}
- >
-
-
-
- {!isCollapsed && }
-
-
-
- ))}
-
-
-
+ }),
+ [`& .MuiDrawer-paper`]: {
+ width: currentWidth,
+ boxSizing: 'border-box',
+ overflowX: 'hidden',
+ transition: (theme) => theme.transitions.create('width', {
+ easing: theme.transitions.easing.sharp,
+ duration: theme.transitions.duration.enteringScreen,
+ }),
+ },
+ }}
+ open
+ >
+ {drawerContent}
+
+ )}
+
+
theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
diff --git a/src_generic/components/EnhancedTable.tsx b/src_generic/components/EnhancedTable.tsx
index f9282ad..1c59d34 100644
--- a/src_generic/components/EnhancedTable.tsx
+++ b/src_generic/components/EnhancedTable.tsx
@@ -6,6 +6,15 @@ import {
IconButton,
Link,
Tooltip,
+ Card,
+ CardContent,
+ CardActions,
+ Grid,
+ Menu,
+ MenuItem,
+ useMediaQuery,
+ useTheme,
+ Divider,
} from '@mui/material';
import {
DataGrid,
@@ -16,6 +25,7 @@ import {
import EditIcon from '@mui/icons-material/Edit';
import DeleteIcon from '@mui/icons-material/Delete';
import VisibilityIcon from '@mui/icons-material/Visibility';
+import MoreVertIcon from '@mui/icons-material/MoreVert';
import { useNavigate } from 'react-router-dom';
import { ResourceConfig } from '../types/config';
@@ -36,6 +46,8 @@ export default function EnhancedTable({
onCreate,
onNavigateToResource,
}: EnhancedTableProps) {
+ const theme = useTheme();
+ const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const navigate = useNavigate();
const columns: GridColDef[] = React.useMemo(() => {
@@ -45,87 +57,7 @@ export default function EnhancedTable({
headerName: field.label,
flex: 1,
minWidth: 150,
- renderCell: (params: GridRenderCellParams) => {
- const value = params.value;
-
- // 0. Link to View if it's the Primary Key (if it's displayed)
- const isPk = key === config.primaryKey;
-
- // 1. Custom Formatter
- if (field.formatter) {
- return field.formatter(value);
- }
-
- // 2. Relational Link
- if (field.relation && value) {
- const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
- const displayValue =
- typeof value === "object"
- ? (
- (field?.displayField && (value as Record)[field.displayField]) ||
- (value as any).id ||
- (value as any)._id ||
- (value as any).pk
- )
- : value;
- if (relationId) {
- return (
- {
- e.stopPropagation();
- onNavigateToResource?.(field.relation!, String(relationId));
- }}
- >
- {displayValue}
-
- );
- }
- }
-
- // 3. Nested Object / Array Display
- if (field.type === 'array' && Array.isArray(value)) {
- if (field.displayField) {
- return value
- .map((item) => (typeof item === 'object' ? item[field.displayField!] : item))
- .filter(Boolean)
- .join(', ');
- }
- return `${value.length} items`;
- }
-
- if (field.type === 'object' && value) {
- if (field.displayField && value[field.displayField]) {
- return value[field.displayField];
- }
- return JSON.stringify(value);
- }
-
- // 4. Default renderings
- if (field.type === 'boolean') return value ? 'Yes' : 'No';
- if (field.type === 'datetime' || field.type === 'date') {
- return (value) ? new Date(value).toLocaleString() : '';
- }
-
- if (isPk) {
- return (
- {
- e.stopPropagation();
- const rowId = params.row[config.primaryKey];
- navigate(`/${config.name}/${rowId}`);
- }}
- >
- {value}
-
- );
- }
-
- return value;
- }
+ renderCell: (params: GridRenderCellParams) =>
};
return col;
});
@@ -157,10 +89,37 @@ export default function EnhancedTable({
return cols;
}, [config, onDelete, navigate, onNavigateToResource]);
+ if (isMobile) {
+ return (
+
+
+ {config.pluralLabel}
+
+
+
+ {data.map((row) => (
+
+
+
+ ))}
+
+
+ );
+ }
+
return (
- {config.pluralLabel}
+ {config.pluralLabel}
@@ -185,7 +144,107 @@ export default function EnhancedTable({
},
}}
pageSizeOptions={[10, 25, 50]}
+ sx={{
+ border: 'none',
+ '& .MuiDataGrid-cell:focus': { outline: 'none' },
+ '& .MuiDataGrid-columnHeader:focus': { outline: 'none' },
+ }}
/>
);
}
+
+function MobileCardRow({ row, config, onDelete, onNavigate, navigate }: any) {
+ const [anchorEl, setAnchorEl] = React.useState(null);
+ const open = Boolean(anchorEl);
+ const id = row[config.primaryKey];
+
+ const handleClick = (event: React.MouseEvent) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ return (
+
+
+
+
+ #{id}
+
+
+
+
+
+
+
+
+ {Object.entries(config.fields).slice(0, 5).map(([key, field]: [string, any]) => (
+
+
+ {field.label}
+
+
+
+
+
+ ))}
+
+
+
+
+
+
+ );
+}
+
+function FieldRenderer({ params, field, fieldKey, config, onNavigate, navigate, isMobile }: any) {
+ const value = params.value;
+ const isPk = fieldKey === config.primaryKey;
+
+ if (field.formatter) return field.formatter(value);
+
+ if (field.relation && value) {
+ const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value;
+ const displayValue = typeof value === "object" ?
+ ((field?.displayField && (value as Record)[field.displayField]) || (value as any).id || (value as any)._id || (value as any).pk) : value;
+
+ if (relationId) {
+ return (
+ { e.stopPropagation(); onNavigate?.(field.relation!, String(relationId)); }}>
+ {displayValue}
+
+ );
+ }
+ }
+
+ if (field.type === 'array' && Array.isArray(value)) {
+ if (field.displayField) {
+ return value.map((item) => (typeof item === 'object' ? item[field.displayField!] : item)).filter(Boolean).join(', ');
+ }
+ return `${value.length} items`;
+ }
+
+ if (field.type === 'object' && value) {
+ if (field.displayField && value[field.displayField]) return value[field.displayField];
+ return isMobile ? 'Object' : JSON.stringify(value);
+ }
+
+ if (field.type === 'boolean') return value ? 'Yes' : 'No';
+ if (field.type === 'datetime' || field.type === 'date') return value ? new Date(value).toLocaleString() : '';
+
+ if (isPk && !isMobile) {
+ return (
+ { e.stopPropagation(); navigate(`/${config.name}/${params.row[config.primaryKey]}`); }}>
+ {value}
+
+ );
+ }
+
+ return value;
+}