Delete Post
This commit is contained in:
parent
0929371c10
commit
a515c447e0
@ -139,6 +139,7 @@ class Post implements JsonSerializable
|
||||
$db = Database::getInstance();
|
||||
$stmt = $db->prepare("DELETE FROM egb_gaestebuch WHERE id = :ID");
|
||||
$stmt->bindValue(":ID", $this->id);
|
||||
$stmt->execute();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
{
|
||||
"NotFound_user:login": "Benutzer existiert nicht",
|
||||
"Unauthorized_login": "Ungültige E-Mail oder Passwort",
|
||||
"Unauthorized_delete": "Keine Berechtigung",
|
||||
"NotFound_post:delete": "Post nicht gefunden",
|
||||
|
||||
"GuestBook": "Gästebuch",
|
||||
|
||||
@ -18,5 +20,13 @@
|
||||
|
||||
"Username": "Benutzername",
|
||||
"Member since": "Mitglied seit",
|
||||
"Post count": "Anzahl Posts"
|
||||
"Post count": "Anzahl Posts",
|
||||
|
||||
"Edit": "Bearbeiten",
|
||||
"Delete": "Löschen",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
|
||||
"Confirm post delete title": "Diesen Post löschen?",
|
||||
"Confirm post delete body": "Möchtest du diesen Post von {{name}} wirklich Löschen?"
|
||||
}
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
"NotFound_user:login": "User does not exist",
|
||||
"Unauthorized_login": "Invalid email or password",
|
||||
|
||||
"Unauthorized_delete": "Unauthorized",
|
||||
"NotFound_post:delete": "Post not found",
|
||||
|
||||
"GuestBook": "GuestBook",
|
||||
|
||||
"Email": "Email",
|
||||
@ -18,5 +21,13 @@
|
||||
|
||||
"Username": "Username",
|
||||
"Member since": "Member since",
|
||||
"Post count": "Post count"
|
||||
"Post count": "Post count",
|
||||
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete",
|
||||
"Yes": "Yes",
|
||||
"No": "No",
|
||||
|
||||
"Confirm post delete title": "Delete this post?",
|
||||
"Confirm post delete body": "Do you really want to delete this post by {{name}}?"
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { PostListAuth, PostListNonAuth } from '../types/Post';
|
||||
import { PostAuth, PostListAuth, PostListNonAuth } from '../types/Post';
|
||||
import { User } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
@ -27,7 +27,6 @@ class ApiImpl {
|
||||
public logIn = async (email: string, password: string): Promise<void> => {
|
||||
const { user, token } = await (await this.post('login', { email, password })).json();
|
||||
this.self = user;
|
||||
this.isAdmin = user.isAdmin;
|
||||
this.token = token;
|
||||
};
|
||||
|
||||
@ -50,15 +49,17 @@ class ApiImpl {
|
||||
return await (await this.get(url)).json();
|
||||
};
|
||||
|
||||
public deletePost = async (id: number): Promise<PostAuth> => {
|
||||
return await (await this.delete(`posts/${id}`)).json();
|
||||
};
|
||||
|
||||
public user = async (id?: number): Promise<User> => {
|
||||
return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json();
|
||||
};
|
||||
|
||||
private post = async (
|
||||
endpoint: string,
|
||||
body: Record<string, unknown> | undefined = undefined,
|
||||
headers: HeadersInit | undefined = undefined
|
||||
) => {
|
||||
/* Internal */
|
||||
|
||||
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
@ -69,11 +70,7 @@ class ApiImpl {
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private postAuth = async (
|
||||
endpoint: string,
|
||||
body: Record<string, unknown> | undefined = undefined,
|
||||
headers: HeadersInit | undefined = undefined
|
||||
) => {
|
||||
private postAuth = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'post',
|
||||
@ -84,7 +81,7 @@ class ApiImpl {
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private get = async (endpoint: string, headers: HeadersInit | undefined = undefined) => {
|
||||
private get = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
@ -94,7 +91,7 @@ class ApiImpl {
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private getAuth = async (endpoint: string, headers: HeadersInit | undefined = undefined) => {
|
||||
private getAuth = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'get',
|
||||
@ -103,6 +100,16 @@ class ApiImpl {
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private delete = async (endpoint: string, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'delete',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
}
|
||||
|
||||
const Api = new ApiImpl();
|
||||
|
||||
@ -52,10 +52,9 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
name="email"
|
||||
validators={{
|
||||
onChange: ({ value }) => (!value ? t('Email required') : undefined),
|
||||
onChangeAsyncDebounceMs: 500,
|
||||
onChangeAsyncDebounceMs: 250,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return value.includes('error') && 'No "error" allowed in email';
|
||||
return !value && t('Email required');
|
||||
},
|
||||
}}
|
||||
children={(field) => {
|
||||
@ -84,10 +83,9 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
name="password"
|
||||
validators={{
|
||||
onChange: ({ value }) => (!value ? t('Password required') : undefined),
|
||||
onChangeAsyncDebounceMs: 500,
|
||||
onChangeAsyncDebounceMs: 250,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return value.includes('error') && 'No "error" allowed in password';
|
||||
return !value && t('Password required');
|
||||
},
|
||||
}}
|
||||
children={(field) => {
|
||||
@ -121,7 +119,7 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{error && <Typography color="error.main">{handleError(error, 'login', t)}</Typography>}
|
||||
{error && <Typography color="error.main">{handleError(error, t, 'login')}</Typography>}
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { AccountCircle, Translate } from '@mui/icons-material';
|
||||
import { AccountCircle, Person, Translate } from '@mui/icons-material';
|
||||
import {
|
||||
AppBar,
|
||||
Avatar,
|
||||
@ -57,7 +57,9 @@ const Header: FC = () => {
|
||||
</IconButton>
|
||||
{user ? (
|
||||
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`} />
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton size="large" onClick={(event) => setAnchorUserMenu(event.currentTarget)} color="inherit">
|
||||
|
||||
@ -1,25 +1,67 @@
|
||||
import { Avatar, Card, CardContent, CardHeader, Link as MUILink, Typography } from '@mui/material';
|
||||
import { Person } from '@mui/icons-material';
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Link as MUILink,
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { FC } from 'react';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
import { PostAuth, PostNonAuth } from '../../types/Post';
|
||||
import convertDate from '../../utils/date';
|
||||
import handleError from '../../utils/errors';
|
||||
|
||||
interface Props {
|
||||
post: PostNonAuth | PostAuth;
|
||||
}
|
||||
|
||||
const Post: FC<Props> = ({ post }) => {
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => {
|
||||
return Api.deletePost(id);
|
||||
},
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Card sx={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
'id' in post.user ? (
|
||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }} underline="none">
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
||||
</MUILink>
|
||||
post.user.id !== Api.getAuthenticatedUser()?.id ? (
|
||||
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
</MUILink>
|
||||
) : (
|
||||
<MUILink component={Link} to="/profile">
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
</MUILink>
|
||||
)
|
||||
) : (
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`} />
|
||||
<Avatar alt={post.user.username} src={`storage/${post.user.image}`}>
|
||||
<Person />
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
title={
|
||||
@ -42,6 +84,42 @@ const Post: FC<Props> = ({ post }) => {
|
||||
<CardContent>
|
||||
<Typography>{post.content}</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions>
|
||||
{Api.isAdmin() && (
|
||||
<>
|
||||
<Button size="small" color="error" onClick={() => setOpen(true)}>
|
||||
{t('Delete')}
|
||||
</Button>
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<DialogTitle>{t('Confirm post delete title')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>{t('Confirm post delete body', { name: post.user.username })}</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setOpen(false)} autoFocus variant="contained">
|
||||
{t('No')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate(post.id);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('Yes')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</CardActions>
|
||||
<Snackbar open={deleteMutation.isError} autoHideDuration={2000} onClose={() => deleteMutation.reset()}>
|
||||
<Alert severity="error" variant="filled" sx={{ width: '100%' }}>
|
||||
{deleteMutation.isError && handleError(deleteMutation.error, t, 'delete')}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { Avatar, Box, Grid, Typography } from '@mui/material';
|
||||
import { Person } from '@mui/icons-material';
|
||||
import { Avatar, Box, Button, Card, CardActions, CardContent, Grid, Typography } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User } from '../../types/User';
|
||||
@ -6,28 +7,37 @@ import convertDate from '../../utils/date';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
const Profile: FC<Props> = ({ user }) => {
|
||||
const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
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>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||
<Person sx={{ width: '60px', height: '60px' }} />
|
||||
</Avatar>
|
||||
</Grid>
|
||||
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
||||
<Typography fontWeight="bold">{t('Username')}:</Typography>
|
||||
<Typography>{user.username}</Typography>
|
||||
<Typography fontWeight="bold">{t('Email')}:</Typography>
|
||||
<Typography>{user.email}</Typography>
|
||||
<Typography fontWeight="bold">{t('Member since')}:</Typography>
|
||||
<Typography>{convertDate(user.memberSince)}</Typography>
|
||||
<Typography fontWeight="bold">{t('Post count')}:</Typography>
|
||||
<Typography>{user.postCount}</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item></Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</CardContent>
|
||||
<CardActions>{canEdit && <Button size="small">{t('Edit')}</Button>}</CardActions>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Box } from '@mui/material';
|
||||
import { QueryClient, useQueryErrorResetBoundary } from '@tanstack/react-query';
|
||||
import { createRootRouteWithContext, ErrorRouteComponent, Outlet, redirect, useRouter } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
@ -13,7 +14,11 @@ const Root = () => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Outlet />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<Box sx={{ maxWidth: '800px' }}>
|
||||
<Outlet />
|
||||
</Box>
|
||||
</Box>
|
||||
{process.env.NODE_ENV === 'development' && <TanStackRouterDevtools />}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -22,7 +22,7 @@ const Home = () => {
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Grid container spacing={2}>
|
||||
{postsQuery.data.map((post) => (
|
||||
<Grid key={post.id} item xs={12} md={6} lg={4} sx={{ display: 'flex' }}>
|
||||
<Grid item xs={12} key={post.id}>
|
||||
<Post post={post} />
|
||||
</Grid>
|
||||
))}
|
||||
|
||||
@ -19,7 +19,7 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Profile user={profileQuery} />
|
||||
<Profile user={profileQuery} canEdit={Api.isAdmin()} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -30,8 +30,9 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
|
||||
stringify: ({ id }) => ({ id: id.toString() }),
|
||||
},
|
||||
loader: ({ context: { queryClient }, params: { id } }) => queryClient.ensureQueryData(profileQueryOptions(id)),
|
||||
beforeLoad: () => {
|
||||
beforeLoad: ({ params: { id } }) => {
|
||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||
if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE });
|
||||
},
|
||||
component: ProfilePage,
|
||||
});
|
||||
|
||||
@ -17,7 +17,7 @@ const ProfilePage = () => {
|
||||
return (
|
||||
<>
|
||||
<Snackbar open={isFetching} message={t('Updating')} />
|
||||
<Profile user={profileQuery} />
|
||||
<Profile user={profileQuery} canEdit={true} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,10 +15,11 @@ export enum ERRORS {
|
||||
const handleError = (
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
context?: string,
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
t: TFunction<'translation', undefined> | ((..._in: any) => any) = (..._in: any) => _in
|
||||
t: TFunction<'translation', undefined> | ((..._in: any) => any) = (..._in: any) => _in,
|
||||
context?: string
|
||||
): string => {
|
||||
console.log(context);
|
||||
if (!error) return t('', {});
|
||||
|
||||
if (error.code) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user