| @@ -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 }; | |||
| }; | |||
| @@ -16,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", | |||
| @@ -40,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/randomData.js --port=4000" | |||
| }, | |||
| "eslintConfig": { | |||
| "extends": [ | |||
| @@ -91,7 +91,7 @@ const PagingSortingFilteringExample = () => { | |||
| elevation={5} | |||
| > | |||
| <Typography sx={{ my: 4 }} variant="h4" gutterBottom align="center"> | |||
| Pagination, Filtering and Sorting Example | |||
| Pagination, Filtering and Sorting Example Client Side | |||
| </Typography> | |||
| <Box | |||
| sx={{ | |||
| @@ -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; | |||
| @@ -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,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; | |||
| @@ -4,6 +4,8 @@ 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 = () => { | |||
| return ( | |||
| @@ -19,6 +21,12 @@ const HomePage = () => { | |||
| </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> | |||
| @@ -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 }); | |||