This commit is contained in:
2025-07-21 14:55:22 +02:00
parent 37212e99d5
commit 3394363b37
240 changed files with 18861 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import type { TableRowProps } from '@mui/material/TableRow';
import TableRow from '@mui/material/TableRow';
import TableCell from '@mui/material/TableCell';
// ----------------------------------------------------------------------
type TableEmptyRowsProps = TableRowProps & {
emptyRows: number;
height?: number;
};
export function TableEmptyRows({ emptyRows, height, sx, ...other }: TableEmptyRowsProps) {
if (!emptyRows) {
return null;
}
return (
<TableRow
sx={[height && { height: height * emptyRows }, ...(Array.isArray(sx) ? sx : [sx])]}
{...other}
>
<TableCell colSpan={9} />
</TableRow>
);
}

View File

@@ -0,0 +1,32 @@
import type { TableRowProps } from '@mui/material/TableRow';
import Box from '@mui/material/Box';
import TableRow from '@mui/material/TableRow';
import TableCell from '@mui/material/TableCell';
import Typography from '@mui/material/Typography';
// ----------------------------------------------------------------------
type TableNoDataProps = TableRowProps & {
searchQuery: string;
};
export function TableNoData({ searchQuery, ...other }: TableNoDataProps) {
return (
<TableRow {...other}>
<TableCell align="center" colSpan={7}>
<Box sx={{ py: 15, textAlign: 'center' }}>
<Typography variant="h6" sx={{ mb: 1 }}>
Not found
</Typography>
<Typography variant="body2">
No results found for &nbsp;
<strong>&quot;{searchQuery}&quot;</strong>.
<br /> Try checking for typos or using complete words.
</Typography>
</Box>
</TableCell>
</TableRow>
);
}

View File

@@ -0,0 +1,69 @@
import Box from '@mui/material/Box';
import TableRow from '@mui/material/TableRow';
import Checkbox from '@mui/material/Checkbox';
import TableHead from '@mui/material/TableHead';
import TableCell from '@mui/material/TableCell';
import TableSortLabel from '@mui/material/TableSortLabel';
import { visuallyHidden } from './utils';
// ----------------------------------------------------------------------
type UserTableHeadProps = {
orderBy: string;
rowCount: number;
numSelected: number;
order: 'asc' | 'desc';
onSort: (id: string) => void;
headLabel: Record<string, any>[];
onSelectAllRows: (checked: boolean) => void;
};
export function UserTableHead({
order,
onSort,
orderBy,
rowCount,
headLabel,
numSelected,
onSelectAllRows,
}: UserTableHeadProps) {
return (
<TableHead>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onSelectAllRows(event.target.checked)
}
/>
</TableCell>
{headLabel.map((headCell) => (
<TableCell
key={headCell.id}
align={headCell.align || 'left'}
sortDirection={orderBy === headCell.id ? order : false}
sx={{ width: headCell.width, minWidth: headCell.minWidth }}
>
<TableSortLabel
hideSortIcon
active={orderBy === headCell.id}
direction={orderBy === headCell.id ? order : 'asc'}
onClick={() => onSort(headCell.id)}
>
{headCell.label}
{orderBy === headCell.id ? (
<Box sx={{ ...visuallyHidden }}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
);
}

View File

@@ -0,0 +1,124 @@
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Avatar from '@mui/material/Avatar';
import Popover from '@mui/material/Popover';
import TableRow from '@mui/material/TableRow';
import Checkbox from '@mui/material/Checkbox';
import MenuList from '@mui/material/MenuList';
import TableCell from '@mui/material/TableCell';
import IconButton from '@mui/material/IconButton';
import MenuItem, { menuItemClasses } from '@mui/material/MenuItem';
import { Label } from 'src/components/label';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
export type UserProps = {
id: string;
name: string;
role: string;
status: string;
company: string;
avatarUrl: string;
isVerified: boolean;
};
type UserTableRowProps = {
row: UserProps;
selected: boolean;
onSelectRow: () => void;
};
export function UserTableRow({ row, selected, onSelectRow }: UserTableRowProps) {
const [openPopover, setOpenPopover] = useState<HTMLButtonElement | null>(null);
const handleOpenPopover = useCallback((event: React.MouseEvent<HTMLButtonElement>) => {
setOpenPopover(event.currentTarget);
}, []);
const handleClosePopover = useCallback(() => {
setOpenPopover(null);
}, []);
return (
<>
<TableRow hover tabIndex={-1} role="checkbox" selected={selected}>
<TableCell padding="checkbox">
<Checkbox disableRipple checked={selected} onChange={onSelectRow} />
</TableCell>
<TableCell component="th" scope="row">
<Box
sx={{
gap: 2,
display: 'flex',
alignItems: 'center',
}}
>
<Avatar alt={row.name} src={row.avatarUrl} />
{row.name}
</Box>
</TableCell>
<TableCell>{row.company}</TableCell>
<TableCell>{row.role}</TableCell>
<TableCell align="center">
{row.isVerified ? (
<Iconify width={22} icon="solar:check-circle-bold" sx={{ color: 'success.main' }} />
) : (
'-'
)}
</TableCell>
<TableCell>
<Label color={(row.status === 'banned' && 'error') || 'success'}>{row.status}</Label>
</TableCell>
<TableCell align="right">
<IconButton onClick={handleOpenPopover}>
<Iconify icon="eva:more-vertical-fill" />
</IconButton>
</TableCell>
</TableRow>
<Popover
open={!!openPopover}
anchorEl={openPopover}
onClose={handleClosePopover}
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<MenuList
disablePadding
sx={{
p: 0.5,
gap: 0.5,
width: 140,
display: 'flex',
flexDirection: 'column',
[`& .${menuItemClasses.root}`]: {
px: 1,
gap: 2,
borderRadius: 0.75,
[`&.${menuItemClasses.selected}`]: { bgcolor: 'action.selected' },
},
}}
>
<MenuItem onClick={handleClosePopover}>
<Iconify icon="solar:pen-bold" />
Edit
</MenuItem>
<MenuItem onClick={handleClosePopover} sx={{ color: 'error.main' }}>
<Iconify icon="solar:trash-bin-trash-bold" />
Delete
</MenuItem>
</MenuList>
</Popover>
</>
);
}

View File

@@ -0,0 +1,66 @@
import Tooltip from '@mui/material/Tooltip';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import OutlinedInput from '@mui/material/OutlinedInput';
import InputAdornment from '@mui/material/InputAdornment';
import { Iconify } from 'src/components/iconify';
// ----------------------------------------------------------------------
type UserTableToolbarProps = {
numSelected: number;
filterName: string;
onFilterName: (event: React.ChangeEvent<HTMLInputElement>) => void;
};
export function UserTableToolbar({ numSelected, filterName, onFilterName }: UserTableToolbarProps) {
return (
<Toolbar
sx={{
height: 96,
display: 'flex',
justifyContent: 'space-between',
p: (theme) => theme.spacing(0, 1, 0, 3),
...(numSelected > 0 && {
color: 'primary.main',
bgcolor: 'primary.lighter',
}),
}}
>
{numSelected > 0 ? (
<Typography component="div" variant="subtitle1">
{numSelected} selected
</Typography>
) : (
<OutlinedInput
fullWidth
value={filterName}
onChange={onFilterName}
placeholder="Search user..."
startAdornment={
<InputAdornment position="start">
<Iconify width={20} icon="eva:search-fill" sx={{ color: 'text.disabled' }} />
</InputAdornment>
}
sx={{ maxWidth: 320 }}
/>
)}
{numSelected > 0 ? (
<Tooltip title="Delete">
<IconButton>
<Iconify icon="solar:trash-bin-trash-bold" />
</IconButton>
</Tooltip>
) : (
<Tooltip title="Filter list">
<IconButton>
<Iconify icon="ic:round-filter-list" />
</IconButton>
</Tooltip>
)}
</Toolbar>
);
}

View File

@@ -0,0 +1,79 @@
import type { UserProps } from './user-table-row';
// ----------------------------------------------------------------------
export const visuallyHidden = {
border: 0,
margin: -1,
padding: 0,
width: '1px',
height: '1px',
overflow: 'hidden',
position: 'absolute',
whiteSpace: 'nowrap',
clip: 'rect(0 0 0 0)',
} as const;
// ----------------------------------------------------------------------
export function emptyRows(page: number, rowsPerPage: number, arrayLength: number) {
return page ? Math.max(0, (1 + page) * rowsPerPage - arrayLength) : 0;
}
// ----------------------------------------------------------------------
function descendingComparator<T>(a: T, b: T, orderBy: keyof T) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
// ----------------------------------------------------------------------
export function getComparator<Key extends keyof any>(
order: 'asc' | 'desc',
orderBy: Key
): (
a: {
[key in Key]: number | string;
},
b: {
[key in Key]: number | string;
}
) => number {
return order === 'desc'
? (a, b) => descendingComparator(a, b, orderBy)
: (a, b) => -descendingComparator(a, b, orderBy);
}
// ----------------------------------------------------------------------
type ApplyFilterProps = {
inputData: UserProps[];
filterName: string;
comparator: (a: any, b: any) => number;
};
export function applyFilter({ inputData, comparator, filterName }: ApplyFilterProps) {
const stabilizedThis = inputData.map((el, index) => [el, index] as const);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) return order;
return a[1] - b[1];
});
inputData = stabilizedThis.map((el) => el[0]);
if (filterName) {
inputData = inputData.filter(
(user) => user.name.toLowerCase().indexOf(filterName.toLowerCase()) !== -1
);
}
return inputData;
}

View File

@@ -0,0 +1 @@
export * from './user-view';

View File

@@ -0,0 +1,203 @@
import { useState, useCallback } from 'react';
import Box from '@mui/material/Box';
import Card from '@mui/material/Card';
import Table from '@mui/material/Table';
import Button from '@mui/material/Button';
import TableBody from '@mui/material/TableBody';
import Typography from '@mui/material/Typography';
import TableContainer from '@mui/material/TableContainer';
import TablePagination from '@mui/material/TablePagination';
import { _users } from 'src/_mock';
import { DashboardContent } from 'src/layouts/dashboard';
import { Iconify } from 'src/components/iconify';
import { Scrollbar } from 'src/components/scrollbar';
import { TableNoData } from '../table-no-data';
import { UserTableRow } from '../user-table-row';
import { UserTableHead } from '../user-table-head';
import { TableEmptyRows } from '../table-empty-rows';
import { UserTableToolbar } from '../user-table-toolbar';
import { emptyRows, applyFilter, getComparator } from '../utils';
import type { UserProps } from '../user-table-row';
// ----------------------------------------------------------------------
export function UserView() {
const table = useTable();
const [filterName, setFilterName] = useState('');
const dataFiltered: UserProps[] = applyFilter({
inputData: _users,
comparator: getComparator(table.order, table.orderBy),
filterName,
});
const notFound = !dataFiltered.length && !!filterName;
return (
<DashboardContent>
<Box
sx={{
mb: 5,
display: 'flex',
alignItems: 'center',
}}
>
<Typography variant="h4" sx={{ flexGrow: 1 }}>
Users
</Typography>
<Button
variant="contained"
color="inherit"
startIcon={<Iconify icon="mingcute:add-line" />}
>
New user
</Button>
</Box>
<Card>
<UserTableToolbar
numSelected={table.selected.length}
filterName={filterName}
onFilterName={(event: React.ChangeEvent<HTMLInputElement>) => {
setFilterName(event.target.value);
table.onResetPage();
}}
/>
<Scrollbar>
<TableContainer sx={{ overflow: 'unset' }}>
<Table sx={{ minWidth: 800 }}>
<UserTableHead
order={table.order}
orderBy={table.orderBy}
rowCount={_users.length}
numSelected={table.selected.length}
onSort={table.onSort}
onSelectAllRows={(checked) =>
table.onSelectAllRows(
checked,
_users.map((user) => user.id)
)
}
headLabel={[
{ id: 'name', label: 'Name' },
{ id: 'company', label: 'Company' },
{ id: 'role', label: 'Role' },
{ id: 'isVerified', label: 'Verified', align: 'center' },
{ id: 'status', label: 'Status' },
{ id: '' },
]}
/>
<TableBody>
{dataFiltered
.slice(
table.page * table.rowsPerPage,
table.page * table.rowsPerPage + table.rowsPerPage
)
.map((row) => (
<UserTableRow
key={row.id}
row={row}
selected={table.selected.includes(row.id)}
onSelectRow={() => table.onSelectRow(row.id)}
/>
))}
<TableEmptyRows
height={68}
emptyRows={emptyRows(table.page, table.rowsPerPage, _users.length)}
/>
{notFound && <TableNoData searchQuery={filterName} />}
</TableBody>
</Table>
</TableContainer>
</Scrollbar>
<TablePagination
component="div"
page={table.page}
count={_users.length}
rowsPerPage={table.rowsPerPage}
onPageChange={table.onChangePage}
rowsPerPageOptions={[5, 10, 25]}
onRowsPerPageChange={table.onChangeRowsPerPage}
/>
</Card>
</DashboardContent>
);
}
// ----------------------------------------------------------------------
export function useTable() {
const [page, setPage] = useState(0);
const [orderBy, setOrderBy] = useState('name');
const [rowsPerPage, setRowsPerPage] = useState(5);
const [selected, setSelected] = useState<string[]>([]);
const [order, setOrder] = useState<'asc' | 'desc'>('asc');
const onSort = useCallback(
(id: string) => {
const isAsc = orderBy === id && order === 'asc';
setOrder(isAsc ? 'desc' : 'asc');
setOrderBy(id);
},
[order, orderBy]
);
const onSelectAllRows = useCallback((checked: boolean, newSelecteds: string[]) => {
if (checked) {
setSelected(newSelecteds);
return;
}
setSelected([]);
}, []);
const onSelectRow = useCallback(
(inputValue: string) => {
const newSelected = selected.includes(inputValue)
? selected.filter((value) => value !== inputValue)
: [...selected, inputValue];
setSelected(newSelected);
},
[selected]
);
const onResetPage = useCallback(() => {
setPage(0);
}, []);
const onChangePage = useCallback((event: unknown, newPage: number) => {
setPage(newPage);
}, []);
const onChangeRowsPerPage = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(parseInt(event.target.value, 10));
onResetPage();
},
[onResetPage]
);
return {
page,
order,
onSort,
orderBy,
selected,
rowsPerPage,
onSelectRow,
onResetPage,
onChangePage,
onSelectAllRows,
onChangeRowsPerPage,
};
}