diff --git a/client/package.json b/client/package.json index cc0a0844..f6efad4a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "statshammer", - "version": "0.4.0", + "version": "0.5.0", "private": true, "proxy": "http://localhost:5000/", "engines": { @@ -31,6 +31,9 @@ "eslint-plugin-jsx-a11y": "^6.2.3", "eslint-plugin-react": "^7.16.0", "format-unicorn": "^1.1.1", + "html2canvas": "^1.0.0-rc.5", + "jspdf": "^1.5.3", + "jspdf-autotable": "^3.2.11", "lodash": "^4.17.15", "lorem-ipsum": "^2.0.3", "nanoid": "^2.1.7", diff --git a/client/src/components/AppBar/AppMenu.jsx b/client/src/components/AppBar/AppMenu.jsx index f55e0e35..2b9eaafa 100644 --- a/client/src/components/AppBar/AppMenu.jsx +++ b/client/src/components/AppBar/AppMenu.jsx @@ -2,42 +2,32 @@ import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { - Menu, MenuItem, IconButton, Typography, Button, useMediaQuery, + Menu, IconButton, Button, useMediaQuery, } from '@material-ui/core'; -import { - MoreVert, BarChart, ImportExport, BrightnessMedium, Delete, -} from '@material-ui/icons'; -import { clearAllUnits, addUnit } from 'actions/units.action'; -import { toggleDarkMode, toggleDesktopGraphList } from 'actions/config.action'; +import { MoreVert } from '@material-ui/icons'; +import { clearAllUnits } from 'actions/units.action'; import { connect } from 'react-redux'; import ConfirmationDialog from 'components/ConfirmationDialog'; -import { useHistory, Route } from 'react-router-dom'; +import { Route } from 'react-router-dom'; import { addNotification } from 'actions/notifications.action'; -import Uploader from 'components/Uploader'; -import { addUnitEnabled } from 'utils/unitHelpers'; - +import PdfDownloadItem from './PdfDownloadItem'; +import ToggleDarkModeItem from './ToggleDarkModeItem'; +import ClearUnitsItem from './ClearUnitsItem'; +import ToggleGraphListItem from './ToggleGraphListItem'; +import ImportUnitItem from './ImportUnitItem'; const useStyles = makeStyles((theme) => ({ menu: {}, icon: { color: theme.palette.primary.contrastText, }, - caption: { - paddingBottom: theme.spacing(1), - }, - menuItemIcon: { - marginRight: theme.spacing(1), - }, })); /** * A menu list containing various actions that can be performed */ -const AppMenu = ({ - clearAllUnits, addNotification, toggleDarkMode, addUnit, toggleDesktopGraphList, -}) => { +const AppMenu = ({ clearAllUnits, addNotification }) => { const classes = useStyles(); - const history = useHistory(); const theme = useTheme(); const mobile = useMediaQuery(theme.breakpoints.down('sm')); @@ -66,16 +56,6 @@ const AppMenu = ({ handleMenuClose(); }, [handleMenuClose]); - /** - * Change the URL bar to a new location. This is used to display dialog boxes that - * retains proper navigation - * @param {string} newloc the new URL to set - */ - const setLocation = useCallback((newloc) => { - handleMenuClose(); - history.push(newloc); - }, [handleMenuClose, history]); - /** * Handle the case when the confirm option is selected from the clear all units dialog */ @@ -84,20 +64,6 @@ const AppMenu = ({ addNotification({ message: 'All units cleared', variant: 'info' }); }, [addNotification, clearAllUnits, menuItemClick]); - /** Is the upload menu item disabled or not */ - const isUploadDisabled = !addUnitEnabled(); - - /** The function to call when a file upload happens. - * In this case that would be importing the uploaded unit data - * @param {object} data the JSON from the uploaded unit - * */ - const onUnitUpload = useCallback((data) => { - if (data && data.name && data.weapon_profiles) { - addNotification({ message: 'Successfully imported unit', variant: 'success' }); - addUnit(data.name, data.weapon_profiles); - } - }, [addNotification, addUnit]); - return (
{mobile @@ -123,33 +89,11 @@ const AppMenu = ({ open={Boolean(anchorEl)} onClose={handleMenuClose} > - setLocation(confirmPath)}> - - Clear Units - - menuItemClick(toggleDarkMode)}> - - Toggle Dark Mode  - - Beta - - - {!mobile && ( - menuItemClick(toggleDesktopGraphList)}> - - Toggle Graph List/Tabs - - )} - menuItemClick(() => onUnitUpload(data))} - disabled={isUploadDisabled} - component={( - - - Import Unit - - )} - /> + + + {!mobile && } + + ({ + menuItemIcon: { + marginRight: theme.spacing(1), + }, + item: { + color: theme.palette.getContrastText(theme.palette.background.paper), + }, + link: { + color: theme.palette.getContrastText(theme.palette.background.paper), + textDecoration: 'none', + }, +})); + +const ClearUnitsItem = ({ onClick }) => { + const classes = useStyles(); + + const handleClick = () => { + onClick(); + }; + + return ( + + + + Clear All Units + + + ); +}; + +ClearUnitsItem.propTypes = { + /** A callback function to call when the menu item is clicked */ + onClick: PropTypes.func.isRequired, +}; + +export default ClearUnitsItem; diff --git a/client/src/components/AppBar/ImportUnitItem.jsx b/client/src/components/AppBar/ImportUnitItem.jsx new file mode 100644 index 00000000..e99e25ec --- /dev/null +++ b/client/src/components/AppBar/ImportUnitItem.jsx @@ -0,0 +1,76 @@ +import React, { useCallback } from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import { MenuItem } from '@material-ui/core'; +import { ImportExport } from '@material-ui/icons'; +import { addUnit } from 'actions/units.action'; +import { connect } from 'react-redux'; +import { addNotification } from 'actions/notifications.action'; +import Uploader from 'components/Uploader'; +import { addUnitEnabled } from 'utils/unitHelpers'; + +const useStyles = makeStyles((theme) => ({ + menu: {}, + icon: { + color: theme.palette.primary.contrastText, + }, + caption: { + paddingBottom: theme.spacing(1), + }, + menuItemIcon: { + marginRight: theme.spacing(1), + }, +})); + + +const ImportUnitItem = ({ + // eslint-disable-next-line no-unused-vars + numUnits, onClick, addNotification, addUnit, +}) => { + const classes = useStyles(); + + /** Is the upload menu item disabled or not */ + const isUploadDisabled = !addUnitEnabled(); + + /** The function to call when a file upload happens. + * In this case that would be importing the uploaded unit data + * @param {object} data the JSON from the uploaded unit + * */ + const onUnitUpload = useCallback((data) => { + if (data && data.name && data.weapon_profiles) { + onClick(); + addNotification({ message: 'Successfully imported unit', variant: 'success' }); + addUnit(data.name, data.weapon_profiles); + } + }, [addNotification, addUnit, onClick]); + + return ( + + + Import Unit + + )} + /> + ); +}; + +ImportUnitItem.propTypes = { + /** The current number of units. Used to ensure that the item is disabled when limit is reached */ + numUnits: PropTypes.number.isRequired, + /** A callback function to call when the menu item is clicked */ + onClick: PropTypes.func.isRequired, + /** A function to call to add a notification to the stack */ + addNotification: PropTypes.func.isRequired, + /** A function to call to add a new unit */ + addUnit: PropTypes.func.isRequired, +}; + +const mapStateToProps = (state) => ({ + numUnits: state.units.length, +}); + +export default connect(mapStateToProps, { addNotification, addUnit })(ImportUnitItem); diff --git a/client/src/components/AppBar/PdfDownloadItem.jsx b/client/src/components/AppBar/PdfDownloadItem.jsx new file mode 100644 index 00000000..4968b895 --- /dev/null +++ b/client/src/components/AppBar/PdfDownloadItem.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import { MenuItem } from '@material-ui/core'; +import { GetApp } from '@material-ui/icons'; +import { Link } from 'react-router-dom'; +import BetaTag from 'components/BetaTag'; + +const useStyles = makeStyles((theme) => ({ + menuItemIcon: { + marginRight: theme.spacing(1), + }, + item: { + color: theme.palette.getContrastText(theme.palette.background.paper), + }, + link: { + color: theme.palette.getContrastText(theme.palette.background.paper), + textDecoration: 'none', + }, +})); + +const PdfDownloadItem = ({ onClick, numUnits }) => { + const classes = useStyles(); + + const handleClick = () => { + onClick(); + }; + + const disabled = numUnits <= 0; + + return ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + + + Download PDF + + + + ); +}; + +PdfDownloadItem.propTypes = { + /** A callback function to call when the menu item is clicked */ + onClick: PropTypes.func.isRequired, + /** The current number of units. Used to disable the button */ + numUnits: PropTypes.number.isRequired, +}; + +const mapStateToProps = (state) => ({ + numUnits: state.units.length, +}); + +export default connect(mapStateToProps)(PdfDownloadItem); diff --git a/client/src/components/AppBar/ToggleDarkModeItem.jsx b/client/src/components/AppBar/ToggleDarkModeItem.jsx new file mode 100644 index 00000000..ad846f7f --- /dev/null +++ b/client/src/components/AppBar/ToggleDarkModeItem.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import { MenuItem, Typography } from '@material-ui/core'; +import { BrightnessMedium } from '@material-ui/icons'; +import { toggleDarkMode } from 'actions/config.action'; +import { connect } from 'react-redux'; + +const useStyles = makeStyles((theme) => ({ + menuItemIcon: { + marginRight: theme.spacing(1), + }, +})); + +const ToggleDarkModeItem = ({ onClick, toggleDarkMode }) => { + const classes = useStyles(); + + const handleClick = () => { + onClick(); + toggleDarkMode(); + }; + + return ( + + + Toggle Dark Mode  + + ); +}; + +ToggleDarkModeItem.propTypes = { + /** A callback function to call when the menu item is clicked */ + onClick: PropTypes.func.isRequired, + /** A function to call to toggle dark/light themes */ + toggleDarkMode: PropTypes.func.isRequired, +}; + +export default connect(null, { toggleDarkMode })(ToggleDarkModeItem); diff --git a/client/src/components/AppBar/ToggleGraphListItem.jsx b/client/src/components/AppBar/ToggleGraphListItem.jsx new file mode 100644 index 00000000..cc3c4cf3 --- /dev/null +++ b/client/src/components/AppBar/ToggleGraphListItem.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import { MenuItem } from '@material-ui/core'; +import { BarChart } from '@material-ui/icons'; +import { toggleDesktopGraphList } from 'actions/config.action'; +import { connect } from 'react-redux'; + +const useStyles = makeStyles((theme) => ({ + menuItemIcon: { + marginRight: theme.spacing(1), + }, + caption: { + paddingBottom: theme.spacing(1), + }, +})); + +const ToggleGraphListItem = ({ onClick, toggleDesktopGraphList }) => { + const classes = useStyles(); + + const handleClick = () => { + onClick(); + toggleDesktopGraphList(); + }; + + return ( + + + Toggle Graph List/Tabs + + ); +}; + +ToggleGraphListItem.propTypes = { + /** A callback function to call when the menu item is clicked */ + onClick: PropTypes.func.isRequired, + /** A function to call to toggle the desktop graph list */ + toggleDesktopGraphList: PropTypes.func.isRequired, +}; + +export default connect(null, { toggleDesktopGraphList })(ToggleGraphListItem); diff --git a/client/src/components/BetaTag/BetaTag.jsx b/client/src/components/BetaTag/BetaTag.jsx new file mode 100644 index 00000000..88d9c9de --- /dev/null +++ b/client/src/components/BetaTag/BetaTag.jsx @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import { Typography } from '@material-ui/core'; +import clsx from 'clsx'; + +const useStyles = makeStyles((theme) => ({ + caption: ({ variant }) => ({ + marginLeft: theme.spacing(0.5), + paddingBottom: theme.spacing(1), + color: theme.palette.secondary[variant], + }), +})); + +const BetaTag = ({ className, variant }) => { + const classes = useStyles({ variant }); + + return ( + + Beta + + ); +}; + +BetaTag.defaultProps = { + className: null, + variant: 'default', +}; + +BetaTag.propTypes = { + className: PropTypes.string, + variant: PropTypes.oneOf(['default', 'light', 'dark']), +}; + +export default BetaTag; diff --git a/client/src/components/BetaTag/index.jsx b/client/src/components/BetaTag/index.jsx new file mode 100644 index 00000000..7821d61a --- /dev/null +++ b/client/src/components/BetaTag/index.jsx @@ -0,0 +1 @@ +export { default } from './BetaTag'; diff --git a/client/src/components/Footer/Footer.jsx b/client/src/components/Footer/Footer.jsx index 3c3f77eb..0428c9a9 100644 --- a/client/src/components/Footer/Footer.jsx +++ b/client/src/components/Footer/Footer.jsx @@ -1,9 +1,9 @@ import React from 'react'; import { makeStyles, useTheme } from '@material-ui/core/styles'; import { - Typography, Paper, useMediaQuery, Button, + Typography, Paper, useMediaQuery, Button, IconButton, } from '@material-ui/core'; -import { GitHub } from '@material-ui/icons'; +import { GitHub, Reddit } from '@material-ui/icons'; import clsx from 'clsx'; const useStyles = makeStyles((theme) => ({ @@ -22,6 +22,18 @@ const useStyles = makeStyles((theme) => ({ mobileActions: { justifyContent: 'flex-start', padding: theme.spacing(1.5, 0), + [theme.breakpoints.down('xs')]: { + padding: theme.spacing(1, 0, 0), + }, + }, + footerButton: { + marginRight: theme.spacing(1), + [theme.breakpoints.down('xs')]: { + marginRight: theme.spacing(0.5), + }, + '&:last-child': { + marginRight: 0, + }, }, })); @@ -32,6 +44,7 @@ const Footer = () => { const classes = useStyles(); const theme = useTheme(); const mobile = useMediaQuery(theme.breakpoints.down('sm')); + const xs = useMediaQuery(theme.breakpoints.down('xs')); return (
@@ -48,16 +61,49 @@ const Footer = () => { component="div" className={clsx(classes.Actions, mobile ? classes.mobileActions : null)} > - + {!xs ? ( +
+ + +
+ ) : ( +
+ + + + + + +
+ )}
diff --git a/client/src/components/Graphs/BarGraph.jsx b/client/src/components/Graphs/BarGraph.jsx index 6db97d50..316e49d1 100644 --- a/client/src/components/Graphs/BarGraph.jsx +++ b/client/src/components/Graphs/BarGraph.jsx @@ -18,12 +18,17 @@ const useStyles = makeStyles({ * A bar graph component for the average damage results */ const BarGraph = ({ - results, unitNames, className, + results, unitNames, className, isAnimationActive, }) => { const classes = useStyles(); const theme = useTheme(); const xAxisLabel = (value) => (value === 'None' ? '-' : `${value}+`); + const formatLegendEntry = (value) => ( + + {value} + + ); return ( @@ -41,13 +46,14 @@ const BarGraph = ({ /> } cursor={{ fill: theme.palette.graphs.grid }} /> - + {unitNames.map((name, index) => ( ))} @@ -58,6 +64,7 @@ const BarGraph = ({ BarGraph.defaultProps = { results: [], className: null, + isAnimationActive: true, }; BarGraph.propTypes = { @@ -67,6 +74,8 @@ BarGraph.propTypes = { unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, /** CSS classname to give the component */ className: PropTypes.string, + /** Whether the play animations for the components */ + isAnimationActive: PropTypes.bool, }; export default BarGraph; diff --git a/client/src/components/Graphs/GraphContainer.jsx b/client/src/components/Graphs/GraphContainer.jsx index b700f531..a2ebf92d 100644 --- a/client/src/components/Graphs/GraphContainer.jsx +++ b/client/src/components/Graphs/GraphContainer.jsx @@ -3,15 +3,24 @@ import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import clsx from 'clsx'; import { ResponsiveContainer } from 'recharts'; +import { Typography } from '@material-ui/core'; -const useSyles = makeStyles({ +const useSyles = makeStyles((theme) => ({ container: { width: '100%', height: '100%', + display: 'flex', + flexDirection: 'column', }, - responsive: {}, -}); + responsive: { + display: 'flex', + }, + title: { + textAlign: 'center', + marginBottom: theme.spacing(1), + }, +})); /** * A wrapper for all of the graphs @@ -21,6 +30,7 @@ const GraphContainer = ({ className, children }) => { return (
+ Average Damage {children} diff --git a/client/src/components/Graphs/LineGraph.jsx b/client/src/components/Graphs/LineGraph.jsx index 3165e778..9d186ec9 100644 --- a/client/src/components/Graphs/LineGraph.jsx +++ b/client/src/components/Graphs/LineGraph.jsx @@ -17,18 +17,21 @@ const useStyles = makeStyles(() => ({ * A line graph component for the average damage results */ const LineGraph = ({ - results, unitNames, className, + results, unitNames, className, isAnimationActive, }) => { const classes = useStyles(); const theme = useTheme(); const xAxisLabel = (value) => (value === 'None' ? '-' : `${value}+`); + const formatLegendEntry = (value) => ( + + {value} + + ); return ( - + @@ -40,7 +43,7 @@ const LineGraph = ({ /> } /> - + {unitNames.map((name, index) => ( ))} @@ -59,6 +63,7 @@ const LineGraph = ({ LineGraph.defaultProps = { results: [], className: null, + isAnimationActive: true, }; LineGraph.propTypes = { @@ -68,6 +73,8 @@ LineGraph.propTypes = { unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, /** CSS classname to give the component */ className: PropTypes.string, + /** Whether the play animations for the components */ + isAnimationActive: PropTypes.bool, }; diff --git a/client/src/components/Graphs/RadarGraph.jsx b/client/src/components/Graphs/RadarGraph.jsx index 808c6249..f1a0c7ac 100644 --- a/client/src/components/Graphs/RadarGraph.jsx +++ b/client/src/components/Graphs/RadarGraph.jsx @@ -25,16 +25,21 @@ const useStyles = makeStyles({ * A radar graph component for the average damage results */ const RadarGraph = ({ - results, unitNames, className, + results, unitNames, className, outerRadius, isAnimationActive, }) => { const classes = useStyles(); const theme = useTheme(); const radarAxisLabel = (value) => (value === 'None' ? '-' : `${value}+`); + const formatLegendEntry = (value) => ( + + {value} + + ); return ( - + 100 ? '40%' : '45%'}> } /> - + {unitNames.map((name, index) => ( ))} @@ -64,6 +70,8 @@ const RadarGraph = ({ RadarGraph.defaultProps = { results: [], className: null, + outerRadius: 120, + isAnimationActive: true, }; RadarGraph.propTypes = { @@ -73,6 +81,9 @@ RadarGraph.propTypes = { unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, /** CSS classname to give the component */ className: PropTypes.string, + outerRadius: PropTypes.number, + /** Whether the play animations for the components */ + isAnimationActive: PropTypes.bool, }; diff --git a/client/src/components/ModifierItem/ModifierDescription.jsx b/client/src/components/ModifierItem/ModifierDescription.jsx index b20ecf44..003dcd94 100644 --- a/client/src/components/ModifierItem/ModifierDescription.jsx +++ b/client/src/components/ModifierItem/ModifierDescription.jsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import { Typography } from '@material-ui/core'; @@ -14,6 +14,25 @@ const useStyles = makeStyles({ }, }); +const getHtmlForValue = (key, value) => `${value}`; + +const getValue = (key, value, asHtml) => (asHtml ? getHtmlForValue(key, value) : value); + +const getFormattedDescription = (definition, options, asHtml = false) => { + const params = Object.keys(options).reduce((acc, key) => { + if (options[key] != null) { + if (definition.options[key].type === 'boolean') { + acc[key] = options[key] ? getValue(key, key, asHtml) : ''; + } else if (options[key] != null && options[key] !== '') { + acc[key] = getValue(key, options[key], asHtml); + } + } + return acc; + }, {}); + const desc = formatUnicorn(definition.description, params).trim().replace(/\s+/g, ' ').replace(/_/g, ' '); + return desc[0].toUpperCase() + desc.slice(1); +}; + /** * A component used to render the modifier description. It will use the definition description * as a base and substitute the current values into it @@ -21,29 +40,14 @@ const useStyles = makeStyles({ const ModifierDescription = React.memo(({ definition, options, className }) => { const classes = useStyles(); - const { description } = definition; - - const getHtmlForValue = (key, value) => `${value}`; - - const getFormattedDescription = useCallback(() => { - const params = Object.keys(options).reduce((acc, key) => { - if (options[key] != null) { - if (definition.options[key].type === 'boolean') { - acc[key] = options[key] ? getHtmlForValue(key, key) : ''; - } else if (options[key] != null && options[key] !== '') { - acc[key] = getHtmlForValue(key, options[key]); - } - } - return acc; - }, {}); - const desc = formatUnicorn(description, params).trim().replace(/\s+/g, ' ').replace(/_/g, ' '); - return desc[0].toUpperCase() + desc.slice(1); - }, [definition.options, description, options]); + const description = useMemo(() => ( + getFormattedDescription(definition, options, true) + ), [definition, options]); return ( {/* eslint-disable-next-line react/no-danger */} - + ); }, (prevProps, nextProps) => _.isEqual(prevProps, nextProps)); @@ -64,4 +68,4 @@ ModifierDescription.propTypes = { className: PropTypes.string, }; -export default ModifierDescription; +export { ModifierDescription as default, getFormattedDescription }; diff --git a/client/src/components/ModifierItem/ModifierItem.jsx b/client/src/components/ModifierItem/ModifierItem.jsx index 66668f84..8e8ad82c 100644 --- a/client/src/components/ModifierItem/ModifierItem.jsx +++ b/client/src/components/ModifierItem/ModifierItem.jsx @@ -6,6 +6,7 @@ import { makeStyles } from '@material-ui/core/styles'; import ListItem from 'components/ListItem'; import _ from 'lodash'; import { getModifierById } from 'utils/modifierHelpers'; +import { scrollToRef } from 'utils/scrollIntoView'; import ModifierInput from './ModifierInput'; import ModifierDescription from './ModifierDescription'; import { errorReducer } from './reducers'; @@ -44,7 +45,7 @@ const ModifierItem = React.memo(({ const definition = getModifierById(id); useEffect(() => { - if (itemRef.current) itemRef.current.scrollIntoView({ behavior: 'smooth' }); + scrollToRef(itemRef); }, [index]); useEffect(() => { diff --git a/client/src/containers/App/App.jsx b/client/src/containers/App/App.jsx index 95c7c91f..b0a0d9bf 100644 --- a/client/src/containers/App/App.jsx +++ b/client/src/containers/App/App.jsx @@ -10,6 +10,7 @@ import { Redirect, } from 'react-router-dom'; import CssBaseline from '@material-ui/core/CssBaseline'; +import PdfContainer from 'containers/PdfContainer'; import AppContentWrapper from './AppContentWrapper'; @@ -21,6 +22,7 @@ const App = ({ config }) => ( + diff --git a/client/src/containers/App/AppContentWrapper.jsx b/client/src/containers/App/AppContentWrapper.jsx index be334b6c..f54909e5 100644 --- a/client/src/containers/App/AppContentWrapper.jsx +++ b/client/src/containers/App/AppContentWrapper.jsx @@ -5,6 +5,7 @@ import { useMediaQuery } from '@material-ui/core'; import StoreSubscriber from 'components/StoreSubscriber'; import Footer from 'components/Footer'; import Notifications from 'components/Notifications'; +import { setAutoScrollEnabled, scrollToRef } from 'utils/scrollIntoView'; import DesktopAppContent from './DesktopAppContent'; import MobileAppContent from './MobileAppContent'; @@ -29,11 +30,13 @@ const AppContentWrapper = () => { const contentRef = useRef(null); useEffect(() => { - if (contentRef.current) { - setTimeout(() => { - contentRef.current.scrollIntoView({ behavior: 'smooth' }); - }, 750); - } + setAutoScrollEnabled(false); + setTimeout(() => { + scrollToRef(contentRef, true); + }, 500); + setTimeout(() => { + setAutoScrollEnabled(true); + }, 1000); }, []); return ( diff --git a/client/src/containers/App/MobileAppContent.jsx b/client/src/containers/App/MobileAppContent.jsx index 08e2f3c1..41a6cafe 100644 --- a/client/src/containers/App/MobileAppContent.jsx +++ b/client/src/containers/App/MobileAppContent.jsx @@ -1,7 +1,7 @@ import React, { useState, useCallback } from 'react'; import PropTypes from 'prop-types'; import Units, { AddUnitsFab } from 'containers/Units'; -import Stats from 'containers/Stats'; +import Stats, { ExportPdfFab } from 'containers/Stats'; import { makeStyles } from '@material-ui/core/styles'; import Tabbed from 'components/Tabbed'; import clsx from 'clsx'; @@ -21,6 +21,20 @@ const useStyles = makeStyles(() => ({ }, })); +const Fab = ({ activeIndex }) => { + switch (activeIndex) { + case 0: + return ; + case 1: + return ; + default: + return null; + } +}; + +Fab.propTypes = { + activeIndex: PropTypes.number.isRequired, +}; const MobileAppContent = ({ className }) => { const classes = useStyles(); @@ -29,7 +43,7 @@ const MobileAppContent = ({ className }) => { return (
- {activeTab === 0 && } + ({ + content: { + height: '350px', + paddingTop: 0, + overflow: 'hidden', + flexBasis: '50%', + }, +})); + +const GraphList = ({ stats, unitNames, graphMap }) => { + const classes = useStyles(); + const firstLoad = (!stats.payload || !stats.payload.length) && stats.pending; + + return ( + + {[...graphMap].map(([name, Graph]) => ( + + + + + + ))} + + ); +}; + +GraphList.propTypes = { + /** The current state of the stats reducer. */ + stats: PropTypes.shape({ + pending: PropTypes.bool, + payload: PropTypes.arrayOf(PropTypes.object), + error: PropTypes.string, + }).isRequired, + /** An array containing the unit names */ + unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, + /** A mapping of Graph Name -> Graph Component, in render order */ + graphMap: PropTypes.instanceOf(Map).isRequired, +}; + +export default GraphList; diff --git a/client/src/containers/Graphs/GraphTabbed.jsx b/client/src/containers/Graphs/GraphTabbed.jsx new file mode 100644 index 00000000..3242b9df --- /dev/null +++ b/client/src/containers/Graphs/GraphTabbed.jsx @@ -0,0 +1,72 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import Tabbed from 'components/Tabbed'; +import { Paper } from '@material-ui/core'; +import ListItem from 'components/ListItem'; +import GraphWrapper from './GraphWrapper'; + +const useStyles = makeStyles((theme) => ({ + tabs: { + margin: '-1em -1em 0', + }, + tab: { + padding: '1em 1em 0', + }, + content: { + height: '350px', + paddingTop: 0, + overflow: 'hidden', + flexBasis: '50%', + }, +})); + +const GraphTabbed = ({ stats, unitNames, graphMap }) => { + const classes = useStyles(); + const firstLoad = (!stats.payload || !stats.payload.length) && stats.pending; + + return ( + + ( + + + + + + ))} + /> + + ); +}; + +GraphTabbed.propTypes = { + /** The current state of the stats reducer. */ + stats: PropTypes.shape({ + pending: PropTypes.bool, + payload: PropTypes.arrayOf(PropTypes.object), + error: PropTypes.string, + }).isRequired, + /** An array containing the unit names */ + unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, + /** A mapping of Graph Name -> Graph Component, in render order */ + graphMap: PropTypes.instanceOf(Map).isRequired, +}; + +export default GraphTabbed; diff --git a/client/src/containers/Graphs/GraphWrapper.jsx b/client/src/containers/Graphs/GraphWrapper.jsx new file mode 100644 index 00000000..62f2c7d0 --- /dev/null +++ b/client/src/containers/Graphs/GraphWrapper.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import GraphSkeleton from 'components/Skeletons/GraphSkeleton'; +import StatsErrorCard from 'components/StatsErrorCard'; + +const useStyles = makeStyles((theme) => ({ + loader: { + padding: '2em', + }, + error: { + height: '350px', + width: 'auto', + margin: theme.spacing(2, 2, 0), + }, +})); + +const GraphWrapper = ({ + loading, error, children, numUnits, +}) => { + const classes = useStyles(); + const groups = numUnits || 1; + + if (loading) { + return ; + } + if (error) { + return ; + } + return
{children}
; +}; + +GraphWrapper.defaultProps = { + loading: false, + error: null, + numUnits: 1, +}; + +GraphWrapper.propTypes = { + /** Whether the child components are busy loading */ + loading: PropTypes.bool, + /** Whether there was an error or not */ + error: PropTypes.string, + /** The child components to render */ + children: PropTypes.node.isRequired, + /** The number of units (used to generate the appropriate skeleton) */ + numUnits: PropTypes.number, +}; + +export default GraphWrapper; diff --git a/client/src/containers/Graphs/Graphs.jsx b/client/src/containers/Graphs/Graphs.jsx new file mode 100644 index 00000000..a14ceef6 --- /dev/null +++ b/client/src/containers/Graphs/Graphs.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { useTheme } from '@material-ui/core/styles'; +import { BarGraph, LineGraph, RadarGraph } from 'components/Graphs'; +import { useMediaQuery } from '@material-ui/core'; +import GraphList from './GraphList'; +import GraphTabbed from './GraphTabbed'; + +/** A mapping of Graph Name -> Graph Component, in render order */ +const graphMap = new Map([ + ['Bar Graph', BarGraph], + ['Line Graph', LineGraph], + ['Radar Graph', RadarGraph], +]); + +const Graphs = ({ stats, unitNames, desktopGraphList }) => { + const theme = useTheme(); + const mobile = useMediaQuery(theme.breakpoints.down('sm')); + + return mobile || desktopGraphList + ? ( + + ) + : ( + + ); +}; + +Graphs.defaultProps = { + desktopGraphList: false, +}; + +Graphs.propTypes = { + /** The current state of the stats reducer. */ + stats: PropTypes.shape({ + pending: PropTypes.bool, + payload: PropTypes.arrayOf(PropTypes.object), + error: PropTypes.string, + }).isRequired, + /** An array containing the unit names */ + unitNames: PropTypes.arrayOf(PropTypes.string).isRequired, + /** Whether the graphs should be rendered as a list on desktop */ + desktopGraphList: PropTypes.bool, +}; + +const mapStateToProps = (state) => ({ + desktopGraphList: state.config.desktopGraphList, +}); + +export default connect(mapStateToProps)(Graphs); diff --git a/client/src/containers/Graphs/index.jsx b/client/src/containers/Graphs/index.jsx new file mode 100644 index 00000000..ec86155e --- /dev/null +++ b/client/src/containers/Graphs/index.jsx @@ -0,0 +1 @@ +export { default } from './Graphs'; diff --git a/client/src/containers/PdfContainer/PdfContainer.jsx b/client/src/containers/PdfContainer/PdfContainer.jsx new file mode 100644 index 00000000..a9ed9e8d --- /dev/null +++ b/client/src/containers/PdfContainer/PdfContainer.jsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { connect } from 'react-redux'; +import { fetchStatsCompare, fetchModifiers } from 'api'; +import { bindActionCreators } from 'redux'; +import PdfGenerator from 'pdf'; + +const applyMapping = (mapping, results) => ( + results.map((result) => Object.keys(result).reduce((acc, key) => { + if (key == null || key === 'save') return acc; + const name = mapping[key]; + if (name) acc[name] = result[key]; + return acc; + }, { save: result.save })) +); + +const PdfContainer = ({ + units, statsPending, payload, modifiersPending, modifiers, fetchStatsCompare, fetchModifiers, +}) => { + const [results, setResults] = useState(null); + + const nameMapping = useMemo(() => ( + units.reduce((acc, { uuid, name }) => { acc[uuid] = name; return acc; }, {}) + ), [units]); + + useEffect(() => { + fetchStatsCompare(); + fetchModifiers(); + }, [fetchStatsCompare, fetchModifiers]); + + useEffect(() => { + if (!statsPending && payload && payload.length) { + const mappedResults = applyMapping(nameMapping, payload); + setResults(mappedResults); + } + }, [statsPending, payload, nameMapping]); + + if (!modifiers || !modifiers.length || !results || !results.length) { + return null; + } + return ; +}; + + +const mapStateToProps = (state) => ({ + units: state.units, + statsPending: state.stats.pending, + payload: state.stats.payload, + modifiersPending: state.modifiers.pending, + modifiers: state.modifiers.modifiers, +}); + +const mapDispatchToProps = (dispatch) => bindActionCreators({ + fetchStatsCompare, + fetchModifiers, +}, dispatch); + +export default connect(mapStateToProps, mapDispatchToProps)(PdfContainer); diff --git a/client/src/containers/PdfContainer/index.jsx b/client/src/containers/PdfContainer/index.jsx new file mode 100644 index 00000000..8c38d9bc --- /dev/null +++ b/client/src/containers/PdfContainer/index.jsx @@ -0,0 +1 @@ +export { default } from './PdfContainer'; diff --git a/client/src/containers/ProfileDialog/DialogContent.jsx b/client/src/containers/ProfileDialog/DialogContent.jsx index 8e419b2e..2c82b54b 100644 --- a/client/src/containers/ProfileDialog/DialogContent.jsx +++ b/client/src/containers/ProfileDialog/DialogContent.jsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import PropTypes from 'prop-types'; import { - Typography, DialogContent as Content, + Typography, DialogContent as Content, TextField, } from '@material-ui/core'; import ModifierList from 'components/ModifierList'; import { makeStyles } from '@material-ui/core/styles'; @@ -38,6 +38,9 @@ const useStyles = makeStyles((theme) => ({ display: 'flex', flexWrap: 'wrap', }, + name: { + marginBottom: theme.spacing(2), + }, characteristics: { display: 'flex', flexDirection: 'row', @@ -70,6 +73,14 @@ const DialogContent = React.memo(({
{ onSubmit(); e.preventDefault(); }}> +
@@ -144,6 +155,7 @@ DialogContent.defaultProps = { DialogContent.propTypes = { profile: PropTypes.shape({ + name: PropTypes.string, num_models: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, attacks: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, to_hit: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, diff --git a/client/src/containers/ProfileDialog/ProfileDialog.jsx b/client/src/containers/ProfileDialog/ProfileDialog.jsx index e76a8ca0..21d2df82 100644 --- a/client/src/containers/ProfileDialog/ProfileDialog.jsx +++ b/client/src/containers/ProfileDialog/ProfileDialog.jsx @@ -47,6 +47,7 @@ const ProfileDialog = ({ editWeaponProfile, open }) => { } const [state, dispatchState] = useReducer(profileReducer, { + name: '', num_models: 1, attacks: 1, to_hit: 4, diff --git a/client/src/containers/ProfileDialog/reducers.js b/client/src/containers/ProfileDialog/reducers.js index 2fd049a2..8d41b740 100644 --- a/client/src/containers/ProfileDialog/reducers.js +++ b/client/src/containers/ProfileDialog/reducers.js @@ -57,6 +57,7 @@ export const profileReducer = (state, action) => { switch (action.type) { case 'INIT_PROFILE': return { + name: action.profile.name, num_models: action.profile.num_models, attacks: action.profile.attacks, to_hit: action.profile.to_hit, diff --git a/client/src/containers/Stats/ExportPdfFab.jsx b/client/src/containers/Stats/ExportPdfFab.jsx new file mode 100644 index 00000000..da981895 --- /dev/null +++ b/client/src/containers/Stats/ExportPdfFab.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import FloatingButton from 'components/FloatingButton'; +import GetAppIcon from '@material-ui/icons/GetApp'; +import { useHistory } from 'react-router-dom'; + +const ExportPdfFab = ({ numUnits }) => { + const history = useHistory(); + return ( + history.push('/pdf')} + icon={} + disabled={numUnits <= 0} + /> + ); +}; + +const mapStateToProps = (state) => ({ + numUnits: state.units.length, +}); + +export default connect(mapStateToProps)(ExportPdfFab); diff --git a/client/src/containers/Stats/Graphs.jsx b/client/src/containers/Stats/Graphs.jsx deleted file mode 100644 index f7e11aff..00000000 --- a/client/src/containers/Stats/Graphs.jsx +++ /dev/null @@ -1,145 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import { makeStyles, useTheme } from '@material-ui/core/styles'; -import Tabbed from 'components/Tabbed'; -import { BarGraph, LineGraph, RadarGraph } from 'components/Graphs'; -import GraphSkeleton from 'components/Skeletons/GraphSkeleton'; -import { useMediaQuery, Typography, Paper } from '@material-ui/core'; -import ListItem from 'components/ListItem'; -import StatsErrorCard from 'components/StatsErrorCard'; - -const useStyles = makeStyles((theme) => ({ - graphContainer: {}, - tabs: { - margin: '-1em -1em 0', - }, - tab: { - padding: '1em 1em 0', - }, - content: { - height: '350px', - paddingTop: '2em', - overflow: 'hidden', - flexBasis: '50%', - }, - loader: { - padding: '2em', - }, - error: { - height: '350px', - width: 'auto', - margin: theme.spacing(2, 2, 0), - }, -})); - -const graphNames = ['Line Graph', 'Bar Graph', 'Radar Graph']; -const graphList = [LineGraph, BarGraph, RadarGraph]; - -const GraphWrapper = ({ - loading, error, children, numUnits, -}) => { - const classes = useStyles(); - const groups = numUnits || 1; - - if (loading) { - return ; - } - if (error) { - return ; - } - return
{children}
; -}; - -const GraphTabbed = ({ stats, unitNames, graphList }) => { - const classes = useStyles(); - const firstLoad = (!stats.payload || !stats.payload.length) && stats.pending; - - return ( - - ( - - - - - - ))} - /> - - ); -}; - -const GraphList = ({ stats, unitNames, graphList }) => { - const classes = useStyles(); - const firstLoad = (!stats.payload || !stats.payload.length) && stats.pending; - - return ( - - {graphList.map((Graph, index) => ( - - - - - - ))} - - ); -}; - -const Graphs = ({ stats, unitNames, config }) => { - const theme = useTheme(); - const mobile = useMediaQuery(theme.breakpoints.down('sm')); - - return mobile || config.desktopGraphList - ? ( - - ) - : ( - - ); -}; - -const mapStateToProps = (state) => ({ - config: state.config, -}); - -export default connect(mapStateToProps)(Graphs); diff --git a/client/src/containers/Stats/Results.jsx b/client/src/containers/Stats/Results.jsx index 6dbf3552..0334cb87 100644 --- a/client/src/containers/Stats/Results.jsx +++ b/client/src/containers/Stats/Results.jsx @@ -1,13 +1,15 @@ import React from 'react'; -import { makeStyles } from '@material-ui/core/styles'; +import { makeStyles, useTheme } from '@material-ui/core/styles'; import clsx from 'clsx'; -import { Typography } from '@material-ui/core'; +import { Typography, Button, useMediaQuery } from '@material-ui/core'; import ListItem from 'components/ListItem'; import _ from 'lodash'; -import Graphs from './Graphs'; +import Graphs from 'containers/Graphs'; +import { Link } from 'react-router-dom'; +import { GetApp } from '@material-ui/icons'; +import BetaTag from 'components/BetaTag'; import ResultsTable from './ResultsTable'; - const useStyles = makeStyles({ results: { flexDirection: 'column', @@ -20,7 +22,11 @@ const useStyles = makeStyles({ const Results = React.memo(({ stats, unitNames, className }) => { const classes = useStyles(); + const theme = useTheme(); const firstLoad = (!stats.payload || !stats.payload.length) && stats.pending; + const mobile = useMediaQuery(theme.breakpoints.down('sm')); + + const downloadDisabled = unitNames.length <= 0; return ( @@ -33,6 +39,22 @@ const Results = React.memo(({ stats, unitNames, className }) => { + {!mobile + && ( + // eslint-disable-next-line jsx-a11y/anchor-is-valid + + + + )} ); }, (prevProps, nextProps) => _.isEqual(prevProps, nextProps)); diff --git a/client/src/containers/Stats/ResultsTable.jsx b/client/src/containers/Stats/ResultsTable.jsx index 69d95b09..9f6a5a09 100644 --- a/client/src/containers/Stats/ResultsTable.jsx +++ b/client/src/containers/Stats/ResultsTable.jsx @@ -20,12 +20,13 @@ const useStyles = makeStyles((theme) => ({ position: 'sticky', left: 0, zIndex: 11, + backgroundColor: theme.palette.background.nested, }, header: { fontWeight: theme.typography.fontWeightBold, }, cell: { - background: theme.palette.background.palette, + background: theme.palette.background.nested, }, error: { width: 'auto', diff --git a/client/src/containers/Stats/index.jsx b/client/src/containers/Stats/index.jsx index cbde4ea6..41eb5f84 100644 --- a/client/src/containers/Stats/index.jsx +++ b/client/src/containers/Stats/index.jsx @@ -1 +1,2 @@ export { default } from './Stats'; +export { default as ExportPdfFab } from './ExportPdfFab'; diff --git a/client/src/containers/Stats/stories/Stats.story.js b/client/src/containers/Stats/stories/Stats.story.js index 3821677e..571ea92e 100644 --- a/client/src/containers/Stats/stories/Stats.story.js +++ b/client/src/containers/Stats/stories/Stats.story.js @@ -1,7 +1,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import Results from 'containers/Stats/Results'; -import Graphs from 'containers/Stats/Graphs'; +import Graphs from 'containers/Graphs'; import ResultsTable from 'containers/Stats/ResultsTable'; import { boolean } from '@storybook/addon-knobs'; import { withProvider } from 'utils/exampleStore'; diff --git a/client/src/containers/Unit/Unit.jsx b/client/src/containers/Unit/Unit.jsx index e11e38fd..25f9f873 100644 --- a/client/src/containers/Unit/Unit.jsx +++ b/client/src/containers/Unit/Unit.jsx @@ -16,7 +16,7 @@ import clsx from 'clsx'; import NoItemsCard from 'components/NoItemsCard'; import { addUnitEnabled } from 'utils/unitHelpers'; import _ from 'lodash'; - +import { scrollToRef } from 'utils/scrollIntoView'; const useStyles = makeStyles((theme) => ({ unit: { @@ -47,7 +47,7 @@ const Unit = React.memo(({ const classes = useStyles(); useEffect(() => { - if (unitRef.current) unitRef.current.scrollIntoView({ behavior: 'smooth' }); + scrollToRef(unitRef); }, [unit.uuid]); const handleDeleteUnit = useCallback((id) => { diff --git a/client/src/containers/WeaponProfile/WeaponProfile.jsx b/client/src/containers/WeaponProfile/WeaponProfile.jsx index 96adcb56..7cdb240f 100644 --- a/client/src/containers/WeaponProfile/WeaponProfile.jsx +++ b/client/src/containers/WeaponProfile/WeaponProfile.jsx @@ -16,6 +16,7 @@ import { useHistory } from 'react-router-dom'; import { getUnitByPosition } from 'utils/unitHelpers'; import { Delete, FileCopy, Edit } from '@material-ui/icons'; import _ from 'lodash'; +import { scrollToRef } from 'utils/scrollIntoView'; import Characteristics from './Characteristics'; const useStyles = makeStyles((theme) => ({ @@ -57,7 +58,7 @@ const WeaponProfile = React.memo(({ // Scroll to the component when it is first created useEffect(() => { - if (profileRef.current) profileRef.current.scrollIntoView({ behavior: 'smooth' }); + scrollToRef(profileRef); }, [profile.uuid]); /** Handle open/close of the edit profile dialog */ @@ -85,11 +86,13 @@ const WeaponProfile = React.memo(({ /** Move this profile down by one space */ const moveProfileDown = () => { moveWeaponProfile(id, id + 1, unitId); }; + const header = `Weapon Profile ${profile.name ? `(${profile.name})` : ''}`; + return (
}, { @@ -134,6 +137,7 @@ WeaponProfile.propTypes = { id: PropTypes.number.isRequired, profile: PropTypes.shape({ uuid: PropTypes.string.isRequired, + name: PropTypes.string, modifiers: PropTypes.array, active: PropTypes.bool, }).isRequired, diff --git a/client/src/hooks/index.jsx b/client/src/hooks/index.jsx index 6b1c6842..f7bb9096 100644 --- a/client/src/hooks/index.jsx +++ b/client/src/hooks/index.jsx @@ -1,2 +1,3 @@ export { default as useLongPress } from './useLongPress'; export { default as usePrevious } from './usePrevious'; +export { default as useRefCallback } from './useRefCallback'; diff --git a/client/src/hooks/useRefCallback.jsx b/client/src/hooks/useRefCallback.jsx new file mode 100644 index 00000000..b82a3fc3 --- /dev/null +++ b/client/src/hooks/useRefCallback.jsx @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +const useRefCallback = (callback) => { + const [node, setRef] = useState(null); + + useEffect(() => { + if (node) { + callback(node); + } + }, [callback, node]); + + return [setRef]; +}; + +export default useRefCallback; diff --git a/client/src/pdf/GraphWrapper.jsx b/client/src/pdf/GraphWrapper.jsx new file mode 100644 index 00000000..b4efcc61 --- /dev/null +++ b/client/src/pdf/GraphWrapper.jsx @@ -0,0 +1,32 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles } from '@material-ui/core/styles'; +import clsx from 'clsx'; + +const useStyles = makeStyles((theme) => ({ + container: { + width: '1000px', + height: '100%', + background: theme.palette.background.paper, + }, + graph: { + height: '400px', + }, +})); + +const GraphWrapper = ({ children }) => { + const classes = useStyles(); + return ( +
+
+ {children} +
+
+ ); +}; + +GraphWrapper.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default GraphWrapper; diff --git a/client/src/pdf/PdfGenerator.jsx b/client/src/pdf/PdfGenerator.jsx new file mode 100644 index 00000000..030fb986 --- /dev/null +++ b/client/src/pdf/PdfGenerator.jsx @@ -0,0 +1,117 @@ +import React, { + useMemo, useLayoutEffect, useCallback, useState, +} from 'react'; +import PropTypes from 'prop-types'; +import { makeStyles, useTheme, ThemeProvider } from '@material-ui/core/styles'; +import { useMediaQuery } from '@material-ui/core'; +import { BarGraph, LineGraph, RadarGraph } from 'components/Graphs'; +import { useHistory } from 'react-router-dom'; +import { useRefCallback } from 'hooks'; +import { lightTheme } from 'themes'; +import generate from './generator'; +import PdfLoader from './PdfLoader'; +import GraphWrapper from './GraphWrapper'; + + +const useStyles = makeStyles(() => ({ + hidden: { + width: '100%', + position: 'absolute', + left: -2000, + }, + graphGroup: { + display: 'flex', + height: '100%', + width: '100%', + }, + line: { + flex: 2, + }, + radar: { + flex: 1, + }, + iframe: { + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + height: '100%', + width: '100%', + }, +})); + +const PdfGenerator = ({ units, results, modifiers }) => { + const classes = useStyles(); + const theme = useTheme(); + const history = useHistory(); + const [doc, setDoc] = useState(null); + const [loading, setLoading] = useState(true); + + const mobile = useMediaQuery(theme.breakpoints.down('sm')); + const unitNames = useMemo(() => units.map(({ name }) => name), [units]); + + const generatePdf = useCallback(() => ( + generate('pdf-copy', units, results, modifiers, unitNames) + ), [modifiers, results, unitNames, units]); + + const refCallback = useCallback(() => { + setTimeout(() => { + setLoading(false); + }, 1000); // Wait for recharts to finish drawing + }, []); + const [ref] = useRefCallback(refCallback); + + useLayoutEffect(() => { + if (!loading) { + generatePdf().then((result) => { + setDoc(result); + if (mobile) { + result.save('aos-statshammer.pdf'); + history.goBack(); + } + }); + } + }, [generatePdf, history, loading, mobile]); + + if (doc) { + // eslint-disable-next-line jsx-a11y/iframe-has-title + return