| @@ -0,0 +1,15 @@ | |||
| const faker = require('faker'); | |||
| module.exports = () => { | |||
| const items = []; | |||
| for (let id = 1; id <= 500; id++) { | |||
| items.push({ | |||
| id: id, | |||
| name: `${faker.commerce.productAdjective()} ${faker.commerce.productMaterial()} ${faker.commerce.product()}`, | |||
| color: faker.commerce.color(), | |||
| price: `$${faker.commerce.price()}`, | |||
| company: faker.company.companyName(), | |||
| }); | |||
| } | |||
| return { items }; | |||
| }; | |||
| @@ -7,6 +7,7 @@ | |||
| "@emotion/styled": "^11.3.0", | |||
| "@mui/icons-material": "^5.0.5", | |||
| "@mui/material": "^5.0.6", | |||
| "@mui/x-data-grid": "^5.0.1", | |||
| "@reduxjs/toolkit": "^1.5.1", | |||
| "@testing-library/jest-dom": "^5.13.0", | |||
| "@testing-library/react": "^11.2.7", | |||
| @@ -15,8 +16,10 @@ | |||
| "date-fns": "^2.22.1", | |||
| "eslint-plugin-prettier": "^3.4.0", | |||
| "eslint-plugin-security": "^1.4.0", | |||
| "faker": "^5.5.3", | |||
| "formik": "^2.2.9", | |||
| "i18next": "^20.3.1", | |||
| "json-server": "^0.17.0", | |||
| "jsonwebtoken": "^8.5.1", | |||
| "lodash": "^4.17.21", | |||
| "lodash.isempty": "^4.4.0", | |||
| @@ -39,7 +42,8 @@ | |||
| "start": "react-scripts start", | |||
| "build": "react-scripts build", | |||
| "test": "react-scripts test", | |||
| "eject": "react-scripts eject" | |||
| "eject": "react-scripts eject", | |||
| "json-serve": "json-server ./db/db.js --port=4000" | |||
| }, | |||
| "eslintConfig": { | |||
| "extends": [ | |||
| @@ -3,10 +3,10 @@ import PropTypes from 'prop-types'; | |||
| import { Backdrop, CircularProgress } from '@mui/material'; | |||
| import { alpha } from '@mui/system'; | |||
| const CustomBackdrop = ({ position = 'fixed', isLoading }) => ( | |||
| const BackdropComponent = ({ position = 'fixed', isLoading }) => ( | |||
| <Backdrop | |||
| sx={{ | |||
| // 'fixed' takes whole page, 'fixed' takes whole space of the parent element which needs to have 'relative' position | |||
| // 'fixed' takes whole page, 'absolute' takes whole space of the parent element which needs to have 'relative' position | |||
| position, | |||
| backgroundColor: ({ palette }) => | |||
| alpha(palette.background.default, palette.action.disabledOpacity), | |||
| @@ -18,9 +18,9 @@ const CustomBackdrop = ({ position = 'fixed', isLoading }) => ( | |||
| </Backdrop> | |||
| ); | |||
| CustomBackdrop.propTypes = { | |||
| BackdropComponent.propTypes = { | |||
| position: PropTypes.oneOf(['fixed', 'absolute']), | |||
| isLoading: PropTypes.bool.isRequired, | |||
| }; | |||
| export default CustomBackdrop; | |||
| export default BackdropComponent; | |||
| @@ -2,14 +2,14 @@ import React from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { Typography } from '@mui/material'; | |||
| const CustomErrorMessage = ({ error }) => ( | |||
| const ErrorMessageComponent = ({ error }) => ( | |||
| <Typography variant="body1" color="error" my={2}> | |||
| {error} | |||
| </Typography> | |||
| ); | |||
| CustomErrorMessage.propTypes = { | |||
| ErrorMessageComponent.propTypes = { | |||
| error: PropTypes.string.isRequired, | |||
| }; | |||
| export default CustomErrorMessage; | |||
| export default ErrorMessageComponent; | |||
| @@ -0,0 +1,29 @@ | |||
| import React from 'react'; | |||
| import { Paper, Typography } from '@mui/material'; | |||
| import { DataGrid } from '@mui/x-data-grid'; | |||
| // Use these values from REDUX? | |||
| const rows = [ | |||
| { id: 1, col1: 'Example', col2: 'Row', col3: '1' }, | |||
| { id: 2, col1: 'Row', col2: 'Example', col3: '2' }, | |||
| { id: 3, col1: '3', col2: 'Row', col3: 'Example' }, | |||
| ]; | |||
| const columns = [ | |||
| { field: 'col1', headerName: 'Column 1', flex: 1 }, | |||
| { field: 'col2', headerName: 'Column 2', flex: 1 }, | |||
| { field: 'col3', headerName: 'Column 2', flex: 1 }, | |||
| ]; | |||
| const DataGridExample = () => { | |||
| return ( | |||
| <Paper sx={{ p: 2 }} elevation={5}> | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| DataGrid Example | |||
| </Typography> | |||
| <DataGrid autoHeight rows={rows} columns={columns} /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default DataGridExample; | |||
| @@ -0,0 +1,64 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Divider, Paper, Typography } from '@mui/material'; | |||
| import DialogComponent from '../DialogComponent'; | |||
| import DrawerComponent from '../DrawerComponent'; | |||
| import PopoverComponent from '../PopoverComponent'; | |||
| const Modals = () => { | |||
| const [dialogOpen, setDialogOpen] = useState(false); | |||
| const [drawerOpen, setDrawerOpen] = useState(false); | |||
| const [popoverOpen, setPopoverOpen] = useState(false); | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| p: 2, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography variant="h4" gutterBottom align="center"> | |||
| Modals Example | |||
| </Typography> | |||
| <Divider /> | |||
| <Button onClick={() => setDialogOpen(true)}>Open Dialog</Button> | |||
| <Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button> | |||
| <Button | |||
| onClick={(e) => { | |||
| setPopoverOpen(true); | |||
| setAnchorEl(e.currentTarget); | |||
| }} | |||
| > | |||
| Open Popover | |||
| </Button> | |||
| <DialogComponent | |||
| title="Dialog Title" | |||
| content={<Typography>Dialog Content</Typography>} | |||
| open={dialogOpen} | |||
| onClose={() => setDialogOpen(false)} | |||
| maxWidth="md" | |||
| fullWidth | |||
| responsive | |||
| /> | |||
| <DrawerComponent | |||
| anchor="left" | |||
| content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>} | |||
| open={drawerOpen} | |||
| toggleOpen={() => setDrawerOpen(!drawerOpen)} | |||
| /> | |||
| <PopoverComponent | |||
| anchorEl={anchorEl} | |||
| open={popoverOpen} | |||
| onClose={() => { | |||
| setPopoverOpen(false); | |||
| setAnchorEl(null); | |||
| }} | |||
| content={<Typography sx={{ p: 2 }}>Popover Content</Typography>} | |||
| /> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default Modals; | |||
| @@ -0,0 +1,183 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import { useDispatch, useSelector, batch } from 'react-redux'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { | |||
| itemsSelector, | |||
| pageSelector, | |||
| itemsPerPageSelector, | |||
| countSelector, | |||
| sortSelector, | |||
| } from '../../../store/selectors/randomDataSelectors'; | |||
| import { | |||
| loadData, | |||
| updatePage, | |||
| updateItemsPerPage, | |||
| updateFilter, | |||
| updateSort, | |||
| } from '../../../store/actions/randomData/randomDataActions'; | |||
| const PagingSortingFilteringExample = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const dispatch = useDispatch(); | |||
| // const { t } = useTranslation(); | |||
| const items = useSelector(itemsSelector); | |||
| const currentPage = useSelector(pageSelector); | |||
| const itemsPerPage = useSelector(itemsPerPageSelector); | |||
| const totalCount = useSelector(countSelector); | |||
| const sort = useSelector(sortSelector) || 'name-asc'; | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| dispatch(loadData(30)); | |||
| dispatch(updateSort(sort)); | |||
| }, []); | |||
| useEffect(() => { | |||
| batch(() => { | |||
| dispatch(updateFilter(filterText)); | |||
| currentPage > 0 && dispatch(updatePage(0)); | |||
| }); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| dispatch(updateSort(sort)); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| dispatch(updatePage(newPage)); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| batch(() => { | |||
| dispatch(updateItemsPerPage(itemsPerPage)); | |||
| dispatch(updatePage(0)); | |||
| }); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| }} | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Client Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| {/* TODO Separate into SelectComponent */} | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| <Grid container> | |||
| {items && | |||
| items.length > 0 && | |||
| items | |||
| .slice( | |||
| currentPage * itemsPerPage, | |||
| currentPage * itemsPerPage + itemsPerPage | |||
| ) | |||
| .map((product, index) => ( | |||
| // ! DON'T USE index for key, this is for example only | |||
| <Grid item sx={{ p: 2 }} xs={12} sm={6} md={4} lg={3} key={index}> | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {product.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Designer: </Typography> | |||
| <Typography display="inline"> {product.designer}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Type: </Typography> | |||
| <Typography display="inline"> {product.type}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> ${product.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExample; | |||
| @@ -0,0 +1,159 @@ | |||
| import React, { useEffect, useState } from 'react'; | |||
| import { | |||
| Paper, | |||
| Box, | |||
| Grid, | |||
| Typography, | |||
| Divider, | |||
| TablePagination, | |||
| TextField, | |||
| FormControl, | |||
| InputLabel, | |||
| Select, | |||
| MenuItem, | |||
| } from '@mui/material'; | |||
| // import { useTranslation } from 'react-i18next'; | |||
| import Backdrop from '../BackdropComponent'; | |||
| import useDebounce from '../../../hooks/useDebounceHook'; | |||
| import { useRandomData } from '../../../context/RandomDataContext'; | |||
| const PagingSortingFilteringExampleServerSide = () => { | |||
| const [filterText, setFilterText] = useState(''); | |||
| const { state, data } = useRandomData(); | |||
| const { items, loading, totalCount, currentPage, itemsPerPage, sort } = data; | |||
| const { setPage, setItemsPerPage, setSort, setFilter } = state; | |||
| // const { t } = useTranslation(); | |||
| // Use debounce to prevent too many rerenders | |||
| const debouncedFilterText = useDebounce(filterText, 500); | |||
| useEffect(() => { | |||
| setFilter(filterText); | |||
| }, [debouncedFilterText]); | |||
| const handleFilterTextChange = (event) => { | |||
| const filterText = event.target.value; | |||
| setFilterText(filterText); | |||
| }; | |||
| const handleSortChange = (event) => { | |||
| const sort = event.target.value; | |||
| setSort(sort); | |||
| }; | |||
| const handlePageChange = (event, newPage) => { | |||
| setPage(newPage); | |||
| }; | |||
| const handleItemsPerPageChange = (event) => { | |||
| const itemsPerPage = parseInt(event.target.value); | |||
| setItemsPerPage(itemsPerPage); | |||
| setPage(0); | |||
| }; | |||
| return ( | |||
| <Paper | |||
| sx={{ | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| justifyContent: 'start', | |||
| py: 2, | |||
| minHeight: 500, | |||
| position: 'relative', | |||
| }} | |||
| elevation={5} | |||
| > | |||
| {loading && <Backdrop isLoading position="absolute" />} | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example Server Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| flexWrap: 'wrap', | |||
| mx: 2, | |||
| }} | |||
| > | |||
| <Box | |||
| sx={{ | |||
| display: 'flex', | |||
| justifyContent: 'space-between', | |||
| width: '100%', | |||
| }} | |||
| > | |||
| <FormControl sx={{ flexGrow: 1 }}> | |||
| <InputLabel id="sort-label">Sort</InputLabel> | |||
| <Select | |||
| label="Sort" | |||
| labelId="sort-label" | |||
| id="sort-select-helper" | |||
| value={sort || ''} | |||
| onChange={handleSortChange} | |||
| > | |||
| <MenuItem value="">None</MenuItem> | |||
| <MenuItem value="name-asc">Name - A-Z</MenuItem> | |||
| <MenuItem value="name-desc">Name - Z-A</MenuItem> | |||
| <MenuItem value="price-asc">Price - Lowest to Highest</MenuItem> | |||
| <MenuItem value="price-desc">Price - Highest to Lowest</MenuItem> | |||
| </Select> | |||
| </FormControl> | |||
| <TextField | |||
| sx={{ flexGrow: 1 }} | |||
| variant="outlined" | |||
| label="Filter" | |||
| placeholder="Filter" | |||
| value={filterText} | |||
| onChange={handleFilterTextChange} | |||
| /> | |||
| </Box> | |||
| <Grid container sx={{ position: 'relative' }}> | |||
| {items && | |||
| items.length > 0 && | |||
| items.map((item) => ( | |||
| <Grid | |||
| item | |||
| sx={{ p: 2 }} | |||
| xs={12} | |||
| sm={6} | |||
| md={4} | |||
| lg={3} | |||
| key={item.id} | |||
| > | |||
| {/* TODO separate into component */} | |||
| <Paper sx={{ p: 3, height: '100%' }} elevation={3}> | |||
| <Typography sx={{ fontWeight: 600 }}>Name: </Typography> | |||
| <Typography display="inline"> {item.name}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Company: </Typography> | |||
| <Typography display="inline"> {item.company}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Color: </Typography> | |||
| <Typography display="inline"> {item.color}</Typography> | |||
| <Divider /> | |||
| <Typography sx={{ fontWeight: 600 }}>Price: </Typography> | |||
| <Typography display="inline"> {item.price}</Typography> | |||
| </Paper> | |||
| </Grid> | |||
| ))} | |||
| </Grid> | |||
| <Box sx={{ width: '100%' }}> | |||
| <TablePagination | |||
| component="div" | |||
| count={totalCount} | |||
| page={currentPage} | |||
| onPageChange={handlePageChange} | |||
| rowsPerPage={itemsPerPage} | |||
| onRowsPerPageChange={handleItemsPerPageChange} | |||
| rowsPerPageOptions={[12, 24, 48, 96]} | |||
| labelRowsPerPage="Items per page" | |||
| showFirstButton | |||
| showLastButton | |||
| /> | |||
| </Box> | |||
| </Box> | |||
| </Paper> | |||
| ); | |||
| }; | |||
| export default PagingSortingFilteringExampleServerSide; | |||
| @@ -1,7 +1,7 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Button, Menu, MenuItem } from '@mui/material'; | |||
| const MenuList = () => { | |||
| const MenuListComponent = () => { | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| const open = Boolean(anchorEl); | |||
| const handleClick = (event) => { | |||
| @@ -23,4 +23,4 @@ const MenuList = () => { | |||
| ); | |||
| }; | |||
| export default MenuList; | |||
| export default MenuListComponent; | |||
| @@ -1,4 +1,4 @@ | |||
| import React, { useState, useMemo } from 'react'; | |||
| import React, { useState, useMemo, useContext } from 'react'; | |||
| import { | |||
| AppBar, | |||
| Badge, | |||
| @@ -16,35 +16,52 @@ import { | |||
| import { useTheme } from '@mui/system'; | |||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||
| import MenuList from './MenuList'; | |||
| import Brightness4Icon from '@mui/icons-material/Brightness4'; | |||
| import Brightness7Icon from '@mui/icons-material/Brightness7'; | |||
| import MenuList from './MenuListComponent'; | |||
| import Drawer from './DrawerComponent'; | |||
| import { ColorModeContext } from '../../context/ColorModeContext'; | |||
| const Navbar = () => { | |||
| const NavbarComponent = () => { | |||
| const [openDrawer, setOpenDrawer] = useState(false); | |||
| const theme = useTheme(); | |||
| const matches = useMediaQuery(theme.breakpoints.down('sm')); | |||
| const toggleColorMode = useContext(ColorModeContext); | |||
| const handleToggleDrawer = () => { | |||
| setOpenDrawer(!openDrawer); | |||
| }; | |||
| const drawerContent = useMemo(() => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| </List> | |||
| ), [handleToggleDrawer]); | |||
| const drawerContent = useMemo( | |||
| () => ( | |||
| <List> | |||
| <ListItemButton divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 1</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItemButton> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemIcon> | |||
| <ListItemText>Link 2</ListItemText> | |||
| </ListItemIcon> | |||
| </ListItem> | |||
| <ListItem divider onClick={handleToggleDrawer}> | |||
| <ListItemText>Link 3</ListItemText> | |||
| </ListItem> | |||
| <ListItem divider> | |||
| <IconButton onClick={toggleColorMode}> | |||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </ListItem> | |||
| </List> | |||
| ), | |||
| [handleToggleDrawer] | |||
| ); | |||
| return ( | |||
| <AppBar | |||
| @@ -118,11 +135,20 @@ const Navbar = () => { | |||
| </IconButton> | |||
| </Box> | |||
| ) : ( | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <Box> | |||
| <IconButton> | |||
| <Badge badgeContent={3} color="primary"> | |||
| <ShoppingBasketIcon color="action" /> | |||
| </Badge> | |||
| </IconButton> | |||
| <IconButton sx={{ ml: 1 }} onClick={toggleColorMode}> | |||
| {theme.palette.mode === 'dark' ? ( | |||
| <Brightness7Icon /> | |||
| ) : ( | |||
| <Brightness4Icon /> | |||
| )} | |||
| </IconButton> | |||
| </Box> | |||
| )} | |||
| </Box> | |||
| </Box> | |||
| @@ -131,4 +157,4 @@ const Navbar = () => { | |||
| ); | |||
| }; | |||
| export default Navbar; | |||
| export default NavbarComponent; | |||
| @@ -0,0 +1,21 @@ | |||
| import React, { createContext } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import { ThemeProvider } from '@mui/material/styles'; | |||
| import useToggleColorMode from '../hooks/useToggleColorMode'; | |||
| export const ColorModeContext = createContext(); | |||
| const ColorModeProvider = ({ children }) => { | |||
| const [toggleColorMode, theme] = useToggleColorMode(); | |||
| return ( | |||
| <ColorModeContext.Provider value={toggleColorMode}> | |||
| <ThemeProvider theme={theme}>{children}</ThemeProvider> | |||
| </ColorModeContext.Provider> | |||
| ); | |||
| }; | |||
| ColorModeProvider.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default ColorModeProvider; | |||
| @@ -0,0 +1,63 @@ | |||
| import React, { createContext, useContext, useState } from 'react'; | |||
| import PropTypes from 'prop-types'; | |||
| import usePagingHook from '../hooks/usePagingHook'; | |||
| import { getRequest } from '../request/jsonServerRequest'; | |||
| const apiCall = (page, itemsPerPage, sort, sortDirection, filter) => | |||
| getRequest('/items', { | |||
| _page: page, | |||
| _limit: itemsPerPage, | |||
| // Conditionally add to params object if keys exist | |||
| ...(sort && { _sort: sort }), | |||
| ...(sortDirection && { _order: sortDirection }), | |||
| ...(filter && { q: filter }), | |||
| }); | |||
| const Context = createContext(); | |||
| export const useRandomData = () => useContext(Context); | |||
| const RandomDataProvider = ({ children }) => { | |||
| const setPage = (page) => { | |||
| setState({ ...state, page }); | |||
| }; | |||
| const setItemsPerPage = (itemsPerPage) => { | |||
| setState({ ...state, itemsPerPage }); | |||
| }; | |||
| const setSort = (sort) => { | |||
| setState({ ...state, sort }); | |||
| }; | |||
| const setFilter = (filter) => { | |||
| setState({ ...state, filter }); | |||
| }; | |||
| const [state, setState] = useState({ | |||
| page: 0, | |||
| setPage, | |||
| itemsPerPage: 12, | |||
| setItemsPerPage, | |||
| sort: '', | |||
| setSort, | |||
| filter: '', | |||
| setFilter, | |||
| }); | |||
| const data = usePagingHook( | |||
| state.page, | |||
| state.itemsPerPage, | |||
| state.sort, | |||
| state.filter, | |||
| apiCall | |||
| ); | |||
| return ( | |||
| <Context.Provider value={{ state, data }}>{children}</Context.Provider> | |||
| ); | |||
| }; | |||
| RandomDataProvider.propTypes = { | |||
| children: PropTypes.node, | |||
| }; | |||
| export default RandomDataProvider; | |||
| @@ -0,0 +1,17 @@ | |||
| import { useEffect, useState } from 'react'; | |||
| const useDebounce = (value, delay) => { | |||
| const [debouncedValue, setDebouncedValue] = useState(value); | |||
| useEffect(() => { | |||
| const timer = setTimeout(() => setDebouncedValue(value), delay || 500); | |||
| return () => { | |||
| clearTimeout(timer); | |||
| }; | |||
| }, [value, delay]); | |||
| return debouncedValue; | |||
| }; | |||
| export default useDebounce; | |||
| @@ -0,0 +1,66 @@ | |||
| import { useState, useCallback, useEffect } from 'react'; | |||
| import { unstable_batchedUpdates } from 'react-dom'; | |||
| const usePagingHook = (page, itemsPerPage, sort, filter, apiCallback) => { | |||
| const [items, setItems] = useState([]); | |||
| const [totalPages, setTotalPages] = useState(0); | |||
| const [currentPage, setCurrentPage] = useState(0); | |||
| const [loading, setLoading] = useState(false); | |||
| const [totalCount, setTotalCount] = useState(0); | |||
| const reload = useCallback(async () => { | |||
| setLoading(true); | |||
| try { | |||
| const [sortColumn, sortDirection] = sort.split('-'); | |||
| const response = await apiCallback( | |||
| page, | |||
| itemsPerPage, | |||
| sortColumn, | |||
| sortDirection, | |||
| filter | |||
| ); | |||
| if (response.status === 200) { | |||
| // Prevents multiple rerenders | |||
| unstable_batchedUpdates(() => { | |||
| setItems(response.data); | |||
| setTotalCount(parseInt(response.headers['x-total-count'])); | |||
| setTotalPages( | |||
| Math.ceil(response.headers['x-total-count'] / itemsPerPage) | |||
| ); | |||
| setCurrentPage(page); | |||
| }); | |||
| } | |||
| } catch (e) { | |||
| console.error(e); | |||
| } finally { | |||
| setLoading(false); | |||
| } | |||
| }, [ | |||
| setItems, | |||
| setLoading, | |||
| setTotalPages, | |||
| setCurrentPage, | |||
| apiCallback, | |||
| page, | |||
| itemsPerPage, | |||
| sort, | |||
| filter, | |||
| ]); | |||
| useEffect(() => { | |||
| reload(); | |||
| }, [reload]); | |||
| return { | |||
| items, | |||
| loading, | |||
| reload, | |||
| totalCount, | |||
| totalPages, | |||
| currentPage, | |||
| itemsPerPage, | |||
| sort, | |||
| }; | |||
| }; | |||
| export default usePagingHook; | |||
| @@ -0,0 +1,31 @@ | |||
| import { useState, useMemo } from 'react'; | |||
| import { createTheme } from '@mui/material/styles'; | |||
| import { | |||
| authScopeSetHelper, | |||
| authScopeStringGetHelper, | |||
| } from '../util/helpers/authScopeHelpers'; | |||
| const useToggleColorMode = () => { | |||
| const currentColorMode = authScopeStringGetHelper('colorMode') || 'light'; | |||
| const [mode, setMode] = useState(currentColorMode); | |||
| const toggleColorMode = () => { | |||
| const nextMode = mode === 'light' ? 'dark' : 'light'; | |||
| setMode(nextMode); | |||
| authScopeSetHelper('colorMode', nextMode); | |||
| }; | |||
| const theme = useMemo( | |||
| () => | |||
| createTheme({ | |||
| palette: { | |||
| mode, | |||
| }, | |||
| }), | |||
| [mode] | |||
| ); | |||
| return [toggleColorMode, theme]; | |||
| }; | |||
| export default useToggleColorMode; | |||
| @@ -16,6 +16,8 @@ export default { | |||
| labelUsername: 'Username', | |||
| labelPassword: 'Password', | |||
| next: 'Next', | |||
| nextPage: 'Next page', | |||
| previousPage: 'Previous page', | |||
| back: 'Back', | |||
| goBack: 'Go Back', | |||
| ok: 'Ok', | |||
| @@ -8,14 +8,17 @@ import App from './App'; | |||
| import store from './store'; | |||
| import './i18n'; | |||
| import ColorModeProvider from './context/ColorModeContext'; | |||
| ReactDOM.render( | |||
| <HelmetProvider> | |||
| <React.StrictMode> | |||
| <Provider store={store}> | |||
| <App /> | |||
| <ColorModeProvider> | |||
| <App /> | |||
| </ColorModeProvider> | |||
| </Provider> | |||
| </React.StrictMode> | |||
| </HelmetProvider>, | |||
| document.getElementById('root'), | |||
| ); | |||
| ); | |||
| @@ -12,7 +12,7 @@ import { | |||
| Link, | |||
| Grid, | |||
| } from '@mui/material'; | |||
| import Backdrop from '../../components/MUI/CustomBackdrop'; | |||
| import Backdrop from '../../components/MUI/BackdropComponent'; | |||
| import { LOGIN_PAGE } from '../../constants/pages'; | |||
| import { NavLink } from 'react-router-dom'; | |||
| @@ -1,71 +1,34 @@ | |||
| import React, { useState } from 'react'; | |||
| import { Box, Button, Divider, Paper, Typography } from '@mui/material'; | |||
| import DialogComponent from '../../components/MUI/DialogComponent'; | |||
| import DrawerComponent from '../../components/MUI/DrawerComponent'; | |||
| import Navbar from '../../components/MUI/Navbar'; | |||
| import PopoverComponent from '../../components/MUI/PopoverComponent'; | |||
| import React from 'react'; | |||
| import { Box, Grid } from '@mui/material'; | |||
| import Navbar from '../../components/MUI/NavbarComponent'; | |||
| import Modals from '../../components/MUI/Examples/ModalsExample'; | |||
| import DataGrid from '../../components/MUI/Examples/DataGridExample'; | |||
| import PagingSortingFiltering from '../../components/MUI/Examples/PagingSortingFilteringExample'; | |||
| import PagingSortingFilteringServerSide from '../../components/MUI/Examples/PagingSortingFilteringExampleServerSide'; | |||
| import RandomDataProvider from '../../context/RandomDataContext'; | |||
| const HomePage = () => { | |||
| const [dialogOpen, setDialogOpen] = useState(false); | |||
| const [drawerOpen, setDrawerOpen] = useState(false); | |||
| const [popoverOpen, setPopoverOpen] = useState(false); | |||
| const [anchorEl, setAnchorEl] = useState(null); | |||
| return ( | |||
| <> | |||
| <Navbar /> | |||
| <Box | |||
| sx={{ | |||
| mt: 4, | |||
| ml: 4, | |||
| display: 'flex', | |||
| flexGrow: 1, | |||
| }} | |||
| > | |||
| <Paper | |||
| sx={{ | |||
| p: 4, | |||
| display: 'flex', | |||
| flexDirection: 'column', | |||
| }} | |||
| > | |||
| <Typography variant="h4" gutterBottom align="center">Modals</Typography> | |||
| <Divider /> | |||
| <Button onClick={() => setDialogOpen(true)}>Open Dialog</Button> | |||
| <Button onClick={() => setDrawerOpen(true)}>Open Drawer</Button> | |||
| <Button | |||
| onClick={(e) => { | |||
| setPopoverOpen(true); | |||
| setAnchorEl(e.currentTarget); | |||
| }} | |||
| > | |||
| Open Popover | |||
| </Button> | |||
| </Paper> | |||
| <DialogComponent | |||
| title="Dialog Title" | |||
| content={<Typography>Dialog Content</Typography>} | |||
| open={dialogOpen} | |||
| onClose={() => setDialogOpen(false)} | |||
| maxWidth="md" | |||
| fullWidth | |||
| responsive | |||
| /> | |||
| <DrawerComponent | |||
| anchor="left" | |||
| content={<Typography sx={{ p: 2 }}>Drawer Content</Typography>} | |||
| open={drawerOpen} | |||
| toggleOpen={() => setDrawerOpen(!drawerOpen)} | |||
| /> | |||
| <PopoverComponent | |||
| anchorEl={anchorEl} | |||
| open={popoverOpen} | |||
| onClose={() => { | |||
| setPopoverOpen(false); | |||
| setAnchorEl(null); | |||
| }} | |||
| content={<Typography sx={{ p: 2 }}>Popover Content</Typography>} | |||
| /> | |||
| <Box sx={{ mt: 4, mx: 4 }}> | |||
| <Grid container spacing={2} justifyContent="center"> | |||
| <Grid item xs={12} md={3}> | |||
| <Modals /> | |||
| </Grid> | |||
| <Grid item xs={12} md={6}> | |||
| <DataGrid /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| <PagingSortingFiltering /> | |||
| </Grid> | |||
| <Grid item xs={12} md={9}> | |||
| {/* Move to higher components? */} | |||
| <RandomDataProvider> | |||
| <PagingSortingFilteringServerSide /> | |||
| </RandomDataProvider> | |||
| </Grid> | |||
| </Grid> | |||
| </Box> | |||
| </> | |||
| ); | |||
| @@ -25,8 +25,8 @@ import { | |||
| Typography, | |||
| } from '@mui/material'; | |||
| import { Visibility, VisibilityOff } from '@mui/icons-material'; | |||
| import Backdrop from '../../components/MUI/CustomBackdrop'; | |||
| import ErrorMessage from '../../components/MUI/CustomErrorMessage'; | |||
| import Backdrop from '../../components/MUI/BackdropComponent'; | |||
| import ErrorMessage from '../../components/MUI/ErrorMessageComponent'; | |||
| import { selectIsLoadingByActionType } from '../../store/selectors/loadingSelectors'; | |||
| import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | |||
| @@ -0,0 +1,13 @@ | |||
| import axios from 'axios'; | |||
| const JSON_SERVER_ENDPOINT = 'http://localhost:4000'; | |||
| const request = axios.create({ | |||
| baseURL: JSON_SERVER_ENDPOINT, | |||
| headers: { | |||
| 'Content-Type': 'application/json', | |||
| }, | |||
| }); | |||
| export const getRequest = (url, params = null, options = null) => | |||
| request.get(url, { params, ...options }); | |||
| @@ -0,0 +1,5 @@ | |||
| export const LOAD_DATA = 'LOAD_DATA'; | |||
| export const UPDATE_PAGE = 'UPDATE_PAGE'; | |||
| export const UPDATE_ITEMS_PER_PAGE = 'UPDATE_ITEMS_PER_PAGE'; | |||
| export const UPDATE_FILTER = 'UPDATE_FILTER'; | |||
| export const UPDATE_SORT = 'UPDATE_SORT'; | |||
| @@ -0,0 +1,32 @@ | |||
| import { | |||
| LOAD_DATA, | |||
| UPDATE_PAGE, | |||
| UPDATE_ITEMS_PER_PAGE, | |||
| UPDATE_FILTER, | |||
| UPDATE_SORT | |||
| } from './randomDataActionConstants'; | |||
| export const loadData = (payload) => ({ | |||
| type: LOAD_DATA, | |||
| payload, | |||
| }); | |||
| export const updatePage = (payload) => ({ | |||
| type: UPDATE_PAGE, | |||
| payload, | |||
| }); | |||
| export const updateItemsPerPage = (payload) => ({ | |||
| type: UPDATE_ITEMS_PER_PAGE, | |||
| payload, | |||
| }); | |||
| export const updateFilter = (payload) => ({ | |||
| type: UPDATE_FILTER, | |||
| payload, | |||
| }) | |||
| export const updateSort = (payload) => ({ | |||
| type: UPDATE_SORT, | |||
| payload, | |||
| }) | |||
| @@ -2,9 +2,11 @@ import { combineReducers } from 'redux'; | |||
| import loginReducer from './login/loginReducer'; | |||
| import loadingReducer from './loading/loadingReducer'; | |||
| import userReducer from './user/userReducer'; | |||
| import randomDataReducer from './randomData/randomDataReducer'; | |||
| export default combineReducers({ | |||
| login: loginReducer, | |||
| user: userReducer, | |||
| loading:loadingReducer | |||
| loading:loadingReducer, | |||
| randomData: randomDataReducer | |||
| }); | |||
| @@ -0,0 +1,103 @@ | |||
| import createReducer from '../../utils/createReducer'; | |||
| import { | |||
| LOAD_DATA, | |||
| UPDATE_PAGE, | |||
| UPDATE_ITEMS_PER_PAGE, | |||
| UPDATE_FILTER, | |||
| UPDATE_SORT, | |||
| } from '../../actions/randomData/randomDataActionConstants.js'; | |||
| import generate from '../../../util/helpers/randomData'; | |||
| const initialState = { | |||
| items: [], | |||
| filteredItems: [], | |||
| count: 0, | |||
| page: 0, | |||
| itemsPerPage: 12, | |||
| filter: '', | |||
| sort: '', | |||
| }; | |||
| export default createReducer( | |||
| { | |||
| [LOAD_DATA]: loadRandomData, | |||
| [UPDATE_PAGE]: updatePage, | |||
| [UPDATE_ITEMS_PER_PAGE]: updateItemsPerPage, | |||
| [UPDATE_FILTER]: updateFilter, | |||
| [UPDATE_SORT]: updateSort, | |||
| }, | |||
| initialState | |||
| ); | |||
| function loadRandomData(state, action) { | |||
| const count = action.payload; | |||
| const items = generate(count); | |||
| return { | |||
| ...state, | |||
| items, | |||
| filteredItems: items, | |||
| count: items.length, | |||
| }; | |||
| } | |||
| function updatePage(state, action) { | |||
| const page = action.payload; | |||
| return { | |||
| ...state, | |||
| page, | |||
| }; | |||
| } | |||
| function updateItemsPerPage(state, action) { | |||
| const itemsPerPage = action.payload; | |||
| return { | |||
| ...state, | |||
| itemsPerPage, | |||
| }; | |||
| } | |||
| function updateFilter(state, action) { | |||
| const filter = action.payload; | |||
| const filteredItems = filter | |||
| ? state.items.filter((item) => item.name.toLowerCase().includes(filter.toLowerCase())) : state.items; | |||
| return { | |||
| ...state, | |||
| filter, | |||
| filteredItems, | |||
| count: filteredItems.length, | |||
| }; | |||
| } | |||
| function updateSort(state, action) { | |||
| const sort = action.payload; | |||
| const [field, direction] = sort.split('-'); | |||
| const sortDirection = direction === 'asc' ? 1 : -1; | |||
| const dataItems = state.filteredItems.length | |||
| ? state.filteredItems | |||
| : state.items; | |||
| const sorted = dataItems.sort((a, b) => { | |||
| if (a[field] > b[field]) { | |||
| return sortDirection; | |||
| } | |||
| if (b[field] > a[field]) { | |||
| return sortDirection * -1; | |||
| } | |||
| return 0; | |||
| }); | |||
| const filteredItems = state.filteredItems.length | |||
| ? sorted | |||
| : state.filteredItems; | |||
| return { | |||
| ...state, | |||
| sort, | |||
| filteredItems, | |||
| }; | |||
| } | |||
| @@ -0,0 +1,32 @@ | |||
| import { createSelector } from 'reselect'; | |||
| const randomDataSelector = (state) => state.randomData; | |||
| export const itemsSelector = createSelector(randomDataSelector, (state) => | |||
| (state.filter) ? state.filteredItems : state.items | |||
| ); | |||
| export const pageSelector = createSelector( | |||
| randomDataSelector, | |||
| (state) => state.page | |||
| ); | |||
| export const itemsPerPageSelector = createSelector( | |||
| randomDataSelector, | |||
| (state) => state.itemsPerPage | |||
| ); | |||
| export const countSelector = createSelector( | |||
| randomDataSelector, | |||
| (state) => state.count | |||
| ); | |||
| export const filterSelector = createSelector( | |||
| randomDataSelector, | |||
| (state) => state.filter | |||
| ); | |||
| export const sortSelector = createSelector( | |||
| randomDataSelector, | |||
| (state) => state.sort | |||
| ); | |||
| @@ -0,0 +1,65 @@ | |||
| const random = (arr) => { | |||
| return arr[Math.floor(Math.random() * arr.length)]; | |||
| }; | |||
| const size = () => { | |||
| return random(['Extra Small', 'Small', 'Medium', 'Large', 'Extra Large']); | |||
| }; | |||
| const color = () => { | |||
| return random(['Red', 'Green', 'Blue', 'Orange', 'Yellow']); | |||
| }; | |||
| const designer = () => { | |||
| return random([ | |||
| 'Ralph Lauren', | |||
| 'Alexander Wang', | |||
| 'Grayse', | |||
| 'Marc NY Performance', | |||
| 'Scrapbook', | |||
| 'J Brand Ready to Wear', | |||
| 'Vintage Havana', | |||
| 'Neiman Marcus Cashmere Collection', | |||
| 'Derek Lam 10 Crosby', | |||
| 'Jordan', | |||
| ]); | |||
| }; | |||
| const type = () => { | |||
| return random([ | |||
| 'Cashmere', | |||
| 'Cardigans', | |||
| 'Crew and Scoop', | |||
| 'V-Neck', | |||
| 'Shoes', | |||
| 'Cowl & Turtleneck', | |||
| ]); | |||
| }; | |||
| const price = () => { | |||
| return (Math.random() * 100).toFixed(2); | |||
| }; | |||
| function generate(count) { | |||
| const data = []; | |||
| for (let i = 0; i < count; i++) { | |||
| const currentColor = color(); | |||
| const currentSize = size(); | |||
| const currentType = type(); | |||
| const currentDesigner = designer(); | |||
| const currentPrice = price(); | |||
| data.push({ | |||
| name: `${currentDesigner} ${currentType} ${currentColor} ${currentSize}`, | |||
| color: currentColor, | |||
| size: currentSize, | |||
| designer: currentDesigner, | |||
| type: currentType, | |||
| price: currentPrice, | |||
| salesPrice: currentPrice | |||
| }); | |||
| } | |||
| return data; | |||
| } | |||
| export default generate; | |||