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} + + + + + + { handleClose(); navigate(`/${config.name}/${id}`); }}>View + { handleClose(); navigate(`/${config.name}/edit/${id}`); }}>Edit + { handleClose(); onDelete(id); }} sx={{ color: 'error.main' }}>Delete + + + + + {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; +}