Post delete, Profile edit
This commit is contained in:
parent
a515c447e0
commit
581cacb636
@ -481,7 +481,7 @@ paths:
|
||||
value:
|
||||
{
|
||||
"code": "FailedUpdate",
|
||||
"fields": ["username", "email", "password"],
|
||||
"fields": ["username", "password", "image"],
|
||||
}
|
||||
tags:
|
||||
- User
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -107,7 +107,7 @@ class Post implements JsonSerializable
|
||||
$data
|
||||
);
|
||||
|
||||
return ["pages" => intdiv($count, $limit) + 1, "data" => $list];
|
||||
return ["pages" => intdiv($count, $limit + 1) + 1, "data" => $list];
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@ -255,7 +255,7 @@ class User implements JsonSerializable
|
||||
$data
|
||||
);
|
||||
|
||||
return ["pages" => intdiv($count, $limit) + 1, "data" => $list];
|
||||
return ["pages" => intdiv($count, $limit + 1), "data" => $list];
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
1
exam/dist/assets/index-CmwTmvyQ.js
vendored
Normal file
1
exam/dist/assets/index-CmwTmvyQ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exam/dist/assets/index-DRZup-fm.js
vendored
1
exam/dist/assets/index-DRZup-fm.js
vendored
File diff suppressed because one or more lines are too long
125
exam/dist/assets/mui-BZej3Yg3.js
vendored
Normal file
125
exam/dist/assets/mui-BZej3Yg3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
125
exam/dist/assets/mui-DQviNP-p.js
vendored
125
exam/dist/assets/mui-DQviNP-p.js
vendored
File diff suppressed because one or more lines are too long
17
exam/dist/assets/tanstack-CLt5K1Fy.js
vendored
17
exam/dist/assets/tanstack-CLt5K1Fy.js
vendored
File diff suppressed because one or more lines are too long
17
exam/dist/assets/tanstack-DeUNQvBN.js
vendored
Normal file
17
exam/dist/assets/tanstack-DeUNQvBN.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
exam/dist/index.html
vendored
6
exam/dist/index.html
vendored
@ -5,10 +5,10 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-DRZup-fm.js"></script>
|
||||
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-CmwTmvyQ.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-DXd9vB-a.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-DQviNP-p.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-CLt5K1Fy.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BZej3Yg3.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DeUNQvBN.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DJgSTqOl.js">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
||||
|
||||
31
exam/dist/locales/de/translation.json
vendored
31
exam/dist/locales/de/translation.json
vendored
@ -2,6 +2,19 @@
|
||||
"NotFound_user:login": "Benutzer existiert nicht",
|
||||
"Unauthorized_login": "Ungültige E-Mail oder Passwort",
|
||||
|
||||
"Unauthorized_delete": "Keine Berechtigung",
|
||||
"NotFound_post:delete": "Post nicht gefunden",
|
||||
|
||||
"Unauthorized_userUpdate": "Keine Berechtigung",
|
||||
"NotFound_user:userUpdate": "Benutzer nicht gefunden",
|
||||
"FailedUpdate_userUpdate": "{{name}} konnte nicht aktualisiert werden",
|
||||
|
||||
"username": "Benutzername",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"image": "Bild",
|
||||
"content": "Beitrag",
|
||||
|
||||
"GuestBook": "Gästebuch",
|
||||
|
||||
"Email": "E-Mail",
|
||||
@ -9,14 +22,28 @@
|
||||
|
||||
"Email required": "E-Mail darf nicht leer sein",
|
||||
"Password required": "Passwort darf nicht leer sein",
|
||||
"Username required": "Benutzername darf nicht leer sein",
|
||||
|
||||
"Log in": "Anmelden",
|
||||
"Log out": "Abmelden",
|
||||
"Profile": "Profil",
|
||||
|
||||
"Updating": "Aktualisiert",
|
||||
"Updating": "Aktualisiert...",
|
||||
|
||||
"Username": "Benutzername",
|
||||
"Member since": "Mitglied seit",
|
||||
"Post count": "Anzahl Posts"
|
||||
"Post count": "Anzahl Posts",
|
||||
|
||||
"Edit": "Bearbeiten",
|
||||
"Delete": "Löschen",
|
||||
"Save": "Speichern",
|
||||
"Cancel": "Abbrechen",
|
||||
"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?",
|
||||
|
||||
"Deleting": "Löscht...",
|
||||
"Edit data": "Daten ändern"
|
||||
}
|
||||
|
||||
32
exam/dist/locales/en/translation.json
vendored
32
exam/dist/locales/en/translation.json
vendored
@ -2,6 +2,19 @@
|
||||
"NotFound_user:login": "User does not exist",
|
||||
"Unauthorized_login": "Invalid email or password",
|
||||
|
||||
"Unauthorized_delete": "Unauthorized",
|
||||
"NotFound_post:delete": "Post not found",
|
||||
|
||||
"Unauthorized_userUpdate": "Unauthorized",
|
||||
"NotFound_user:userUpdate": "User not found",
|
||||
"FailedUpdate_userUpdate": "Failed to update {{name}}",
|
||||
|
||||
"username": "username",
|
||||
"email": "email",
|
||||
"password": "password",
|
||||
"image": "image",
|
||||
"content": "content",
|
||||
|
||||
"GuestBook": "GuestBook",
|
||||
|
||||
"Email": "Email",
|
||||
@ -9,14 +22,29 @@
|
||||
|
||||
"Email required": "Email required",
|
||||
"Password required": "Password required",
|
||||
"Username required": "Username required",
|
||||
|
||||
"Log in": "Log in",
|
||||
"Log out": "Log out",
|
||||
"Profile": "Profile",
|
||||
|
||||
"Updating": "Updating",
|
||||
"Updating": "Updating...",
|
||||
|
||||
"Username": "Username",
|
||||
"Member since": "Member since",
|
||||
"Post count": "Post count"
|
||||
"Post count": "Post count",
|
||||
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"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}}?",
|
||||
|
||||
"Deleting": "Deleting...",
|
||||
|
||||
"Edit data": "Edit date"
|
||||
}
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -5,14 +5,12 @@ module.exports = {
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react-hooks/recommended',
|
||||
'plugin:@tanstack/eslint-plugin-query/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
{
|
||||
"NotFound_user:login": "Benutzer existiert nicht",
|
||||
"Unauthorized_login": "Ungültige E-Mail oder Passwort",
|
||||
|
||||
"Unauthorized_delete": "Keine Berechtigung",
|
||||
"NotFound_post:delete": "Post nicht gefunden",
|
||||
|
||||
"Unauthorized_userUpdate": "Keine Berechtigung",
|
||||
"NotFound_user:userUpdate": "Benutzer nicht gefunden",
|
||||
"FailedUpdate_userUpdate": "{{name}} konnte nicht aktualisiert werden",
|
||||
|
||||
"username": "Benutzername",
|
||||
"email": "E-Mail",
|
||||
"password": "Passwort",
|
||||
"image": "Bild",
|
||||
"content": "Beitrag",
|
||||
|
||||
"GuestBook": "Gästebuch",
|
||||
|
||||
"Email": "E-Mail",
|
||||
@ -11,12 +22,13 @@
|
||||
|
||||
"Email required": "E-Mail darf nicht leer sein",
|
||||
"Password required": "Passwort darf nicht leer sein",
|
||||
"Username required": "Benutzername darf nicht leer sein",
|
||||
|
||||
"Log in": "Anmelden",
|
||||
"Log out": "Abmelden",
|
||||
"Profile": "Profil",
|
||||
|
||||
"Updating": "Aktualisiert",
|
||||
"Updating": "Aktualisiert...",
|
||||
|
||||
"Username": "Benutzername",
|
||||
"Member since": "Mitglied seit",
|
||||
@ -24,9 +36,14 @@
|
||||
|
||||
"Edit": "Bearbeiten",
|
||||
"Delete": "Löschen",
|
||||
"Save": "Speichern",
|
||||
"Cancel": "Abbrechen",
|
||||
"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?"
|
||||
"Confirm post delete body": "Möchtest du diesen Post von {{name}} wirklich Löschen?",
|
||||
|
||||
"Deleting": "Löscht...",
|
||||
"Edit data": "Daten ändern"
|
||||
}
|
||||
|
||||
@ -5,6 +5,16 @@
|
||||
"Unauthorized_delete": "Unauthorized",
|
||||
"NotFound_post:delete": "Post not found",
|
||||
|
||||
"Unauthorized_userUpdate": "Unauthorized",
|
||||
"NotFound_user:userUpdate": "User not found",
|
||||
"FailedUpdate_userUpdate": "Failed to update {{name}}",
|
||||
|
||||
"username": "username",
|
||||
"email": "email",
|
||||
"password": "password",
|
||||
"image": "image",
|
||||
"content": "content",
|
||||
|
||||
"GuestBook": "GuestBook",
|
||||
|
||||
"Email": "Email",
|
||||
@ -12,12 +22,13 @@
|
||||
|
||||
"Email required": "Email required",
|
||||
"Password required": "Password required",
|
||||
"Username required": "Username required",
|
||||
|
||||
"Log in": "Log in",
|
||||
"Log out": "Log out",
|
||||
"Profile": "Profile",
|
||||
|
||||
"Updating": "Updating",
|
||||
"Updating": "Updating...",
|
||||
|
||||
"Username": "Username",
|
||||
"Member since": "Member since",
|
||||
@ -25,9 +36,15 @@
|
||||
|
||||
"Edit": "Edit",
|
||||
"Delete": "Delete",
|
||||
"Save": "Save",
|
||||
"Cancel": "Cancel",
|
||||
"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}}?"
|
||||
"Confirm post delete body": "Do you really want to delete this post by {{name}}?",
|
||||
|
||||
"Deleting": "Deleting...",
|
||||
|
||||
"Edit data": "Edit date"
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { PostAuth, PostListAuth, PostListNonAuth } from '../types/Post';
|
||||
import { User } from '../types/User';
|
||||
import { User, UserUpdate } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
|
||||
@ -20,6 +20,7 @@ class ApiImpl {
|
||||
}
|
||||
|
||||
public hasAuth = () => this.token !== undefined;
|
||||
//FIXME: TESTING
|
||||
public isAdmin = () => this.hasAuth() && this.self?.isAdmin;
|
||||
public getAuthenticatedUser = () => this.self;
|
||||
public getCurrentSession = () => [this.token, this.refreshToken];
|
||||
@ -57,6 +58,10 @@ class ApiImpl {
|
||||
return await (await this.getAuth(`users/${id ?? this.self?.id}`)).json();
|
||||
};
|
||||
|
||||
public updateUser = async (data: UserUpdate, id?: number): Promise<User> => {
|
||||
return await (await this.patch(`users/${id ?? 'self'}`, data as Record<string, unknown>)).json();
|
||||
};
|
||||
|
||||
/* Internal */
|
||||
|
||||
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
@ -110,6 +115,17 @@ class ApiImpl {
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
|
||||
private patch = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
method: 'patch',
|
||||
headers: { token: this.token ?? '', ...headers },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (response.ok) return response;
|
||||
throw await response.json();
|
||||
};
|
||||
}
|
||||
|
||||
const Api = new ApiImpl();
|
||||
|
||||
35
exam/react/src/components/Error/ErrorComponent.tsx
Normal file
35
exam/react/src/components/Error/ErrorComponent.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Typography } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ERRORS } from './Errors';
|
||||
|
||||
interface Props {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
const ErrorComponent: FC<Props> = ({ error, context }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!error) return null;
|
||||
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
case ERRORS.NOT_FOUND:
|
||||
return <Typography color="error.main">{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
|
||||
case ERRORS.UNAUTHORIZED:
|
||||
return <Typography color="error.main">{t(error.code, { context })}</Typography>;
|
||||
case ERRORS.FAILEDUPDATE:
|
||||
return error.fields.map((field: string) => (
|
||||
<Typography key={`error_${field}`} color="error.main">
|
||||
{t(error.code, { context, name: t(field) })}
|
||||
</Typography>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return <Typography color="error.main">{t(error?.message ?? 'Unknown', { context })}</Typography>;
|
||||
};
|
||||
|
||||
export default ErrorComponent;
|
||||
5
exam/react/src/components/Error/Errors.ts
Normal file
5
exam/react/src/components/Error/Errors.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum ERRORS {
|
||||
NOT_FOUND = 'NotFound',
|
||||
UNAUTHORIZED = 'Unauthorized',
|
||||
FAILEDUPDATE = 'FailedUpdate',
|
||||
}
|
||||
@ -1,10 +1,10 @@
|
||||
import { Box, Button, TextField, Typography } from '@mui/material';
|
||||
import { Box, Button, TextField } from '@mui/material';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useRouter } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
import handleError from '../../../utils/errors';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
interface Props {
|
||||
handleClose: () => void;
|
||||
@ -119,7 +119,7 @@ const LoginForm: FC<Props> = ({ handleClose }) => {
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
{error && <Typography color="error.main">{handleError(error, t, 'login')}</Typography>}
|
||||
{error && <ErrorComponent error={error} context="login" />}
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
|
||||
160
exam/react/src/components/Forms/UserEdit/UserEditDialog.tsx
Normal file
160
exam/react/src/components/Forms/UserEdit/UserEditDialog.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import {
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
useMediaQuery,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { FC, FormEvent, useState } from 'react';
|
||||
import Api from '../../../api/Api';
|
||||
import { User, UserUpdate } from '../../../types/User';
|
||||
import ErrorComponent from '../../Error/ErrorComponent';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const [error, setError] = useState<any>();
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
|
||||
return Api.updateUser(data, id);
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm<UserUpdate>({
|
||||
defaultValues: {
|
||||
username: user.username,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
try {
|
||||
updateMutation.mutate(
|
||||
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleClose();
|
||||
|
||||
const queryKey = Api.getAuthenticatedUser()?.id === user.id ? ['profile'] : ['profile', { id: user.id }];
|
||||
queryClient.invalidateQueries({ queryKey });
|
||||
},
|
||||
onError: setError,
|
||||
}
|
||||
);
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (error: any) {
|
||||
setError(error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const theme = useTheme();
|
||||
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleClose = () => {
|
||||
form.reset();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
PaperProps={{
|
||||
component: 'form',
|
||||
onSubmit: (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
form.handleSubmit();
|
||||
},
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLFormElement>) => {
|
||||
if (event.key === 'Tab') {
|
||||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
noValidate: true,
|
||||
}}
|
||||
>
|
||||
<DialogTitle>{t('Edit data')}</DialogTitle>
|
||||
<DialogContent>
|
||||
<form.Field
|
||||
name="username"
|
||||
validators={{
|
||||
onChange: ({ value }) => (!value ? t('Username required') : undefined),
|
||||
onChangeAsyncDebounceMs: 250,
|
||||
onChangeAsync: async ({ value }) => {
|
||||
return !value && t('Username required');
|
||||
},
|
||||
}}
|
||||
children={(field) => {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
onChange={(e) => field.handleChange(e.target.value)}
|
||||
size="small"
|
||||
label={t('Username')}
|
||||
required
|
||||
margin="dense"
|
||||
autoComplete="new-username"
|
||||
fullWidth
|
||||
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
|
||||
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<form.Subscribe
|
||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||
children={([canSubmit]) => (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!canSubmit || updateMutation.isPending}
|
||||
autoFocus
|
||||
variant="contained"
|
||||
endIcon={updateMutation.isPending && <CircularProgress color="inherit" size="20px" />}
|
||||
>
|
||||
{t('Save')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</DialogActions>
|
||||
{error && (
|
||||
<DialogContent>
|
||||
<ErrorComponent error={error} context="userUpdate" />
|
||||
</DialogContent>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserEditDialog;
|
||||
@ -16,27 +16,29 @@ import {
|
||||
Snackbar,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
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';
|
||||
import ErrorComponent from '../Error/ErrorComponent';
|
||||
|
||||
interface Props {
|
||||
post: PostNonAuth | PostAuth;
|
||||
}
|
||||
|
||||
const Post: FC<Props> = ({ post }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: number) => {
|
||||
return Api.deletePost(id);
|
||||
},
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -104,7 +106,13 @@ const Post: FC<Props> = ({ post }) => {
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate(post.id);
|
||||
deleteMutation.mutate(post.id, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ['posts'],
|
||||
});
|
||||
},
|
||||
});
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
@ -117,9 +125,10 @@ const Post: FC<Props> = ({ post }) => {
|
||||
</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')}
|
||||
{deleteMutation.isError && <ErrorComponent error={deleteMutation.error} context="delete" />}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
<Snackbar open={deleteMutation.isPending} message={t('Deleting')} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { Person } from '@mui/icons-material';
|
||||
import { Avatar, Box, Button, Card, CardActions, CardContent, Grid, Typography } from '@mui/material';
|
||||
import { FC } from 'react';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { User } from '../../types/User';
|
||||
import convertDate from '../../utils/date';
|
||||
import UserEditDialog from '../Forms/UserEdit/UserEditDialog';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
@ -11,6 +12,8 @@ interface Props {
|
||||
}
|
||||
|
||||
const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
@ -36,7 +39,14 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
<CardActions>{canEdit && <Button size="small">{t('Edit')}</Button>}</CardActions>
|
||||
<CardActions>
|
||||
{canEdit && (
|
||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
)}
|
||||
</CardActions>
|
||||
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
8
exam/react/src/queries/postsQuery.ts
Normal file
8
exam/react/src/queries/postsQuery.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import Api from '../api/Api';
|
||||
|
||||
export const postsQueryOptions = (page?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
|
||||
queryFn: () => Api.posts(page),
|
||||
});
|
||||
13
exam/react/src/queries/profileQuery.ts
Normal file
13
exam/react/src/queries/profileQuery.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import Api from '../api/Api';
|
||||
|
||||
export const profileSelfQueryOptions = queryOptions({
|
||||
queryKey: ['profile'],
|
||||
queryFn: () => Api.user(),
|
||||
});
|
||||
|
||||
export const profileQueryOptions = (id?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile', { id }],
|
||||
queryFn: () => Api.user(id),
|
||||
});
|
||||
@ -4,9 +4,9 @@ import { createRootRouteWithContext, ErrorRouteComponent, Outlet, redirect, useR
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
import { useEffect } from 'react';
|
||||
import Api from '../api/Api';
|
||||
import { ERRORS } from '../components/Error/Errors';
|
||||
import Header from '../components/Header/Header';
|
||||
import { ROUTES } from '../types/Routes';
|
||||
import { ERRORS } from '../utils/errors';
|
||||
|
||||
const Root = () => {
|
||||
//TODO: REAUTH HERE
|
||||
|
||||
@ -1,17 +1,11 @@
|
||||
import { Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
||||
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../api/Api';
|
||||
import Post from '../components/Post/Post';
|
||||
import { postsQueryOptions } from '../queries/postsQuery';
|
||||
import { ROUTES } from '../types/Routes';
|
||||
|
||||
const postsQueryOptions = (page?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['posts', { page: page ?? 0, hasAuth: Api.hasAuth() }],
|
||||
queryFn: () => Api.posts(page),
|
||||
});
|
||||
|
||||
const Home = () => {
|
||||
const { page } = Route.useSearch();
|
||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||
|
||||
@ -1,17 +1,12 @@
|
||||
import { Snackbar } from '@mui/material';
|
||||
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import Api from '../../api/Api';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profileQueryOptions } from '../../queries/profileQuery';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const profileQueryOptions = (id?: number) =>
|
||||
queryOptions({
|
||||
queryKey: ['profile', { id }],
|
||||
queryFn: () => Api.user(id),
|
||||
});
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { id } = Route.useParams();
|
||||
const { data: profileQuery, isFetching } = useSuspenseQuery(profileQueryOptions(id));
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import { Snackbar } from '@mui/material';
|
||||
import { queryOptions, useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router';
|
||||
import { t } from 'i18next';
|
||||
import Api from '../../api/Api';
|
||||
import Profile from '../../components/Profile/Profile';
|
||||
import { profileSelfQueryOptions } from '../../queries/profileQuery';
|
||||
import { ROUTES } from '../../types/Routes';
|
||||
|
||||
const profileQueryOptions = queryOptions({
|
||||
queryKey: ['profile'],
|
||||
queryFn: () => Api.user(),
|
||||
});
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { data: profileQuery, isFetching } = useSuspenseQuery(profileQueryOptions);
|
||||
const { data: profileQuery, isFetching } = useSuspenseQuery(profileSelfQueryOptions);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -23,7 +19,7 @@ const ProfilePage = () => {
|
||||
};
|
||||
|
||||
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
|
||||
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(profileQueryOptions),
|
||||
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(profileSelfQueryOptions),
|
||||
beforeLoad: () => {
|
||||
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
|
||||
},
|
||||
|
||||
@ -10,3 +10,9 @@ export interface User {
|
||||
memberSince: Timestamp;
|
||||
postCount: number;
|
||||
}
|
||||
|
||||
export interface UserUpdate {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
export enum ERRORS {
|
||||
NOT_FOUND = 'NotFound',
|
||||
UNAUTHORIZED = 'Unauthorized',
|
||||
}
|
||||
|
||||
/**
|
||||
* Return translated error
|
||||
* @param error Error object
|
||||
* @param context Optional context
|
||||
* @param t Optional translation function, defautls to pass through
|
||||
* @returns Translated error or inputs if t as unspecified
|
||||
*/
|
||||
const handleError = (
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
error: any,
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
t: TFunction<'translation', undefined> | ((..._in: any) => any) = (..._in: any) => _in,
|
||||
context?: string
|
||||
): string => {
|
||||
console.log(context);
|
||||
if (!error) return t('', {});
|
||||
|
||||
if (error.code) {
|
||||
switch (error.code) {
|
||||
case ERRORS.NOT_FOUND:
|
||||
return t(error.code, { context: `${error.entity}:${context}` });
|
||||
case ERRORS.UNAUTHORIZED:
|
||||
return t(error.code, { context });
|
||||
default:
|
||||
return t('Unknown', { context });
|
||||
}
|
||||
}
|
||||
|
||||
return t(error?.message ?? 'Unknown', { context });
|
||||
};
|
||||
|
||||
export default handleError;
|
||||
Loading…
x
Reference in New Issue
Block a user