Auth redirects, language switch, basic profile

This commit is contained in:
2024-07-26 01:14:12 +02:00
parent 2091bdb4e3
commit 36a4659915
31 changed files with 5638 additions and 331 deletions
@@ -4,13 +4,14 @@ import { useRouter } from '@tanstack/react-router';
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();
interface Props {
handleClose: () => void;
}
const setUser = useGuestBookStore((state) => state.setUser);
const LoginForm: FC<Props> = ({ handleClose }) => {
const [error, setError] = useState();
const { t } = useTranslation();
const router = useRouter();
@@ -22,9 +23,9 @@ const Login: FC = () => {
},
onSubmit: async ({ value }) => {
try {
const [user, token] = await Api.logIn(value.email, value.password);
setUser(user, token);
await Api.logIn(value.email, value.password);
router.invalidate();
handleClose();
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
@@ -126,4 +127,4 @@ const Login: FC = () => {
);
};
export default Login;
export default LoginForm;
+16 -57
View File
@@ -1,22 +1,20 @@
import { AccountCircle } from '@mui/icons-material';
import { AccountCircle, Translate } from '@mui/icons-material';
import {
AppBar,
Avatar,
Box,
CircularProgress,
IconButton,
Menu,
MenuItem,
Link as MUILink,
Toolbar,
useScrollTrigger,
} from '@mui/material';
import { Link, useRouter, useRouterState } from '@tanstack/react-router';
import { Link, useRouterState } from '@tanstack/react-router';
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';
import LanguageMenu from '../Menus/Language/LanguageMenu';
import UserMenu from '../Menus/User/UserMenu';
const ElevationScroll = ({ children }: { children: ReactElement }) => {
const trigger = useScrollTrigger({
@@ -30,21 +28,17 @@ const ElevationScroll = ({ children }: { children: ReactElement }) => {
};
const Header: FC = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const user = useGuestBookStore((state) => state.user);
const setUser = useGuestBookStore((state) => state.setUser);
const [anchorUserMenu, setAnchorUserMenu] = useState<null | HTMLElement>(null);
const [anchorLanguageMenu, setAnchorLanguageMenu] = useState<null | HTMLElement>(null);
const { t } = useTranslation();
const router = useRouter();
const isLoading = useRouterState({ select: (s) => s.status === 'pending' });
const handleMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const user = Api.getAuthenticatedUser();
const handleClose = () => {
setAnchorEl(null);
setAnchorLanguageMenu(null);
setAnchorUserMenu(null);
};
return (
@@ -58,55 +52,20 @@ const Header: FC = () => {
</MUILink>
{isLoading && <CircularProgress size={16} thickness={10} sx={{ color: 'white' }} />}
</Box>
<IconButton size="large" onClick={(event) => setAnchorLanguageMenu(event.currentTarget)}>
<Translate sx={{ color: 'white' }} />
</IconButton>
{user ? (
<IconButton onClick={handleMenu} sx={{ p: 0 }}>
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
<Avatar alt={user.username} src={`storage/${user.image}`} />
</IconButton>
) : (
<IconButton size="large" onClick={handleMenu} color="inherit">
<IconButton size="large" onClick={(event) => setAnchorUserMenu(event.currentTarget)} 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}
sx={{
'& .MuiMenu-paper': {
minWidth: '240px',
},
}}
>
{user ? (
[
<MenuItem key="profile" onClick={handleClose}>
{t('Profile')}
</MenuItem>,
<MenuItem
key="logout"
onClick={() => {
Api.logOut();
setUser();
router.invalidate();
}}
>
{t('Log out')}
</MenuItem>,
]
) : (
<Login />
)}
</Menu>
<LanguageMenu anchorEl={anchorLanguageMenu} handleClose={handleClose} />
<UserMenu anchorEl={anchorUserMenu} handleClose={handleClose} />
</Toolbar>
</AppBar>
<Toolbar />
@@ -0,0 +1,55 @@
import { Menu, MenuItem } from '@mui/material';
import { FC } from 'react';
import i18n from '../../../i18n';
interface Props {
anchorEl: HTMLElement | null;
handleClose: () => void;
}
const LanguageMenu: FC<Props> = ({ anchorEl, handleClose }) => {
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
sx={{
'& .MuiMenu-paper': {
minWidth: '240px',
},
}}
>
<MenuItem
key="de"
selected={i18n.language === 'en'}
onClick={() => {
i18n.changeLanguage('en');
handleClose();
}}
>
English
</MenuItem>
<MenuItem
key="en"
selected={i18n.language === 'de'}
onClick={() => {
i18n.changeLanguage('de');
handleClose();
}}
>
Deutsch
</MenuItem>
</Menu>
);
};
export default LanguageMenu;
@@ -0,0 +1,69 @@
import { Menu, MenuItem } from '@mui/material';
import { useNavigate, useRouter } from '@tanstack/react-router';
import { t } from 'i18next';
import { FC } from 'react';
import Api from '../../../api/Api';
import { ROUTES } from '../../../types/Routes';
import LoginForm from '../../Forms/Login/LoginForm';
interface Props {
anchorEl: HTMLElement | null;
handleClose: () => void;
}
const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
const navigate = useNavigate();
const router = useRouter();
const user = Api.getAuthenticatedUser();
return (
<Menu
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
sx={{
'& .MuiMenu-paper': {
minWidth: '240px',
},
}}
>
{user ? (
[
<MenuItem
key="profile"
onClick={() => {
navigate({ to: ROUTES.PROFILE });
handleClose();
}}
>
{t('Profile')}
</MenuItem>,
<MenuItem
key="logout"
onClick={() => {
Api.logOut();
router.invalidate();
handleClose();
}}
>
{t('Log out')}
</MenuItem>,
]
) : (
<LoginForm handleClose={handleClose} />
)}
</Menu>
);
};
export default UserMenu;
+31 -13
View File
@@ -1,7 +1,9 @@
import { Avatar, Card, CardContent, CardHeader, Link as MUILink, Typography } from '@mui/material';
import { Link } from '@tanstack/react-router';
import { FC } from 'react';
import Api from '../../api/Api';
import { PostAuth, PostNonAuth } from '../../types/Post';
import convertDate from '../../utils/date';
interface Props {
post: PostNonAuth | PostAuth;
@@ -12,21 +14,37 @@ const Post: FC<Props> = ({ post }) => {
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
<CardHeader
avatar={
<MUILink component={Link} to="/" color="#FFF" variant="h6" underline="none">
'id' in post.user ? (
<MUILink
component={Link}
to="/profile/$id"
params={{ id: post.user.id }}
color="#FFF"
variant="h6"
underline="none"
>
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
</MUILink>
) : (
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
</MUILink>
)
}
title={post.user.username}
subheader={new Date(post.postedAt.date).toLocaleString(navigator.languages[0] ?? 'de-DE', {
timeZone: post.postedAt.timezone,
weekday: 'short',
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
hour12: false,
minute: '2-digit',
})}
title={
'id' in post.user ? (
post.user.id !== Api.getAuthenticatedUser()?.id ? (
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
{post.user.username}
</MUILink>
) : (
<MUILink component={Link} to="/profile">
{post.user.username}
</MUILink>
)
) : (
post.user.username
)
}
subheader={convertDate(post.postedAt)}
/>
<CardContent>
<Typography>{post.content}</Typography>
@@ -0,0 +1,34 @@
import { Avatar, Box, Grid, Typography } from '@mui/material';
import { FC } from 'react';
import { useTranslation } from 'react-i18next';
import { User } from '../../types/User';
import convertDate from '../../utils/date';
interface Props {
user: User;
}
const Profile: FC<Props> = ({ user }) => {
const { t } = useTranslation();
return (
<Box>
<Grid container spacing={2}>
<Grid item sx={{ display: 'grid', gridTemplateColumns: 'fit-content(100%) 1fr', columnGap: 2, rowGap: 1 }}>
<Box sx={{ gridColumn: '1/3', display: 'flex', justifyContent: 'center' }}>
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: 100, height: 100 }} />
</Box>
<Typography fontWeight="bold">{t('Username')}:</Typography>
<Typography>{user.username}</Typography>
<Typography fontWeight="bold">{t('Member since')}:</Typography>
<Typography>{convertDate(user.memberSince)}</Typography>
<Typography fontWeight="bold">{t('Post count')}:</Typography>
<Typography>{user.postCount}</Typography>
</Grid>
<Grid item></Grid>
</Grid>
</Box>
);
};
export default Profile;