diff --git a/exam/classes/Models/Post/Post.php b/exam/classes/Models/Post/Post.php index 22915f9..6bcac1c 100644 --- a/exam/classes/Models/Post/Post.php +++ b/exam/classes/Models/Post/Post.php @@ -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; } diff --git a/exam/react/public/locales/de/translation.json b/exam/react/public/locales/de/translation.json index c16da02..d7c9d64 100644 --- a/exam/react/public/locales/de/translation.json +++ b/exam/react/public/locales/de/translation.json @@ -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?" } diff --git a/exam/react/public/locales/en/translation.json b/exam/react/public/locales/en/translation.json index 207729e..3edd3e7 100644 --- a/exam/react/public/locales/en/translation.json +++ b/exam/react/public/locales/en/translation.json @@ -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}}?" } diff --git a/exam/react/src/api/Api.ts b/exam/react/src/api/Api.ts index b34f9fe..9e7b77a 100644 --- a/exam/react/src/api/Api.ts +++ b/exam/react/src/api/Api.ts @@ -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 => { 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 => { + return await (await this.delete(`posts/${id}`)).json(); + }; + public user = async (id?: number): Promise => { return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json(); }; - private post = async ( - endpoint: string, - body: Record | undefined = undefined, - headers: HeadersInit | undefined = undefined - ) => { + /* Internal */ + + private post = async (endpoint: string, body?: Record, 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 | undefined = undefined, - headers: HeadersInit | undefined = undefined - ) => { + private postAuth = async (endpoint: string, body?: Record, 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(); diff --git a/exam/react/src/components/Forms/Login/LoginForm.tsx b/exam/react/src/components/Forms/Login/LoginForm.tsx index de65977..29040a7 100644 --- a/exam/react/src/components/Forms/Login/LoginForm.tsx +++ b/exam/react/src/components/Forms/Login/LoginForm.tsx @@ -52,10 +52,9 @@ const LoginForm: FC = ({ 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 = ({ 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 = ({ handleClose }) => { )} /> - {error && {handleError(error, 'login', t)}} + {error && {handleError(error, t, 'login')}} ); diff --git a/exam/react/src/components/Forms/UserEdit/UserEditForm.tsx b/exam/react/src/components/Forms/UserEdit/UserEditForm.tsx new file mode 100644 index 0000000..e69de29 diff --git a/exam/react/src/components/Header/Header.tsx b/exam/react/src/components/Header/Header.tsx index 63602f6..6f2c2e8 100644 --- a/exam/react/src/components/Header/Header.tsx +++ b/exam/react/src/components/Header/Header.tsx @@ -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 = () => { {user ? ( setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}> - + + + ) : ( setAnchorUserMenu(event.currentTarget)} color="inherit"> diff --git a/exam/react/src/components/Post/Post.tsx b/exam/react/src/components/Post/Post.tsx index e588645..d53e989 100644 --- a/exam/react/src/components/Post/Post.tsx +++ b/exam/react/src/components/Post/Post.tsx @@ -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 = ({ post }) => { + const deleteMutation = useMutation({ + mutationFn: (id: number) => { + return Api.deletePost(id); + }, + }); + + const [open, setOpen] = useState(false); + + const { t } = useTranslation(); + return ( - + - - + post.user.id !== Api.getAuthenticatedUser()?.id ? ( + + + + + + ) : ( + + + + + + ) ) : ( - + + + ) } title={ @@ -42,6 +84,42 @@ const Post: FC = ({ post }) => { {post.content} + + + {Api.isAdmin() && ( + <> + + setOpen(false)}> + {t('Confirm post delete title')} + + {t('Confirm post delete body', { name: post.user.username })} + + + + + + + + )} + + deleteMutation.reset()}> + + {deleteMutation.isError && handleError(deleteMutation.error, t, 'delete')} + + ); }; diff --git a/exam/react/src/components/Profile/Profile.tsx b/exam/react/src/components/Profile/Profile.tsx index 438b630..fb7c8c6 100644 --- a/exam/react/src/components/Profile/Profile.tsx +++ b/exam/react/src/components/Profile/Profile.tsx @@ -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 = ({ user }) => { +const Profile: FC = ({ user, canEdit }) => { const { t } = useTranslation(); return ( - - - - - - - {t('Username')}: - {user.username} - {t('Member since')}: - {convertDate(user.memberSince)} - {t('Post count')}: - {user.postCount} + + + + + + + + + + + {t('Username')}: + {user.username} + {t('Email')}: + {user.email} + {t('Member since')}: + {convertDate(user.memberSince)} + {t('Post count')}: + {user.postCount} + + - - - + + {canEdit && } + ); }; diff --git a/exam/react/src/routes/__root.tsx b/exam/react/src/routes/__root.tsx index 044cad3..6f1734a 100644 --- a/exam/react/src/routes/__root.tsx +++ b/exam/react/src/routes/__root.tsx @@ -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 ( <>
- + + + + + {process.env.NODE_ENV === 'development' && } ); diff --git a/exam/react/src/routes/index.tsx b/exam/react/src/routes/index.tsx index 0fcb579..716713d 100644 --- a/exam/react/src/routes/index.tsx +++ b/exam/react/src/routes/index.tsx @@ -22,7 +22,7 @@ const Home = () => { {postsQuery.data.map((post) => ( - + ))} diff --git a/exam/react/src/routes/profile/$id.tsx b/exam/react/src/routes/profile/$id.tsx index 6620685..3158730 100644 --- a/exam/react/src/routes/profile/$id.tsx +++ b/exam/react/src/routes/profile/$id.tsx @@ -19,7 +19,7 @@ const ProfilePage = () => { return ( <> - + ); }; @@ -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, }); diff --git a/exam/react/src/routes/profile/index.tsx b/exam/react/src/routes/profile/index.tsx index 617a38e..675312f 100644 --- a/exam/react/src/routes/profile/index.tsx +++ b/exam/react/src/routes/profile/index.tsx @@ -17,7 +17,7 @@ const ProfilePage = () => { return ( <> - + ); }; diff --git a/exam/react/src/utils/errors.ts b/exam/react/src/utils/errors.ts index 4a3e05b..c4d0d04 100644 --- a/exam/react/src/utils/errors.ts +++ b/exam/react/src/utils/errors.ts @@ -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) {