mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
replace class components (HOCs) with React Hooks
This commit is contained in:
@@ -3,13 +3,13 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@material-ui/core": "^4.11.4",
|
||||
"@material-ui/core": "^4.12.3",
|
||||
"@material-ui/icons": "^4.11.2",
|
||||
"@msgpack/msgpack": "^2.7.0",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^15.0.1",
|
||||
"@types/react": "^17.0.4",
|
||||
"@types/react-dom": "^17.0.3",
|
||||
"@types/lodash": "^4.14.172",
|
||||
"@types/node": "^12.20.20",
|
||||
"@types/react": "^17.0.19",
|
||||
"@types/react-dom": "^17.0.9",
|
||||
"@types/react-material-ui-form-validator": "^2.1.0",
|
||||
"@types/react-router": "^5.1.13",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
@@ -30,7 +30,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
"sockette": "^2.0.6",
|
||||
"typescript": "4.2.4",
|
||||
"typescript": "4.3.5",
|
||||
"zlib": "^1.0.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -22,25 +22,13 @@ interface UnauthenticatedRouteProps
|
||||
| React.ComponentType<any>;
|
||||
}
|
||||
|
||||
type RenderComponent = (props: RouteComponentProps<any>) => React.ReactNode;
|
||||
|
||||
class UnauthenticatedRoute extends Route<UnauthenticatedRouteProps> {
|
||||
public render() {
|
||||
const {
|
||||
authenticationContext,
|
||||
component: Component,
|
||||
features,
|
||||
...rest
|
||||
} = this.props;
|
||||
const renderComponent: RenderComponent = (props) => {
|
||||
const { authenticationContext, features, ...rest } = this.props;
|
||||
if (authenticationContext.me) {
|
||||
return <Redirect to={Authentication.fetchLoginRedirect(features)} />;
|
||||
}
|
||||
if (Component) {
|
||||
return <Component {...props} />;
|
||||
}
|
||||
};
|
||||
return <Route {...rest} render={renderComponent} />;
|
||||
return <Route {...rest} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
56
interface/src/components/FormLoader.tsx
Normal file
56
interface/src/components/FormLoader.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { FC } from 'react';
|
||||
|
||||
import { makeStyles, Theme, createStyles } from '@material-ui/core/styles';
|
||||
import { Button, LinearProgress, Typography } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
loadingSettings: {
|
||||
margin: theme.spacing(0.5)
|
||||
},
|
||||
loadingSettingsDetails: {
|
||||
margin: theme.spacing(4),
|
||||
textAlign: 'center'
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(2),
|
||||
marginTop: theme.spacing(2)
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
interface FormLoaderProps {
|
||||
errorMessage?: string;
|
||||
loadData: () => void;
|
||||
}
|
||||
|
||||
const FormLoader: FC<FormLoaderProps> = ({ errorMessage, loadData }) => {
|
||||
const classes = useStyles();
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<div className={classes.loadingSettings}>
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
{errorMessage}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
className={classes.button}
|
||||
onClick={loadData}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={classes.loadingSettings}>
|
||||
<LinearProgress className={classes.loadingSettingsDetails} />
|
||||
<Typography variant="h6" className={classes.loadingSettingsDetails}>
|
||||
Loading…
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormLoader;
|
||||
@@ -5,6 +5,7 @@ export { default as HighlightAvatar } from './HighlightAvatar';
|
||||
export { default as MenuAppBar } from './MenuAppBar';
|
||||
export { default as PasswordValidator } from './PasswordValidator';
|
||||
export { default as RestFormLoader } from './RestFormLoader';
|
||||
export { default as FormLoader } from './FormLoader';
|
||||
export { default as SectionContent } from './SectionContent';
|
||||
export { default as WebSocketFormLoader } from './WebSocketFormLoader';
|
||||
export { default as ErrorButton } from './ErrorButton';
|
||||
|
||||
@@ -1,58 +1,31 @@
|
||||
import { Component } from 'react';
|
||||
import { FC } from 'react';
|
||||
|
||||
import { Features } from './types';
|
||||
import { FeaturesContext } from './FeaturesContext';
|
||||
import FullScreenLoading from '../components/FullScreenLoading';
|
||||
import ApplicationError from '../components/ApplicationError';
|
||||
import { FEATURES_ENDPOINT } from '../api';
|
||||
import { useRest } from '../hooks';
|
||||
|
||||
interface FeaturesWrapperState {
|
||||
features?: Features;
|
||||
error?: string;
|
||||
}
|
||||
import { Features } from './types';
|
||||
import { FeaturesContext } from './FeaturesContext';
|
||||
|
||||
class FeaturesWrapper extends Component<{}, FeaturesWrapperState> {
|
||||
state: FeaturesWrapperState = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchFeaturesDetails();
|
||||
}
|
||||
|
||||
fetchFeaturesDetails = () => {
|
||||
fetch(FEATURES_ENDPOINT)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw Error('Unexpected status code: ' + response.status);
|
||||
}
|
||||
})
|
||||
.then((features) => {
|
||||
this.setState({ features });
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({ error: error.message });
|
||||
const FeaturesWrapper: FC = ({ children }) => {
|
||||
const { data: features, errorMessage: error } = useRest<Features>({
|
||||
endpoint: FEATURES_ENDPOINT
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { features, error } = this.state;
|
||||
if (features) {
|
||||
return (
|
||||
<FeaturesContext.Provider
|
||||
value={{
|
||||
features
|
||||
}}
|
||||
>
|
||||
{this.props.children}
|
||||
<FeaturesContext.Provider value={{ features }}>
|
||||
{children}
|
||||
</FeaturesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <ApplicationError error={error} />;
|
||||
}
|
||||
|
||||
return <FullScreenLoading />;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default FeaturesWrapper;
|
||||
|
||||
2
interface/src/hooks/index.ts
Normal file
2
interface/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as useRest } from './useRest';
|
||||
export { default as useAuthorizedRest } from './useAuthorizedRest';
|
||||
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
12
interface/src/hooks/useAuthorizedRest.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { redirectingAuthorizedFetch } from '../authentication';
|
||||
import useRest, { RestRequestOptions } from './useRest';
|
||||
|
||||
const useAuthorizedRest = <D>({
|
||||
endpoint
|
||||
}: Omit<RestRequestOptions, 'fetchFunction'>) =>
|
||||
useRest<D>({
|
||||
endpoint,
|
||||
fetchFunction: redirectingAuthorizedFetch
|
||||
});
|
||||
|
||||
export default useAuthorizedRest;
|
||||
79
interface/src/hooks/useRest.ts
Normal file
79
interface/src/hooks/useRest.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useSnackbar } from 'notistack';
|
||||
|
||||
export interface RestRequestOptions {
|
||||
endpoint: string;
|
||||
fetchFunction?: typeof fetch;
|
||||
}
|
||||
|
||||
const useRest = <D>({
|
||||
endpoint,
|
||||
fetchFunction = fetch
|
||||
}: RestRequestOptions) => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [data, setData] = useState<D>();
|
||||
const [errorMessage, setErrorMessage] = useState<string>();
|
||||
|
||||
const handleError = useCallback(
|
||||
(error: any) => {
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
enqueueSnackbar('Problem fetching: ' + errorMessage, {
|
||||
variant: 'error'
|
||||
});
|
||||
setErrorMessage(errorMessage);
|
||||
},
|
||||
[enqueueSnackbar]
|
||||
);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setData(undefined);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
const response = await fetchFunction(endpoint);
|
||||
if (response.status !== 200) {
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
setData(await response.json());
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
}, [handleError, fetchFunction, endpoint]);
|
||||
|
||||
const save = useCallback(
|
||||
async (data: D) => {
|
||||
setSaving(true);
|
||||
setErrorMessage(undefined);
|
||||
try {
|
||||
const response = await fetchFunction(endpoint, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
throw Error('Invalid status code: ' + response.status);
|
||||
}
|
||||
enqueueSnackbar('Update successful.', { variant: 'success' });
|
||||
setData(await response.json());
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[enqueueSnackbar, handleError, fetchFunction, endpoint]
|
||||
);
|
||||
|
||||
const saveData = () => data && save(data);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, [loadData]);
|
||||
|
||||
return { loadData, saveData, saving, setData, data, errorMessage } as const;
|
||||
};
|
||||
|
||||
export default useRest;
|
||||
33
interface/src/utils/binding.ts
Normal file
33
interface/src/utils/binding.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
type UpdateEntity<S> = (state: (prevState: Readonly<S>) => S) => void;
|
||||
|
||||
export const extractEventValue = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
switch (event.target.type) {
|
||||
case 'number':
|
||||
return event.target.valueAsNumber;
|
||||
case 'checkbox':
|
||||
return event.target.checked;
|
||||
default:
|
||||
return event.target.value;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[event.target.name]: extractEventValue(event)
|
||||
}));
|
||||
};
|
||||
|
||||
export const updateBooleanValue = <S>(updateEntity: UpdateEntity<S>) => (
|
||||
name: string,
|
||||
value?: boolean
|
||||
) => {
|
||||
updateEntity((prevState) => ({
|
||||
...prevState,
|
||||
[name]: value
|
||||
}));
|
||||
};
|
||||
1
interface/src/utils/index.ts
Normal file
1
interface/src/utils/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './binding';
|
||||
Reference in New Issue
Block a user