Login/Logout + Menu + i18next
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
import useGuestBookStore from '../../../store/store';
|
||||
import handleError from '../../../utils/errors';
|
||||
|
||||
const Login: FC = () => {
|
||||
const [error, setError] = useState();
|
||||
|
||||
const setUser = useGuestBookStore((state) => state.setUser);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
email: '',
|
||||
password: '',
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
setUser(await Api.logIn(value.email, value.password));
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
}}
|
||||
noValidate
|
||||
>
|
||||
<Box sx={{ display: 'grid', gap: 1, padding: 1, minWidth: '100px' }}>
|
||||
<form.Field
|
||||
name="email"
|
||||
validators={{
|
||||
onChange: ({ value }) => (!value ? 'Email required' : undefined),
|
||||
onChangeAsyncDebounceMs: 500,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return value.includes('error') && 'No "error" allowed in email';
|
||||
},
|
||||
}}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
size="small"
|
||||
label={t('E-Mail')}
|
||||
required
|
||||
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Field
|
||||
name="password"
|
||||
validators={{
|
||||
onChange: ({ value }) => (!value ? 'Password required' : undefined),
|
||||
onChangeAsyncDebounceMs: 500,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return value.includes('error') && 'No "error" allowed in password';
|
||||
},
|
||||
}}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
size="small"
|
||||
label={t('Password')}
|
||||
required
|
||||
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit]) => (
|
||||
<>
|
||||
<Button type="submit" disabled={!canSubmit} variant="contained">
|
||||
{t('Log in')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<Typography color="error.main">{t(...handleError(error, 'login'))}</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,92 @@
|
||||
import { AccountCircle } from '@mui/icons-material';
|
||||
import { AppBar, Avatar, IconButton, Menu, MenuItem, Toolbar, Typography, useScrollTrigger } from '@mui/material';
|
||||
import { cloneElement, FC, ReactElement, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
import useGuestBookStore from '../../store/store';
|
||||
import Login from '../Forms/Login/Login';
|
||||
|
||||
const ElevationScroll = ({ children }: { children: ReactElement }) => {
|
||||
const trigger = useScrollTrigger({
|
||||
disableHysteresis: true,
|
||||
threshold: 0,
|
||||
});
|
||||
|
||||
return cloneElement(children, {
|
||||
elevation: trigger ? 4 : 0,
|
||||
});
|
||||
};
|
||||
|
||||
const Header: FC = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const user = useGuestBookStore((state) => state.user);
|
||||
const setUser = useGuestBookStore((state) => state.setUser);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<ElevationScroll>
|
||||
<AppBar>
|
||||
<Toolbar>
|
||||
<Typography variant="h6" component="div" sx={{ flexGrow: 1 }}>
|
||||
{t('GuestBook')}
|
||||
</Typography>
|
||||
{user ? (
|
||||
<IconButton onClick={handleMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton size="large" onClick={handleMenu} color="inherit">
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
)}
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorEl)}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{user ? (
|
||||
[
|
||||
<MenuItem key="profile" onClick={handleClose}>
|
||||
{t('Profile')}
|
||||
</MenuItem>,
|
||||
<MenuItem
|
||||
key="logout"
|
||||
onClick={() => {
|
||||
Api.logOut();
|
||||
setUser(undefined);
|
||||
}}
|
||||
>
|
||||
{t('Log out')}
|
||||
</MenuItem>,
|
||||
]
|
||||
) : (
|
||||
<Login />
|
||||
)}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
</ElevationScroll>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
Reference in New Issue
Block a user