444 lines
14 KiB
JavaScript
444 lines
14 KiB
JavaScript
|
|
import * as React from 'react';
|
|||
|
|
import PropTypes from 'prop-types';
|
|||
|
|
import Box from '@mui/joy/Box';
|
|||
|
|
import Table from '@mui/joy/Table';
|
|||
|
|
import Typography from '@mui/joy/Typography';
|
|||
|
|
import Sheet from '@mui/joy/Sheet';
|
|||
|
|
import Checkbox from '@mui/joy/Checkbox';
|
|||
|
|
import FormControl from '@mui/joy/FormControl';
|
|||
|
|
import FormLabel from '@mui/joy/FormLabel';
|
|||
|
|
import IconButton from '@mui/joy/IconButton';
|
|||
|
|
import Link from '@mui/joy/Link';
|
|||
|
|
import Tooltip from '@mui/joy/Tooltip';
|
|||
|
|
import Select from '@mui/joy/Select';
|
|||
|
|
import Option from '@mui/joy/Option';
|
|||
|
|
import DeleteIcon from '@mui/icons-material/Delete';
|
|||
|
|
import FilterListIcon from '@mui/icons-material/FilterList';
|
|||
|
|
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
|||
|
|
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
|||
|
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
|||
|
|
import { visuallyHidden } from '@mui/utils';
|
|||
|
|
|
|||
|
|
function createData(name, calories, fat, carbs, protein) {
|
|||
|
|
return {
|
|||
|
|
name,
|
|||
|
|
calories,
|
|||
|
|
fat,
|
|||
|
|
carbs,
|
|||
|
|
protein,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const rows = [
|
|||
|
|
createData('Cupcake', 305, 3.7, 67, 4.3),
|
|||
|
|
createData('Donut', 452, 25.0, 51, 4.9),
|
|||
|
|
createData('Eclair', 262, 16.0, 24, 6.0),
|
|||
|
|
createData('Frozen yoghurt', 159, 6.0, 24, 4.0),
|
|||
|
|
createData('Gingerbread', 356, 16.0, 49, 3.9),
|
|||
|
|
createData('Honeycomb', 408, 3.2, 87, 6.5),
|
|||
|
|
createData('Ice cream sandwich', 237, 9.0, 37, 4.3),
|
|||
|
|
createData('Jelly Bean', 375, 0.0, 94, 0.0),
|
|||
|
|
createData('KitKat', 518, 26.0, 65, 7.0),
|
|||
|
|
createData('Lollipop', 392, 0.2, 98, 0.0),
|
|||
|
|
createData('Marshmallow', 318, 0, 81, 2.0),
|
|||
|
|
createData('Nougat', 360, 19.0, 9, 37.0),
|
|||
|
|
createData('Oreo', 437, 18.0, 63, 4.0),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function labelDisplayedRows({ from, to, count }) {
|
|||
|
|
return `${from}–${to} of ${count !== -1 ? count : `more than ${to}`}`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function descendingComparator(a, b, orderBy) {
|
|||
|
|
if (b[orderBy] < a[orderBy]) {
|
|||
|
|
return -1;
|
|||
|
|
}
|
|||
|
|
if (b[orderBy] > a[orderBy]) {
|
|||
|
|
return 1;
|
|||
|
|
}
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function getComparator(order, orderBy) {
|
|||
|
|
return order === 'desc'
|
|||
|
|
? (a, b) => descendingComparator(a, b, orderBy)
|
|||
|
|
: (a, b) => -descendingComparator(a, b, orderBy);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const headCells = [
|
|||
|
|
{
|
|||
|
|
id: 'name',
|
|||
|
|
numeric: false,
|
|||
|
|
disablePadding: true,
|
|||
|
|
label: 'Dessert (100g serving)',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'calories',
|
|||
|
|
numeric: true,
|
|||
|
|
disablePadding: false,
|
|||
|
|
label: 'Calories',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'fat',
|
|||
|
|
numeric: true,
|
|||
|
|
disablePadding: false,
|
|||
|
|
label: 'Fat (g)',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'carbs',
|
|||
|
|
numeric: true,
|
|||
|
|
disablePadding: false,
|
|||
|
|
label: 'Carbs (g)',
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
id: 'protein',
|
|||
|
|
numeric: true,
|
|||
|
|
disablePadding: false,
|
|||
|
|
label: 'Protein (g)',
|
|||
|
|
},
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
function EnhancedTableHead(props) {
|
|||
|
|
const { onSelectAllClick, order, orderBy, numSelected, rowCount, onRequestSort } =
|
|||
|
|
props;
|
|||
|
|
const createSortHandler = (property) => (event) => {
|
|||
|
|
onRequestSort(event, property);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<thead>
|
|||
|
|
<tr>
|
|||
|
|
<th>
|
|||
|
|
<Checkbox
|
|||
|
|
indeterminate={numSelected > 0 && numSelected < rowCount}
|
|||
|
|
checked={rowCount > 0 && numSelected === rowCount}
|
|||
|
|
onChange={onSelectAllClick}
|
|||
|
|
slotProps={{
|
|||
|
|
input: {
|
|||
|
|
'aria-label': 'select all desserts',
|
|||
|
|
},
|
|||
|
|
}}
|
|||
|
|
sx={{ verticalAlign: 'sub' }}
|
|||
|
|
/>
|
|||
|
|
</th>
|
|||
|
|
{headCells.map((headCell) => {
|
|||
|
|
const active = orderBy === headCell.id;
|
|||
|
|
return (
|
|||
|
|
<th
|
|||
|
|
key={headCell.id}
|
|||
|
|
aria-sort={
|
|||
|
|
active ? { asc: 'ascending', desc: 'descending' }[order] : undefined
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<Link
|
|||
|
|
underline="none"
|
|||
|
|
color="neutral"
|
|||
|
|
textColor={active ? 'primary.plainColor' : undefined}
|
|||
|
|
component="button"
|
|||
|
|
onClick={createSortHandler(headCell.id)}
|
|||
|
|
startDecorator={
|
|||
|
|
headCell.numeric ? (
|
|||
|
|
<ArrowDownwardIcon
|
|||
|
|
sx={[active ? { opacity: 1 } : { opacity: 0 }]}
|
|||
|
|
/>
|
|||
|
|
) : null
|
|||
|
|
}
|
|||
|
|
endDecorator={
|
|||
|
|
!headCell.numeric ? (
|
|||
|
|
<ArrowDownwardIcon
|
|||
|
|
sx={[active ? { opacity: 1 } : { opacity: 0 }]}
|
|||
|
|
/>
|
|||
|
|
) : null
|
|||
|
|
}
|
|||
|
|
sx={{
|
|||
|
|
fontWeight: 'lg',
|
|||
|
|
'& svg': {
|
|||
|
|
transition: '0.2s',
|
|||
|
|
transform:
|
|||
|
|
active && order === 'desc' ? 'rotate(0deg)' : 'rotate(180deg)',
|
|||
|
|
},
|
|||
|
|
'&:hover': { '& svg': { opacity: 1 } },
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
{headCell.label}
|
|||
|
|
{active ? (
|
|||
|
|
<Box component="span" sx={visuallyHidden}>
|
|||
|
|
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
|
|||
|
|
</Box>
|
|||
|
|
) : null}
|
|||
|
|
</Link>
|
|||
|
|
</th>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</tr>
|
|||
|
|
</thead>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EnhancedTableHead.propTypes = {
|
|||
|
|
numSelected: PropTypes.number.isRequired,
|
|||
|
|
onRequestSort: PropTypes.func.isRequired,
|
|||
|
|
onSelectAllClick: PropTypes.func.isRequired,
|
|||
|
|
order: PropTypes.oneOf(['asc', 'desc']).isRequired,
|
|||
|
|
orderBy: PropTypes.string.isRequired,
|
|||
|
|
rowCount: PropTypes.number.isRequired,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
function EnhancedTableToolbar(props) {
|
|||
|
|
const { numSelected } = props;
|
|||
|
|
return (
|
|||
|
|
<Box
|
|||
|
|
sx={[
|
|||
|
|
{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
py: 1,
|
|||
|
|
pl: { sm: 2 },
|
|||
|
|
pr: { xs: 1, sm: 1 },
|
|||
|
|
borderTopLeftRadius: 'var(--unstable_actionRadius)',
|
|||
|
|
borderTopRightRadius: 'var(--unstable_actionRadius)',
|
|||
|
|
},
|
|||
|
|
numSelected > 0 && {
|
|||
|
|
bgcolor: 'background.level1',
|
|||
|
|
},
|
|||
|
|
]}
|
|||
|
|
>
|
|||
|
|
{numSelected > 0 ? (
|
|||
|
|
<Typography sx={{ flex: '1 1 100%' }} component="div">
|
|||
|
|
{numSelected} selected
|
|||
|
|
</Typography>
|
|||
|
|
) : (
|
|||
|
|
<Typography
|
|||
|
|
level="body-lg"
|
|||
|
|
sx={{ flex: '1 1 100%' }}
|
|||
|
|
id="tableTitle"
|
|||
|
|
component="div"
|
|||
|
|
>
|
|||
|
|
Nutrition
|
|||
|
|
</Typography>
|
|||
|
|
)}
|
|||
|
|
{numSelected > 0 ? (
|
|||
|
|
<Tooltip title="Delete">
|
|||
|
|
<IconButton size="sm" color="danger" variant="solid">
|
|||
|
|
<DeleteIcon />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
) : (
|
|||
|
|
<Tooltip title="Filter list">
|
|||
|
|
<IconButton size="sm" variant="outlined" color="neutral">
|
|||
|
|
<FilterListIcon />
|
|||
|
|
</IconButton>
|
|||
|
|
</Tooltip>
|
|||
|
|
)}
|
|||
|
|
</Box>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
EnhancedTableToolbar.propTypes = {
|
|||
|
|
numSelected: PropTypes.number.isRequired,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default function TableSortAndSelection() {
|
|||
|
|
const [order, setOrder] = React.useState('asc');
|
|||
|
|
const [orderBy, setOrderBy] = React.useState('calories');
|
|||
|
|
const [selected, setSelected] = React.useState([]);
|
|||
|
|
const [page, setPage] = React.useState(0);
|
|||
|
|
const [rowsPerPage, setRowsPerPage] = React.useState(5);
|
|||
|
|
const handleRequestSort = (event, property) => {
|
|||
|
|
const isAsc = orderBy === property && order === 'asc';
|
|||
|
|
setOrder(isAsc ? 'desc' : 'asc');
|
|||
|
|
setOrderBy(property);
|
|||
|
|
};
|
|||
|
|
const handleSelectAllClick = (event) => {
|
|||
|
|
if (event.target.checked) {
|
|||
|
|
const newSelected = rows.map((n) => n.name);
|
|||
|
|
setSelected(newSelected);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
setSelected([]);
|
|||
|
|
};
|
|||
|
|
const handleClick = (event, name) => {
|
|||
|
|
const selectedIndex = selected.indexOf(name);
|
|||
|
|
let newSelected = [];
|
|||
|
|
if (selectedIndex === -1) {
|
|||
|
|
newSelected = newSelected.concat(selected, name);
|
|||
|
|
} else if (selectedIndex === 0) {
|
|||
|
|
newSelected = newSelected.concat(selected.slice(1));
|
|||
|
|
} else if (selectedIndex === selected.length - 1) {
|
|||
|
|
newSelected = newSelected.concat(selected.slice(0, -1));
|
|||
|
|
} else if (selectedIndex > 0) {
|
|||
|
|
newSelected = newSelected.concat(
|
|||
|
|
selected.slice(0, selectedIndex),
|
|||
|
|
selected.slice(selectedIndex + 1),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
setSelected(newSelected);
|
|||
|
|
};
|
|||
|
|
const handleChangePage = (newPage) => {
|
|||
|
|
setPage(newPage);
|
|||
|
|
};
|
|||
|
|
const handleChangeRowsPerPage = (event, newValue) => {
|
|||
|
|
setRowsPerPage(parseInt(newValue.toString(), 10));
|
|||
|
|
setPage(0);
|
|||
|
|
};
|
|||
|
|
const getLabelDisplayedRowsTo = () => {
|
|||
|
|
if (rows.length === -1) {
|
|||
|
|
return (page + 1) * rowsPerPage;
|
|||
|
|
}
|
|||
|
|
return rowsPerPage === -1
|
|||
|
|
? rows.length
|
|||
|
|
: Math.min(rows.length, (page + 1) * rowsPerPage);
|
|||
|
|
};
|
|||
|
|
// Avoid a layout jump when reaching the last page with empty rows.
|
|||
|
|
const emptyRows =
|
|||
|
|
page > 0 ? Math.max(0, (1 + page) * rowsPerPage - rows.length) : 0;
|
|||
|
|
return (
|
|||
|
|
<Sheet
|
|||
|
|
variant="outlined"
|
|||
|
|
sx={{ width: '100%', boxShadow: 'sm', borderRadius: 'sm' }}
|
|||
|
|
>
|
|||
|
|
<EnhancedTableToolbar numSelected={selected.length} />
|
|||
|
|
<Table
|
|||
|
|
aria-labelledby="tableTitle"
|
|||
|
|
hoverRow
|
|||
|
|
sx={{
|
|||
|
|
'--TableCell-headBackground': 'transparent',
|
|||
|
|
'--TableCell-selectedBackground': (theme) =>
|
|||
|
|
theme.vars.palette.success.softBg,
|
|||
|
|
'& thead th:nth-child(1)': {
|
|||
|
|
width: '40px',
|
|||
|
|
},
|
|||
|
|
'& thead th:nth-child(2)': {
|
|||
|
|
width: '30%',
|
|||
|
|
},
|
|||
|
|
'& tr > *:nth-child(n+3)': { textAlign: 'right' },
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<EnhancedTableHead
|
|||
|
|
numSelected={selected.length}
|
|||
|
|
order={order}
|
|||
|
|
orderBy={orderBy}
|
|||
|
|
onSelectAllClick={handleSelectAllClick}
|
|||
|
|
onRequestSort={handleRequestSort}
|
|||
|
|
rowCount={rows.length}
|
|||
|
|
/>
|
|||
|
|
<tbody>
|
|||
|
|
{[...rows]
|
|||
|
|
.sort(getComparator(order, orderBy))
|
|||
|
|
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
|
|||
|
|
.map((row, index) => {
|
|||
|
|
const isItemSelected = selected.includes(row.name);
|
|||
|
|
const labelId = `enhanced-table-checkbox-${index}`;
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<tr
|
|||
|
|
onClick={(event) => handleClick(event, row.name)}
|
|||
|
|
role="checkbox"
|
|||
|
|
aria-checked={isItemSelected}
|
|||
|
|
tabIndex={-1}
|
|||
|
|
key={row.name}
|
|||
|
|
// selected={isItemSelected}
|
|||
|
|
style={
|
|||
|
|
isItemSelected
|
|||
|
|
? {
|
|||
|
|
'--TableCell-dataBackground':
|
|||
|
|
'var(--TableCell-selectedBackground)',
|
|||
|
|
'--TableCell-headBackground':
|
|||
|
|
'var(--TableCell-selectedBackground)',
|
|||
|
|
}
|
|||
|
|
: {}
|
|||
|
|
}
|
|||
|
|
>
|
|||
|
|
<th scope="row">
|
|||
|
|
<Checkbox
|
|||
|
|
checked={isItemSelected}
|
|||
|
|
slotProps={{
|
|||
|
|
input: {
|
|||
|
|
'aria-labelledby': labelId,
|
|||
|
|
},
|
|||
|
|
}}
|
|||
|
|
sx={{ verticalAlign: 'top' }}
|
|||
|
|
/>
|
|||
|
|
</th>
|
|||
|
|
<th id={labelId} scope="row">
|
|||
|
|
{row.name}
|
|||
|
|
</th>
|
|||
|
|
<td>{row.calories}</td>
|
|||
|
|
<td>{row.fat}</td>
|
|||
|
|
<td>{row.carbs}</td>
|
|||
|
|
<td>{row.protein}</td>
|
|||
|
|
</tr>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
{emptyRows > 0 && (
|
|||
|
|
<tr
|
|||
|
|
style={{
|
|||
|
|
height: `calc(${emptyRows} * 40px)`,
|
|||
|
|
'--TableRow-hoverBackground': 'transparent',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<td colSpan={6} aria-hidden />
|
|||
|
|
</tr>
|
|||
|
|
)}
|
|||
|
|
</tbody>
|
|||
|
|
<tfoot>
|
|||
|
|
<tr>
|
|||
|
|
<td colSpan={6}>
|
|||
|
|
<Box
|
|||
|
|
sx={{
|
|||
|
|
display: 'flex',
|
|||
|
|
alignItems: 'center',
|
|||
|
|
gap: 2,
|
|||
|
|
justifyContent: 'flex-end',
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<FormControl orientation="horizontal" size="sm">
|
|||
|
|
<FormLabel>Rows per page:</FormLabel>
|
|||
|
|
<Select onChange={handleChangeRowsPerPage} value={rowsPerPage}>
|
|||
|
|
<Option value={5}>5</Option>
|
|||
|
|
<Option value={10}>10</Option>
|
|||
|
|
<Option value={25}>25</Option>
|
|||
|
|
</Select>
|
|||
|
|
</FormControl>
|
|||
|
|
<Typography sx={{ textAlign: 'center', minWidth: 80 }}>
|
|||
|
|
{labelDisplayedRows({
|
|||
|
|
from: rows.length === 0 ? 0 : page * rowsPerPage + 1,
|
|||
|
|
to: getLabelDisplayedRowsTo(),
|
|||
|
|
count: rows.length === -1 ? -1 : rows.length,
|
|||
|
|
})}
|
|||
|
|
</Typography>
|
|||
|
|
<Box sx={{ display: 'flex', gap: 1 }}>
|
|||
|
|
<IconButton
|
|||
|
|
size="sm"
|
|||
|
|
color="neutral"
|
|||
|
|
variant="outlined"
|
|||
|
|
disabled={page === 0}
|
|||
|
|
onClick={() => handleChangePage(page - 1)}
|
|||
|
|
sx={{ bgcolor: 'background.surface' }}
|
|||
|
|
>
|
|||
|
|
<KeyboardArrowLeftIcon />
|
|||
|
|
</IconButton>
|
|||
|
|
<IconButton
|
|||
|
|
size="sm"
|
|||
|
|
color="neutral"
|
|||
|
|
variant="outlined"
|
|||
|
|
disabled={
|
|||
|
|
rows.length !== -1
|
|||
|
|
? page >= Math.ceil(rows.length / rowsPerPage) - 1
|
|||
|
|
: false
|
|||
|
|
}
|
|||
|
|
onClick={() => handleChangePage(page + 1)}
|
|||
|
|
sx={{ bgcolor: 'background.surface' }}
|
|||
|
|
>
|
|||
|
|
<KeyboardArrowRightIcon />
|
|||
|
|
</IconButton>
|
|||
|
|
</Box>
|
|||
|
|
</Box>
|
|||
|
|
</td>
|
|||
|
|
</tr>
|
|||
|
|
</tfoot>
|
|||
|
|
</Table>
|
|||
|
|
</Sheet>
|
|||
|
|
);
|
|||
|
|
}
|