| 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 }; | |||||
| }; |
| "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/randomData.js --port=4000" | |||||
| }, | }, | ||||
| "eslintConfig": { | "eslintConfig": { | ||||
| "extends": [ | "extends": [ |
| elevation={5} | elevation={5} | ||||
| > | > | ||||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | ||||
| Pagination, Filtering and Sorting Example | |||||
| Pagination, Filtering and Sorting Example Client Side | |||||
| </Typography> | </Typography> | ||||
| <Box | <Box | ||||
| sx={{ | sx={{ |
| 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, { 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 { 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 Modals from '../../components/MUI/Examples/ModalsExample'; | import Modals from '../../components/MUI/Examples/ModalsExample'; | ||||
| import DataGrid from '../../components/MUI/Examples/DataGridExample'; | import DataGrid from '../../components/MUI/Examples/DataGridExample'; | ||||
| import PagingSortingFiltering from '../../components/MUI/Examples/PagingSortingFilteringExample'; | 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 ( | ||||
| </Grid> | </Grid> | ||||
| <Grid item xs={12} md={9}> | <Grid item xs={12} md={9}> | ||||
| <PagingSortingFiltering /> | <PagingSortingFiltering /> | ||||
| </Grid> | |||||
| <Grid item xs={12} md={9}> | |||||
| {/* Move to higher components? */} | |||||
| <RandomDataProvider> | |||||
| <PagingSortingFilteringServerSide /> | |||||
| </RandomDataProvider> | |||||
| </Grid> | </Grid> | ||||
| </Grid> | </Grid> | ||||
| </Box> | </Box> |
| 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 }); |