Commit 111112f1 authored by Florent Chehab's avatar Florent Chehab

refacto(Front): more hookification

parent 8bc54c74
......@@ -17,65 +17,80 @@ import {
settingsMenuItems
} from "./menuItems";
class DrawerMenu extends React.Component {
toListItem(items, inset = false) {
const { closeDrawer } = this.props;
const ListItemHeading = ({ label, Icon }) => (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={label} />
</ListItem>
);
return items.map(({ label, route, Icon }, idx) => (
ListItemHeading.propTypes = {
label: PropTypes.node.isRequired,
Icon: PropTypes.object.isRequired
};
const ListItemsTmp = ({ items, onClick, inset }) => (
<>
{items.map(({ label, route, Icon }, idx) => (
// eslint-disable-next-line react/no-array-index-key
<CustomNavLink key={idx} to={route} onClick={() => closeDrawer()}>
<ListItem button onClick={() => closeDrawer()}>
{Icon !== null ? (
<CustomNavLink key={idx} to={route} onClick={onClick}>
<ListItem button onClick={onClick}>
{Icon !== null && (
<ListItemIcon>
<Icon />
</ListItemIcon>
) : (
<></>
)}
<ListItemText primary={label} inset={inset} />
</ListItem>
</CustomNavLink>
));
}
))}
</>
);
ListItemsTmp.propTypes = {
items: PropTypes.array.isRequired,
onClick: PropTypes.func.isRequired,
inset: PropTypes.bool
};
toListItemBasic(label, Icon) {
return (
<ListItem>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={label} />
</ListItem>
);
}
ListItemsTmp.defaultProps = {
inset: false
};
render() {
const { closeDrawer, open } = this.props;
function DrawerMenu({ open, closeDrawer }) {
const ListItems = props => <ListItemsTmp {...props} onClick={closeDrawer} />;
return (
<div style={{ zIndex: 200000 }}>
<Drawer open={open} onClose={() => closeDrawer()}>
<List>{this.toListItem([mainMenuHome])}</List>
<Divider />
<List>{this.toListItem(mainMenuItems)}</List>
<Divider />
<List>{this.toListItem(secondaryMenuItems)}</List>
<Divider />
<List>
{this.toListItemBasic(<em>Informations</em>, InfoIcon)}
<Divider variant="inset" />
{this.toListItem(infoMenuItems, true)}
</List>
return (
<div style={{ zIndex: 200000 }}>
<Drawer open={open} onClose={closeDrawer}>
<List>
<ListItems items={[mainMenuHome]} />
</List>
<Divider />
<List>
<ListItems items={mainMenuItems} />
</List>
<Divider />
<List>
<ListItems items={secondaryMenuItems} />
<Divider />
<List>
{this.toListItemBasic(<em>Paramètres</em>, SettingsIcon)}
<Divider variant="inset" />
{this.toListItem(settingsMenuItems, true)}
</List>
</Drawer>
</div>
);
}
</List>
<List>
<ListItemHeading label={<em>Informations</em>} Icon={InfoIcon} />
<Divider variant="inset" />
<ListItems items={infoMenuItems} inset />
</List>
<Divider />
<List>
<ListItemHeading label={<em>Paramètres</em>} Icon={SettingsIcon} />
<Divider variant="inset" />
<ListItems items={settingsMenuItems} inset />
</List>
</Drawer>
</div>
);
}
DrawerMenu.propTypes = {
......@@ -83,4 +98,4 @@ DrawerMenu.propTypes = {
closeDrawer: PropTypes.func.isRequired
};
export default DrawerMenu;
export default React.memo(DrawerMenu);
import React from "react";
import PropTypes from "prop-types";
import { compose } from "recompose";
import { connect } from "react-redux";
import Alert from "./Alert";
import getActions from "../../redux/api/getActions";
import RequestParams from "../../redux/api/RequestParams";
import useDeleteOne from "../../hooks/useDeleteOne";
class DeleteHandler extends React.Component {
handleDelete() {
const { route, id, performDelete, performClose } = this.props;
performDelete(route, id, () => performClose());
}
function DeleteHandler({ performClose, route, id }) {
const performDelete = useDeleteOne(route);
render() {
const { performClose } = this.props;
return (
<Alert
open
info={false}
title="Confirmer la suppression de l'object."
description="Ếtes-vous sûr⋅e ?"
agreeText="Oui"
disagreeText="Non"
handleClose={() => performClose()}
handleResponse={confirmed => {
if (confirmed) this.handleDelete();
else performClose();
}}
/>
);
}
return (
<Alert
open
info={false}
title="Confirmer la suppression de l'object."
description="Ếtes-vous sûr⋅e ?"
agreeText="Oui"
disagreeText="Non"
handleClose={performClose}
handleResponse={confirmed => {
if (confirmed) performDelete(id, () => performClose());
else performClose();
}}
/>
);
}
DeleteHandler.propTypes = {
......@@ -39,24 +29,7 @@ DeleteHandler.propTypes = {
PropTypes.string.isRequired,
PropTypes.number.isRequired
]).isRequired,
performClose: PropTypes.func.isRequired,
performDelete: PropTypes.func.isRequired
performClose: PropTypes.func.isRequired
};
const mapDispatchToProps = dispatch => ({
performDelete: (route, id, onSuccess) =>
dispatch(
getActions(route).delete(
RequestParams.Builder.withId(id)
.withOnSuccessCallback(onSuccess)
.build()
)
)
});
export default compose(
connect(
() => ({}),
mapDispatchToProps
)
)(DeleteHandler);
export default DeleteHandler;
import React from "react";
import React, { useCallback, useState } from "react";
import Menu from "@material-ui/core/Menu";
import PropTypes from "prop-types";
import Fab from "@material-ui/core/Fab";
......@@ -15,72 +15,62 @@ const ForwardedNavLink = React.forwardRef((props, ref) => (
</div>
));
class IconWithMenu extends React.Component {
state = {
anchorEl: null
};
function IconWithMenu({ fabProps, Icon, menuItems, iconProps }) {
const [anchorEl, setAnchorEl] = useState(null);
handleOpenMenu = event => {
this.setState({ anchorEl: event.currentTarget });
};
const handleOpenMenu = useCallback(event => {
setAnchorEl(event.currentTarget);
}, []);
handleCloseMenu = () => {
this.setState({ anchorEl: null });
};
const handleCloseMenu = useCallback(() => {
setAnchorEl(null);
}, []);
render() {
const { anchorEl } = this.state;
const open = Boolean(anchorEl);
return (
<>
<Fab
size="medium"
color="inherit"
onClick={this.handleOpenMenu}
{...this.props.fabProps}
>
{<this.props.Icon {...this.props.iconProps} />}
</Fab>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "right"
}}
open={open}
onClose={this.handleCloseMenu}
>
{this.props.menuItems.map(({ label, route, hardRedirect }, idx) =>
hardRedirect ? (
<MenuItem
// eslint-disable-next-line react/no-array-index-key
key={idx}
component="a"
href={route}
onClick={this.handleCloseMenu}
>
{label}
</MenuItem>
) : (
<MenuItem
// eslint-disable-next-line react/no-array-index-key
key={idx}
component={ForwardedNavLink}
to={route}
onClick={this.handleCloseMenu}
>
{label}
</MenuItem>
)
)}
</Menu>
</>
);
}
const open = anchorEl !== null;
return (
<>
<Fab size="medium" color="inherit" onClick={handleOpenMenu} {...fabProps}>
{<Icon {...iconProps} />}
</Fab>
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
transformOrigin={{
vertical: "top",
horizontal: "right"
}}
open={open}
onClose={handleCloseMenu}
>
{menuItems.map(({ label, route, hardRedirect }, idx) =>
hardRedirect ? (
<MenuItem
// eslint-disable-next-line react/no-array-index-key
key={idx}
component="a"
href={route}
onClick={handleCloseMenu}
>
{label}
</MenuItem>
) : (
<MenuItem
// eslint-disable-next-line react/no-array-index-key
key={idx}
component={ForwardedNavLink}
to={route}
onClick={handleCloseMenu}
>
{label}
</MenuItem>
)
)}
</Menu>
</>
);
}
IconWithMenu.defaultProps = {
......@@ -95,4 +85,4 @@ IconWithMenu.propTypes = {
iconProps: PropTypes.object
};
export default IconWithMenu;
export default React.memo(IconWithMenu);
import React from "react";
import React, { useState } from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import CircularProgress from "@material-ui/core/CircularProgress";
import { makeStyles } from "@material-ui/styles";
import useInterval from "../../utils/useInterval";
const useStyles = makeStyles(theme => ({
progress: {
margin: theme.spacing(2)
}
}));
/**
* React component to display a loading indicator
*
* @class Loading
* @extends {React.Component}
*/
class Loading extends React.Component {
timer = null;
state = {
completionPercentage: 0
};
componentDidMount() {
this.timer = setInterval(this.progress.bind(this), 20);
}
componentWillUnmount() {
// Delete the intervaler on unmount
clearInterval(this.timer);
}
/**
* Make circular progress turn on its self in conjunction with componentDidMount
*
* @memberof Loading
*/
progress() {
const { completionPercentage } = this.state;
this.setState({
completionPercentage:
completionPercentage >= 100 ? 0 : completionPercentage + 1
});
}
render() {
const { classes, size } = this.props;
const { completionPercentage } = this.state;
return (
<CircularProgress
className={classes.progress}
variant="determinate"
size={size}
value={completionPercentage}
/>
);
}
function Loading({ size }) {
const classes = useStyles();
const [completion, setCompletion] = useState(0);
useInterval(() => {
setCompletion(completion >= 100 ? 0 : completion + 1);
}, 20);
return (
<CircularProgress
className={classes.progress}
variant="determinate"
size={size}
value={completion}
/>
);
}
Loading.propTypes = {
classes: PropTypes.object.isRequired,
size: PropTypes.number
};
......@@ -62,10 +39,4 @@ Loading.defaultProps = {
size: 50
};
const styles = theme => ({
progress: {
margin: theme.spacing(2)
}
});
export default withStyles(styles)(Loading);
export default Loading;
import React from "react";
import React, { useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import withStyles from "@material-ui/core/styles/withStyles";
import MobileStepper from "@material-ui/core/MobileStepper";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
......@@ -9,125 +8,110 @@ import Divider from "@material-ui/core/Divider";
import Button from "@material-ui/core/Button";
import KeyboardArrowLeft from "@material-ui/icons/KeyboardArrowLeft";
import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight";
import isEqual from "lodash/isEqual";
import { makeStyles } from "@material-ui/styles";
import range from "../../utils/range";
import APP_ROUTES from "../../config/appRoutes";
import CustomNavLink from "../common/CustomNavLink";
import useStepper from "../../hooks/useStepper";
const useStyles = makeStyles(theme => ({
root: {
maxWidth: 650,
marginLeft: "auto",
marginRight: "auto",
flexGrow: 1
},
header: {
display: "flex",
alignItems: "center",
height: 50,
paddingLeft: theme.spacing(4),
marginBottom: 20,
backgroundColor: theme.palette.background.default
}
}));
/**
* Component to provide a nice list of universities that can be swiped
*
* @class UnivList
* @extends {React.Component}
*/
class UnivList extends React.Component {
state = {
activeStep: 0
};
function UnivList({ universitiesToList, itemsPerPage }) {
const classes = useStyles();
const [activeStep, goNext, goBack, goTo] = useStepper(0);
componentDidUpdate(prevProps) {
useEffect(() => {
// If the list is not the same, then make sure to "reset" and move to the first page
if (!isEqual(prevProps.universitiesToList, this.props.universitiesToList)) {
this.handleStepChange(0);
}
}
goTo(0);
}, [universitiesToList]);
handleNext() {
this.setState(prevState => ({
activeStep: prevState.activeStep + 1
}));
}
handleBack() {
this.setState(prevState => ({
activeStep: prevState.activeStep - 1
}));
}
handleStepChange(activeStep) {
this.setState({ activeStep });
}
const numberOfItems = universitiesToList.length;
const maxSteps = Math.ceil(numberOfItems / itemsPerPage);
render() {
const { classes, theme, itemsPerPage, universitiesToList } = this.props;
const { activeStep } = this.state;
const numberOfItems = universitiesToList.length;
const maxSteps = Math.ceil(numberOfItems / itemsPerPage);
// Prevent bug
if (!numberOfItems) {
return <></>;
}
function getIndexesOnPage(page) {
const getIndexesOnPage = useCallback(
page => {
const firstIdxOnPage = page * itemsPerPage;
const lastIdxOnPage = Math.min((page + 1) * itemsPerPage, numberOfItems);
return range(lastIdxOnPage - firstIdxOnPage).map(
el => el + firstIdxOnPage
);
}
return (
<div className={classes.root}>
<List component="nav">
<Divider />
{getIndexesOnPage(activeStep).map(univIdx => (
<CustomNavLink
to={APP_ROUTES.forUniversity(universitiesToList[univIdx].id)}
key={univIdx}
>
<ListItem button divider key={univIdx}>
<ListItemText primary={universitiesToList[univIdx].name} />
</ListItem>
</CustomNavLink>
))}
</List>
},
[itemsPerPage, numberOfItems]
);
<MobileStepper
steps={maxSteps}
position="static"
variant="progress"
activeStep={activeStep}
className={classes.mobileStepper}
nextButton={
<Button
color="secondary"
size="small"
onClick={() => this.handleNext()}
disabled={activeStep >= maxSteps - 1}
>
Suivant
{theme.direction === "rtl" ? (
<KeyboardArrowLeft />
) : (
<KeyboardArrowRight />
)}
</Button>
}
backButton={
<Button
color="secondary"
size="small"
onClick={() => this.handleBack()}
disabled={activeStep === 0}
>
{theme.direction === "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
Précédent
</Button>
}
/>
</div>
);
// Prevent bug
if (!numberOfItems) {
return <></>;
}
return (
<div className={classes.root}>
<List component="nav">
<Divider />
{getIndexesOnPage(activeStep).map(univIdx => (
<CustomNavLink
to={APP_ROUTES.forUniversity(universitiesToList[univIdx].id)}
key={univIdx}
>
<ListItem button divider key={univIdx}>
<ListItemText primary={universitiesToList[univIdx].name} />
</ListItem>
</CustomNavLink>
))}
</List>
<MobileStepper
steps={maxSteps}
position="static"
variant="progress"
activeStep={activeStep}
className={classes.mobileStepper}
nextButton={
<Button
color="secondary"
size="small"
onClick={goNext}
disabled={activeStep >= maxSteps - 1}
>
Suivant
<KeyboardArrowRight />
</Button>
}
backButton={
<Button
color="secondary"
size="small"
onClick={goBack}
disabled={activeStep === 0}
>
<KeyboardArrowLeft />
Précédent
</Button>
}
/>
</div>
);
}
UnivList.propTypes = {
classes: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired,
universitiesToList: PropTypes.array.isRequired,
itemsPerPage: PropTypes.number
};
......@@ -136,21 +120,4 @@ UnivList.defaultProps = {
itemsPerPage: 10
};
const styles = theme => ({
root: {
maxWidth: 650,
marginLeft: "auto",
marginRight: "auto",
flexGrow: 1
},
header: {
display: "flex",
alignItems: "center",
height: 50,
paddingLeft: theme.spacing(4),
marginBottom: 20,
backgroundColor: theme.palette.background.default
}
});
export default withStyles(styles, { withTheme: true })(UnivList);
export default UnivList;
......@@ -15,6 +15,7 @@ import App from "../components/app/App";
import ThemeProvider from "../components/common/theme/ThemeProvider";
import SnackbarCloseButton from "../components/app/SnackbarCloseButton";
import NavigationService from "../services/NavigationService";
import OfflineThemeProvider from "../components/common/theme/OfflineThemeProvider";
/**
* Get the correct style for a color of notistack (not enough contrast some times with default settings)
......@@ -72,11 +73,14 @@ function MainReactEntry() {
return (
<Provider store={store}>
{/* <React.StrictMode> */}
<ThemeProvider>
<Router ref={NavigationService.setComponent}>
<SubEntry />
</Router>
</ThemeProvider>
<OfflineThemeProvider>
{/* We make sure to have at least one theme active, that's why there is an offline theme too */}
<ThemeProvider>
<Router ref={NavigationService.setComponent}>
<SubEntry />
</Router>
</ThemeProvider>
</OfflineThemeProvider>