Post delete, Profile edit

This commit is contained in:
2024-07-27 00:54:23 +02:00
parent a515c447e0
commit 581cacb636
33 changed files with 533 additions and 238 deletions
@@ -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;
@@ -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>
);
@@ -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;
+14 -5
View File
@@ -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>
);
};
+12 -2
View File
@@ -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>
);
};