Auth redirects, language switch, basic profile
This commit is contained in:
+8
-7
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user