paging-sorting-filtering into master 3 years ago
| 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 }; | |||||
| }; |
| "@emotion/styled": "^11.3.0", | "@emotion/styled": "^11.3.0", | ||||
| "@mui/icons-material": "^5.0.5", | "@mui/icons-material": "^5.0.5", | ||||
| "@mui/material": "^5.0.6", | "@mui/material": "^5.0.6", | ||||
| "@mui/x-data-grid": "^5.0.1", | |||||
| "@reduxjs/toolkit": "^1.5.1", | "@reduxjs/toolkit": "^1.5.1", | ||||
| "@testing-library/jest-dom": "^5.13.0", | "@testing-library/jest-dom": "^5.13.0", | ||||
| "@testing-library/react": "^11.2.7", | "@testing-library/react": "^11.2.7", | ||||
| "date-fns": "^2.22.1", | "date-fns": "^2.22.1", | ||||
| "eslint-plugin-prettier": "^3.4.0", | "eslint-plugin-prettier": "^3.4.0", | ||||
| "eslint-plugin-security": "^1.4.0", | "eslint-plugin-security": "^1.4.0", | ||||
| "faker": "^5.5.3", | |||||
| "formik": "^2.2.9", | "formik": "^2.2.9", | ||||
| "i18next": "^20.3.1", | "i18next": "^20.3.1", | ||||
| "json-server": "^0.17.0", | |||||
| "jsonwebtoken": "^8.5.1", | "jsonwebtoken": "^8.5.1", | ||||
| "lodash": "^4.17.21", | "lodash": "^4.17.21", | ||||
| "lodash.isempty": "^4.4.0", | "lodash.isempty": "^4.4.0", | ||||
| "start": "react-scripts start", | "start": "react-scripts start", | ||||
| "build": "react-scripts build", | "build": "react-scripts build", | ||||
| "test": "react-scripts test", | "test": "react-scripts test", | ||||
| "eject": "react-scripts eject" | |||||
| "eject": "react-scripts eject", | |||||
| "json-serve": "json-server ./db/db.js --port=4000" | |||||
| }, | }, | ||||
| "eslintConfig": { | "eslintConfig": { | ||||
| "extends": [ | "extends": [ |
| import { Backdrop, CircularProgress } from '@mui/material'; | import { Backdrop, CircularProgress } from '@mui/material'; | ||||
| import { alpha } from '@mui/system'; | import { alpha } from '@mui/system'; | ||||
| const CustomBackdrop = ({ position = 'fixed', isLoading }) => ( | |||||
| const BackdropComponent = ({ position = 'fixed', isLoading }) => ( | |||||
| <Backdrop | <Backdrop | ||||
| sx={{ | 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, | position, | ||||
| backgroundColor: ({ palette }) => | backgroundColor: ({ palette }) => | ||||
| alpha(palette.background.default, palette.action.disabledOpacity), | alpha(palette.background.default, palette.action.disabledOpacity), | ||||
| </Backdrop> | </Backdrop> | ||||
| ); | ); | ||||
| CustomBackdrop.propTypes = { | |||||
| BackdropComponent.propTypes = { | |||||
| position: PropTypes.oneOf(['fixed', 'absolute']), | position: PropTypes.oneOf(['fixed', 'absolute']), | ||||
| isLoading: PropTypes.bool.isRequired, | isLoading: PropTypes.bool.isRequired, | ||||
| }; | }; | ||||
| export default CustomBackdrop; | |||||
| export default BackdropComponent; |
| import React from 'react'; | |||||
| import PropTypes from 'prop-types'; | |||||
| import { | |||||
| Dialog, | |||||
| DialogContent, | |||||
| DialogTitle, | |||||
| DialogActions, | |||||
| Button, | |||||
| useMediaQuery, | |||||
| useTheme, | |||||
| } from '@mui/material'; | |||||
| const DialogComponent = ({ | |||||
| title, | |||||
| content, | |||||
| onClose, | |||||
| open, | |||||
| maxWidth, | |||||
| fullWidth, | |||||
| responsive, | |||||
| }) => { | |||||
| const theme = useTheme(); | |||||
| const fullScreen = useMediaQuery(theme.breakpoints.down('md')); | |||||
| const handleClose = () => { | |||||
| onClose(); | |||||
| }; | |||||
| return ( | |||||
| <Dialog | |||||
| maxWidth={maxWidth} | |||||
| fullWidth={fullWidth} | |||||
| fullScreen={responsive && fullScreen} | |||||
| onClose={handleClose} | |||||
| open={open} | |||||
| > | |||||
| <DialogTitle>{title}</DialogTitle> | |||||
| {content && <DialogContent>{content}</DialogContent>} | |||||
| <DialogActions> | |||||
| <Button onClick={handleClose}>OK</Button> | |||||
| <Button onClick={handleClose}>Cancel</Button> | |||||
| </DialogActions> | |||||
| </Dialog> | |||||
| ); | |||||
| }; | |||||
| DialogComponent.propTypes = { | |||||
| title: PropTypes.string, | |||||
| open: PropTypes.bool.isRequired, | |||||
| content: PropTypes.any, | |||||
| onClose: PropTypes.func.isRequired, | |||||
| maxWidth: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']), | |||||
| fullWidth: PropTypes.bool, | |||||
| responsive: PropTypes.bool, | |||||
| }; | |||||
| export default DialogComponent; |
| import React from 'react'; | import React from 'react'; | ||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
| import { | |||||
| Drawer, | |||||
| List, | |||||
| ListItem, | |||||
| ListItemButton, | |||||
| ListItemIcon, | |||||
| ListItemText, | |||||
| } from '@mui/material'; | |||||
| import { Drawer } from '@mui/material'; | |||||
| const DrawerComponent = ({ open, toggleOpen }) => ( | |||||
| <Drawer anchor="right" open={open} onClose={toggleOpen}> | |||||
| <List> | |||||
| <ListItemButton divider onClick={toggleOpen}> | |||||
| <ListItemIcon> | |||||
| <ListItemText>Link 1</ListItemText> | |||||
| </ListItemIcon> | |||||
| </ListItemButton> | |||||
| <ListItem divider onClick={toggleOpen}> | |||||
| <ListItemIcon> | |||||
| <ListItemText>Link 2</ListItemText> | |||||
| </ListItemIcon> | |||||
| </ListItem> | |||||
| <ListItem divider onClick={toggleOpen}> | |||||
| <ListItemText>Link 3</ListItemText> | |||||
| </ListItem> | |||||
| </List> | |||||
| const DrawerComponent = ({ open, toggleOpen, content, anchor = 'right' }) => ( | |||||
| <Drawer | |||||
| sx={{ | |||||
| minWidth: 250, | |||||
| '& .MuiDrawer-paper': { | |||||
| minWidth: 250, | |||||
| }, | |||||
| }} | |||||
| anchor={anchor} | |||||
| open={open} | |||||
| onClose={toggleOpen} | |||||
| > | |||||
| {content ? content : null} | |||||
| </Drawer> | </Drawer> | ||||
| ); | ); | ||||
| DrawerComponent.propTypes = { | DrawerComponent.propTypes = { | ||||
| open: PropTypes.bool, | open: PropTypes.bool, | ||||
| toggleOpen: PropTypes.func, | toggleOpen: PropTypes.func, | ||||
| content: PropTypes.any, | |||||
| anchor: PropTypes.oneOf(['top', 'right', 'left', 'bottom']), | |||||
| }; | }; | ||||
| export default DrawerComponent; | export default DrawerComponent; |
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||
| import { Typography } from '@mui/material'; | import { Typography } from '@mui/material'; | ||||
| const CustomErrorMessage = ({ error }) => ( | |||||
| const ErrorMessageComponent = ({ error }) => ( | |||||
| <Typography variant="body1" color="error" my={2}> | <Typography variant="body1" color="error" my={2}> | ||||
| {error} | {error} | ||||
| </Typography> | </Typography> | ||||
| ); | ); | ||||
| CustomErrorMessage.propTypes = { | |||||
| ErrorMessageComponent.propTypes = { | |||||
| error: PropTypes.string.isRequired, | error: PropTypes.string.isRequired, | ||||
| }; | }; | ||||
| export default CustomErrorMessage; | |||||
| export default ErrorMessageComponent; |
| 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; |
| 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; |
| 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; |
| 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; |
| import React, { useState } from 'react'; | import React, { useState } from 'react'; | ||||
| import { Button, Menu, MenuItem } from '@mui/material'; | import { Button, Menu, MenuItem } from '@mui/material'; | ||||
| const MenuList = () => { | |||||
| const MenuListComponent = () => { | |||||
| const [anchorEl, setAnchorEl] = useState(null); | const [anchorEl, setAnchorEl] = useState(null); | ||||
| const open = Boolean(anchorEl); | const open = Boolean(anchorEl); | ||||
| const handleClick = (event) => { | const handleClick = (event) => { | ||||
| ); | ); | ||||
| }; | }; | ||||
| export default MenuList; | |||||
| export default MenuListComponent; |
| import React, { useState } from 'react'; | |||||
| import { | |||||
| AppBar, | |||||
| Badge, | |||||
| Box, | |||||
| IconButton, | |||||
| Toolbar, | |||||
| Typography, | |||||
| useMediaQuery, | |||||
| } from '@mui/material'; | |||||
| import { useTheme } from '@mui/system'; | |||||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||||
| import MenuList from './MenuList'; | |||||
| import Drawer from './DrawerComponent'; | |||||
| const Navbar = () => { | |||||
| const [openDrawer, setOpenDrawer] = useState(false); | |||||
| const theme = useTheme(); | |||||
| const matches = useMediaQuery(theme.breakpoints.down('sm')); | |||||
| const handleToggleDrawer = () => { | |||||
| setOpenDrawer(!openDrawer); | |||||
| }; | |||||
| return ( | |||||
| <> | |||||
| <AppBar elevation={2} sx={{ backgroundColor: 'background.default' }}> | |||||
| <Toolbar> | |||||
| <Box | |||||
| component="div" | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'space-between', | |||||
| alignItems: 'center', | |||||
| width: '100%', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Drawer open={openDrawer} toggleOpen={handleToggleDrawer} /> | |||||
| ) : ( | |||||
| <Box sx={{ display: 'flex' }}> | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 1 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="body1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 2 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="subtitle1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 3 | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| <Box> | |||||
| <MenuList /> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'center', | |||||
| alignItems: 'center', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Box> | |||||
| <IconButton onClick={handleToggleDrawer}> | |||||
| <MenuOutlinedIcon /> | |||||
| </IconButton> | |||||
| </Box> | |||||
| ) : ( | |||||
| <IconButton> | |||||
| <Badge badgeContent={3} color="primary"> | |||||
| <ShoppingBasketIcon color="action" /> | |||||
| </Badge> | |||||
| </IconButton> | |||||
| )} | |||||
| </Box> | |||||
| </Box> | |||||
| </Toolbar> | |||||
| </AppBar> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Navbar; |
| import React, { useState, useMemo, useContext } from 'react'; | |||||
| import { | |||||
| AppBar, | |||||
| Badge, | |||||
| Box, | |||||
| IconButton, | |||||
| Toolbar, | |||||
| Typography, | |||||
| List, | |||||
| ListItem, | |||||
| ListItemButton, | |||||
| ListItemIcon, | |||||
| ListItemText, | |||||
| useMediaQuery, | |||||
| } from '@mui/material'; | |||||
| import { useTheme } from '@mui/system'; | |||||
| import MenuOutlinedIcon from '@mui/icons-material/MenuOutlined'; | |||||
| import ShoppingBasketIcon from '@mui/icons-material/ShoppingBasket'; | |||||
| 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 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> | |||||
| <ListItem divider> | |||||
| <IconButton onClick={toggleColorMode}> | |||||
| <ListItemText>Toggle {theme.palette.mode} mode</ListItemText> | |||||
| {theme.palette.mode === 'dark' ? ( | |||||
| <Brightness7Icon /> | |||||
| ) : ( | |||||
| <Brightness4Icon /> | |||||
| )} | |||||
| </IconButton> | |||||
| </ListItem> | |||||
| </List> | |||||
| ), | |||||
| [handleToggleDrawer] | |||||
| ); | |||||
| return ( | |||||
| <AppBar | |||||
| elevation={2} | |||||
| sx={{ backgroundColor: 'background.default', position: 'relative' }} | |||||
| > | |||||
| <Toolbar> | |||||
| <Box | |||||
| component="div" | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'space-between', | |||||
| alignItems: 'center', | |||||
| width: '100%', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Drawer | |||||
| open={openDrawer} | |||||
| toggleOpen={handleToggleDrawer} | |||||
| content={drawerContent} | |||||
| /> | |||||
| ) : ( | |||||
| <Box sx={{ display: 'flex' }}> | |||||
| <Typography | |||||
| variant="h6" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 1 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="body1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 2 | |||||
| </Typography> | |||||
| <Typography | |||||
| variant="subtitle1" | |||||
| sx={{ | |||||
| marginRight: 3, | |||||
| cursor: 'pointer', | |||||
| color: 'text.primary', | |||||
| }} | |||||
| > | |||||
| Link 3 | |||||
| </Typography> | |||||
| </Box> | |||||
| )} | |||||
| <Box> | |||||
| <MenuList /> | |||||
| </Box> | |||||
| <Box | |||||
| sx={{ | |||||
| display: 'flex', | |||||
| justifyContent: 'center', | |||||
| alignItems: 'center', | |||||
| }} | |||||
| > | |||||
| {matches ? ( | |||||
| <Box> | |||||
| <IconButton onClick={handleToggleDrawer}> | |||||
| <MenuOutlinedIcon /> | |||||
| </IconButton> | |||||
| </Box> | |||||
| ) : ( | |||||
| <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> | |||||
| </Toolbar> | |||||
| </AppBar> | |||||
| ); | |||||
| }; | |||||
| export default NavbarComponent; |
| import React from 'react'; | |||||
| import PropTypes from 'prop-types'; | |||||
| import { Box, Popover } from '@mui/material'; | |||||
| const PopoverComponent = ({ open, anchorEl, onClose, content }) => { | |||||
| const handleClose = () => { | |||||
| onClose(); | |||||
| }; | |||||
| return ( | |||||
| <Box component="div"> | |||||
| <Popover | |||||
| sx={{ p: 5 }} | |||||
| open={open} | |||||
| anchorEl={anchorEl} | |||||
| onClose={handleClose} | |||||
| anchorOrigin={{ | |||||
| vertical: 'bottom', | |||||
| horizontal: 'left', | |||||
| }} | |||||
| > | |||||
| {content} | |||||
| </Popover> | |||||
| </Box> | |||||
| ); | |||||
| }; | |||||
| PopoverComponent.propTypes = { | |||||
| anchorEl: PropTypes.object, | |||||
| open: PropTypes.bool.isRequired, | |||||
| onClose: PropTypes.func.isRequired, | |||||
| content: PropTypes.any, | |||||
| }; | |||||
| export default PopoverComponent; |
| 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; |
| 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; |
| 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; |
| 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; |
| 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; |
| i18n.use(initReactI18next).init({ | i18n.use(initReactI18next).init({ | ||||
| lng: 'en', | lng: 'en', | ||||
| fallbackLng: 'en', | fallbackLng: 'en', | ||||
| debug: true, | |||||
| debug: false, | |||||
| supportedLngs: ['en'], | supportedLngs: ['en'], | ||||
| resources: { | resources: { | ||||
| en: { | en: { |
| labelUsername: 'Username', | labelUsername: 'Username', | ||||
| labelPassword: 'Password', | labelPassword: 'Password', | ||||
| next: 'Next', | next: 'Next', | ||||
| nextPage: 'Next page', | |||||
| previousPage: 'Previous page', | |||||
| back: 'Back', | back: 'Back', | ||||
| goBack: 'Go Back', | goBack: 'Go Back', | ||||
| ok: 'Ok', | ok: 'Ok', |
| import store from './store'; | import store from './store'; | ||||
| import './i18n'; | import './i18n'; | ||||
| import ColorModeProvider from './context/ColorModeContext'; | |||||
| ReactDOM.render( | ReactDOM.render( | ||||
| <HelmetProvider> | <HelmetProvider> | ||||
| <React.StrictMode> | <React.StrictMode> | ||||
| <Provider store={store}> | <Provider store={store}> | ||||
| <App /> | |||||
| <ColorModeProvider> | |||||
| <App /> | |||||
| </ColorModeProvider> | |||||
| </Provider> | </Provider> | ||||
| </React.StrictMode> | </React.StrictMode> | ||||
| </HelmetProvider>, | </HelmetProvider>, |
| Link, | Link, | ||||
| Grid, | Grid, | ||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import Backdrop from '../../components/MUI/CustomBackdrop'; | |||||
| import Backdrop from '../../components/MUI/BackdropComponent'; | |||||
| import { LOGIN_PAGE } from '../../constants/pages'; | import { LOGIN_PAGE } from '../../constants/pages'; | ||||
| import { NavLink } from 'react-router-dom'; | import { NavLink } from 'react-router-dom'; | ||||
| import React from 'react'; | import React from 'react'; | ||||
| import Navbar from '../../components/MUI/Navbar'; | |||||
| 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 HomePage = () => { | ||||
| return ( | return ( | ||||
| <Navbar /> | |||||
| ) | |||||
| <> | |||||
| <Navbar /> | |||||
| <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> | |||||
| </> | |||||
| ); | |||||
| }; | }; | ||||
| export default HomePage; | export default HomePage; |
| Typography, | Typography, | ||||
| } from '@mui/material'; | } from '@mui/material'; | ||||
| import { Visibility, VisibilityOff } from '@mui/icons-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 { selectIsLoadingByActionType } from '../../store/selectors/loadingSelectors'; | ||||
| import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | import { LOGIN_USER_LOADING } from '../../store/actions/login/loginActionConstants'; | ||||
| const LoginValidationSchema = Yup.object().shape({ | |||||
| username: Yup.string().required(i18next.t('login.usernameRequired')), | |||||
| password: Yup.string().required(i18next.t('login.passwordRequired')), | |||||
| }); | |||||
| const LoginPage = ({ history }) => { | const LoginPage = ({ history }) => { | ||||
| const dispatch = useDispatch(); | const dispatch = useDispatch(); | ||||
| const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
| ); | ); | ||||
| const handleApiResponseSuccess = () => { | const handleApiResponseSuccess = () => { | ||||
| // history.push({ | |||||
| // pathname: HOME_PAGE, | |||||
| // state: { | |||||
| // from: history.location.pathname, | |||||
| // }, | |||||
| // }); | |||||
| history.push({ | |||||
| pathname: HOME_PAGE, | |||||
| state: { | |||||
| from: history.location.pathname, | |||||
| }, | |||||
| }); | |||||
| }; | }; | ||||
| const handleSubmit = (values) => { | const handleSubmit = (values) => { | ||||
| username: '', | username: '', | ||||
| password: '', | password: '', | ||||
| }, | }, | ||||
| validationSchema: LoginValidationSchema, | |||||
| validationSchema: Yup.object().shape({ | |||||
| username: Yup.string().required(t('login.usernameRequired')), | |||||
| password: Yup.string().required(t('login.passwordRequired')), | |||||
| }), | |||||
| onSubmit: handleSubmit, | onSubmit: handleSubmit, | ||||
| validateOnBlur: true, | validateOnBlur: true, | ||||
| enableReinitialize: true, | enableReinitialize: true, |
| 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 }); |
| 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'; |
| 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, | |||||
| }) |
| import loginReducer from './login/loginReducer'; | import loginReducer from './login/loginReducer'; | ||||
| import loadingReducer from './loading/loadingReducer'; | import loadingReducer from './loading/loadingReducer'; | ||||
| import userReducer from './user/userReducer'; | import userReducer from './user/userReducer'; | ||||
| import randomDataReducer from './randomData/randomDataReducer'; | |||||
| export default combineReducers({ | export default combineReducers({ | ||||
| login: loginReducer, | login: loginReducer, | ||||
| user: userReducer, | user: userReducer, | ||||
| loading:loadingReducer | |||||
| loading:loadingReducer, | |||||
| randomData: randomDataReducer | |||||
| }); | }); |
| 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, | |||||
| }; | |||||
| } |
| 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 | |||||
| ); |
| 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; |