diff --git a/package.json b/package.json index 4e76d96daa19f537cc2a67dfe155a0be8c73d740..5efe222fae5cae6a465d710b6d3ee85b0f95491e 100644 --- a/package.json +++ b/package.json @@ -11,11 +11,14 @@ }, "dependencies": { "@auth0/auth0-spa-js": "^1.0.2", + "@date-io/date-fns": "1.3.13", "@material-ui/core": "^4.9.5", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "^4.0.0-alpha.46", + "@material-ui/pickers": "^3.2.10", "axios": "^0.19.0", "clsx": "^1.1.0", + "date-fns": "^2.12.0", "final-form": "^4.19.1", "lodash": "^4.17.15", "react": "^16.8.6", diff --git a/src/components/DataTable.js b/src/components/DataTable.js new file mode 100644 index 0000000000000000000000000000000000000000..f46a2da291f9574a1f54113035b5ff0a803cebaa --- /dev/null +++ b/src/components/DataTable.js @@ -0,0 +1,102 @@ +import React from "react"; +import { makeStyles } from "@material-ui/core/styles"; +import PropTypes from "prop-types"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import Paper from "@material-ui/core/Paper"; +import Skeleton from "@material-ui/lab/Skeleton"; +import { useHistory } from "react-router-dom"; + +const useStyles = makeStyles((theme) => ({ + table: { + minWidth: 650, + overflowY: "scroll" + }, + tableContainer: { + margin: 24 + }, + tableRow: { + "&:hover": { + cursor: "pointer", + backgroundColor: theme.palette.background.default + } + } +})); + +const createNumbers = (numbers) => [...Array(31)].map((_value, index) => (numbers ? numbers[index + 1] : index + 1)); + +function createData({ month, year, ...row }) { + return { month, year, sunValues: createNumbers(row) }; +} + +const headerTemplate = { + year: "Year", + month: "MÄ›sĂc", + labels: createNumbers() +}; + +const DataTable = ({ rows, initialized }) => { + const history = useHistory(); + const classes = useStyles(); + return ( + <TableContainer className={classes.tableContainer} component={Paper}> + <Table className={classes.table} aria-label="simple table"> + <TableHead> + <TableRow> + <TableCell>{headerTemplate.year}</TableCell> + <TableCell align="right">{headerTemplate.month}</TableCell> + {headerTemplate.labels.map((label) => ( + <TableCell key={label}>{label}</TableCell> + ))} + </TableRow> + </TableHead> + <TableBody> + {initialized && + rows.map((data, index) => { + const row = createData(data); + return ( + <TableRow key={`${row.name}-${index}`} onClick={() => history.push(`/detail/${data._id}`)} className={classes.tableRow}> + <TableCell component="th" scope="row"> + {row.year} + </TableCell> + <TableCell align="right">{row.month}</TableCell> + {row.sunValues.map((number, index) => ( + <TableCell key={`${number}-${index}`}>{number}</TableCell> + ))} + </TableRow> + ); + })} + {!initialized && + [...new Array(10)].map((_i, key) => ( + <TableRow key={`${key}-loader`} className={classes.tableRow}> + <TableCell scope="row" colSpan={headerTemplate.labels.length + 1}> + <Skeleton /> + </TableCell> + </TableRow> + ))} + </TableBody> + </Table> + </TableContainer> + ); +}; + +DataTable.propTypes = { + rows: PropTypes.arrayOf( + PropTypes.shape({ + month: PropTypes.node, + year: PropTypes.node + }) + ), + initialized: PropTypes.bool +}; + +DataTable.defaultProps = { + rows: [], + initialized: false +}; + +export default DataTable; diff --git a/src/views/HomePage.js b/src/views/HomePage.js index 39d0e059f166b948c298557d3dd4028cf38e4102..b393bc4c380476fc772c5e3400bef3197a40e19c 100644 --- a/src/views/HomePage.js +++ b/src/views/HomePage.js @@ -1,89 +1,18 @@ import React, { useEffect, useState } from "react"; import Api from "../utils/api"; - -import debounce from "lodash/debounce"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import Paper from "@material-ui/core/Paper"; import Pagination from "@material-ui/lab/Pagination"; -import CircularProgress from "@material-ui/core/CircularProgress"; import TextField from "@material-ui/core/TextField"; -import { useHistory } from "react-router-dom"; - -const useStyles = makeStyles((theme) => ({ - table: { - minWidth: 650, - overflowY: "scroll" - }, - tableContainer: { - margin: 24 - }, - tableRow: { - "&:hover": { - cursor: "pointer", - backgroundColor: theme.palette.background.default - } - } -})); - -const createNumbers = (numbers) => [...Array(31)].map((_value, index) => (numbers ? numbers[index + 1] : index + 1)); - -function createData({ month, year, ...row }) { - return { month, year, sunValues: createNumbers(row) }; -} - -const headerTemplate = { - year: "Year", - month: "MÄ›sĂc", - labels: createNumbers() -}; - -const DataTable = ({ rows }) => { - const history = useHistory(); - const classes = useStyles(); - return ( - <TableContainer className={classes.tableContainer} component={Paper}> - <Table className={classes.table} aria-label="simple table"> - <TableHead> - <TableRow> - <TableCell>{headerTemplate.year}</TableCell> - <TableCell align="right">{headerTemplate.month}</TableCell> - {headerTemplate.labels.map((label) => ( - <TableCell key={label}>{label}</TableCell> - ))} - </TableRow> - </TableHead> - <TableBody> - {rows.map((data, index) => { - const row = createData(data); - return ( - <TableRow key={`${row.name}-${index}`} onClick={() => history.push(`/detail/${data._id}`)} className={classes.tableRow}> - <TableCell component="th" scope="row"> - {row.year} - </TableCell> - <TableCell align="right">{row.month}</TableCell> - {row.sunValues.map((number, index) => ( - <TableCell key={`${number}-${index}`}>{number}</TableCell> - ))} - </TableRow> - ); - })} - </TableBody> - </Table> - </TableContainer> - ); -}; +import DataTable from "../components/DataTable"; +import Grid from "@material-ui/core/Grid"; +import Toolbar from "@material-ui/core/Toolbar"; +import Autocomplete from "@material-ui/lab/Autocomplete"; const queryBuilder = ({ max = 15, skip = 0, filter, sortBy }) => { - return `max=${max}&skip=${skip}`; + return `q={${filter ? `"year": ${filter}` : ""}}&max=${max}&skip=${skip}`; }; const HomePage = () => { + const [years, setYears] = useState([]); const [data, setData] = useState([]); const [{ max, skip, count, filterValue }, setPagination] = useState({ max: 15, @@ -92,41 +21,32 @@ const HomePage = () => { filterValue: "" }); const [initialized, setInitialized] = useState(false); - const [filter, setFilter] = useState(""); - const handleFilter = ({ target: { value } }) => { - setFilter(value); - }; - const onFilter = () => + + const onFilter = (e, filter) => { setPagination((prevPagination) => ({ ...prevPagination, filterValue: filter })); + }; const handlePagination = (_event, page) => { + setInitialized(false); setPagination((prevPagination) => ({ ...prevPagination, skip: (page - 1) * prevPagination.max })); }; - const query = queryBuilder({ max, skip, filterValue }); useEffect(() => { setInitialized(false); Promise.all([ - Api.get(`rest/sunshine?q={}&${query}`).then((data) => setData(data)), - Api.get(`rest/sunshine?q={}&h={"$aggregate":["COUNT:"]}`).then((data) => - setPagination((prevPagination) => ({ ...prevPagination, count: data["COUNT "] })) - ) + Api.get(`rest/sunshine?${queryBuilder({ max, skip, filter: filterValue })}&totals=true`).then(({ data, totals }) => { + setData(data); + setPagination((prevPagination) => ({ ...prevPagination, count: totals.total })); + }), + Api.get(`rest/sunshine?q={}&h={"$fields": {"year": 1}}`).then((data) => setYears([...new Set(data.map(({ year }) => `${year}`))])) ]).then(() => setInitialized(true)); }, [skip, filterValue]); - if (!initialized) { - return ( - <div> - <CircularProgress /> - </div> - ); - } - const addMessage = () => Api.updateData("professors.messages", { message: "Hola" }, "add"); const replaceMessage = () => Api.updateData("professors.messages", { message: "Hola" }, "replace"); return ( @@ -134,9 +54,34 @@ const HomePage = () => { <h1>Home page</h1> <button onClick={addMessage}>Add message</button> <button onClick={replaceMessage}>replace message</button> - <TextField onBlur={onFilter} type="number" label="Filter by year" value={filter} onChange={handleFilter} /> - <Pagination count={Math.ceil(count / max)} page={skip / max + 1} onChange={handlePagination} /> - <DataTable rows={data} /> + <Toolbar> + <Grid container spacing={3} justify="space-between"> + <Grid item> + <Autocomplete + style={{ minWidth: "150px" }} + options={years} + id="year-picker" + value={filterValue} + onChange={onFilter} + disabled={!initialized} + renderInput={(params) => <TextField {...params} label="Select year" margin="normal" />} + /> + </Grid> + <Grid item> + <Pagination + style={{ marginTop: "12px" }} + count={Math.ceil(count / max)} + page={skip / max + 1} + onChange={handlePagination} + variant="outlined" + shape="rounded" + showFirstButton + showLastButton + /> + </Grid> + </Grid> + </Toolbar> + <DataTable rows={data} initialized={initialized} /> </div> ); }; diff --git a/yarn.lock b/yarn.lock index 17164dbaa19713562f4ef1994e06c27eaf6316dc..f67f285e8669372318de36f46a205fa34b6af369 100644 --- a/yarn.lock +++ b/yarn.lock @@ -959,7 +959,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.8.3", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.6.0", "@babel/runtime@^7.8.3", "@babel/runtime@^7.9.2": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== @@ -1017,6 +1017,18 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-9.0.1.tgz#c27b391d8457d1e893f1eddeaf5e5412d12ffbb5" integrity sha512-6It2EVfGskxZCQhuykrfnALg7oVeiI6KclWSmGDqB0AiInVrTGB9Jp9i4/Ad21u9Jde/voVQz6eFX/eSg/UsPA== +"@date-io/core@1.x", "@date-io/core@^1.3.13": + version "1.3.13" + resolved "https://registry.yarnpkg.com/@date-io/core/-/core-1.3.13.tgz#90c71da493f20204b7a972929cc5c482d078b3fa" + integrity sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA== + +"@date-io/date-fns@1.3.13": + version "1.3.13" + resolved "https://registry.yarnpkg.com/@date-io/date-fns/-/date-fns-1.3.13.tgz#7798844041640ab393f7e21a7769a65d672f4735" + integrity sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA== + dependencies: + "@date-io/core" "^1.3.13" + "@emotion/hash@^0.7.4": version "0.7.4" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.7.4.tgz#f14932887422c9056b15a8d222a9074a7dfa2831" @@ -1238,6 +1250,18 @@ prop-types "^15.7.2" react-is "^16.8.0" +"@material-ui/pickers@^3.2.10": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" + integrity sha512-B8G6Obn5S3RCl7hwahkQj9sKUapwXWFjiaz/Bsw1fhYFdNMnDUolRiWQSoKPb1/oKe37Dtfszoywi1Ynbo3y8w== + dependencies: + "@babel/runtime" "^7.6.0" + "@date-io/core" "1.x" + "@types/styled-jsx" "^2.2.8" + clsx "^1.0.2" + react-transition-group "^4.0.0" + rifm "^0.7.0" + "@material-ui/styles@^4.9.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.9.0.tgz#10c31859f6868cfa9d3adf6b6c3e32c9d676bc76" @@ -1496,6 +1520,13 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/styled-jsx@^2.2.8": + version "2.2.8" + resolved "https://registry.yarnpkg.com/@types/styled-jsx/-/styled-jsx-2.2.8.tgz#b50d13d8a3c34036282d65194554cf186bab7234" + integrity sha512-Yjye9VwMdYeXfS71ihueWRSxrruuXTwKCbzue4+5b2rjnQ//AtyM7myZ1BEhNhBQ/nL/RE7bdToUoLln2miKvg== + dependencies: + "@types/react" "*" + "@types/yargs-parser@*": version "13.1.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.1.0.tgz#c563aa192f39350a1d18da36c5a8da382bbd8228" @@ -3259,6 +3290,11 @@ data-urls@^1.0.0, data-urls@^1.1.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" +date-fns@^2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.12.0.tgz#01754c8a2f3368fc1119cf4625c3dad8c1845ee6" + integrity sha512-qJgn99xxKnFgB1qL4jpxU7Q2t0LOn1p8KMIveef3UZD7kqjT3tpFNNdXJelEHhE+rUgffriXriw/sOSU+cS1Hw== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -8353,7 +8389,7 @@ react-scripts@3.0.1: optionalDependencies: fsevents "2.0.6" -react-transition-group@^4.3.0: +react-transition-group@^4.0.0, react-transition-group@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683" integrity sha512-1qRV1ZuVSdxPlPf4O8t7inxUGpdyO5zG9IoNfJxSO0ImU2A1YWkEQvFPuIPZmMLkg5hYs7vv5mMOyfgSkvAwvw== @@ -8697,6 +8733,13 @@ rgba-regex@^1.0.0: resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3" integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= +rifm@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.7.0.tgz#debe951a9c83549ca6b33e5919f716044c2230be" + integrity sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ== + dependencies: + "@babel/runtime" "^7.3.1" + rimraf@2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"