Login/Logout + Menu + i18next

This commit is contained in:
2024-07-25 01:09:20 +02:00
parent dd48f72c42
commit 65848f094b
17 changed files with 1042 additions and 211 deletions
@@ -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;