Compare commits

...

2 Commits

14 changed files with 373 additions and 80 deletions

9
exam/dist/assets/index-BFREkcQ0.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,7 +23,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GuestBook</title> <title>GuestBook</title>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-DBCDWqoJ.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BFREkcQ0.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C9_qfvjK.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BnAUJOoN.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-BqkrhB-y.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-BqkrhB-y.js">

View File

@ -97,5 +97,9 @@
"Session expired": "Deine Sitzung ist abgelaufen.", "Session expired": "Deine Sitzung ist abgelaufen.",
"General error": "Da ist wohl was schief gelaufen.", "General error": "Da ist wohl was schief gelaufen.",
"Favicon": "Gästebuch Icons erstellt von Smashicons - Flaticon" "Favicon": "Gästebuch Icons erstellt von Smashicons - Flaticon",
"Password confirm": "Passwort bestätigen",
"Password match": "Passwörter stimmen nicht überein",
"Change password": "Passwort ändern"
} }

View File

@ -98,5 +98,9 @@
"Session expired": "Your session has expired.", "Session expired": "Your session has expired.",
"General error": "Looks like something went wrong.", "General error": "Looks like something went wrong.",
"Favicon": "Guests book icons created by Smashicons - Flaticon" "Favicon": "Guests book icons created by Smashicons - Flaticon",
"Password confirm": "Confirm password",
"Password match": "Password do not match",
"Change password": "Change password"
} }

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{ {
"name": "react", "name": "react",
"private": true, "private": true,
"version": "1.0.2", "version": "1.1.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@ -97,5 +97,9 @@
"Session expired": "Deine Sitzung ist abgelaufen.", "Session expired": "Deine Sitzung ist abgelaufen.",
"General error": "Da ist wohl was schief gelaufen.", "General error": "Da ist wohl was schief gelaufen.",
"Favicon": "Gästebuch Icons erstellt von Smashicons - Flaticon" "Favicon": "Gästebuch Icons erstellt von Smashicons - Flaticon",
"Password confirm": "Passwort bestätigen",
"Password match": "Passwörter stimmen nicht überein",
"Change password": "Passwort ändern"
} }

View File

@ -98,5 +98,9 @@
"Session expired": "Your session has expired.", "Session expired": "Your session has expired.",
"General error": "Looks like something went wrong.", "General error": "Looks like something went wrong.",
"Favicon": "Guests book icons created by Smashicons - Flaticon" "Favicon": "Guests book icons created by Smashicons - Flaticon",
"Password confirm": "Confirm password",
"Password match": "Password do not match",
"Change password": "Change password"
} }

View File

@ -40,16 +40,23 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}, },
}); });
const form = useForm<UserCreate>({ const form = useForm<UserCreate & { passwordConfirm: string }>({
defaultValues: { defaultValues: {
username: '', username: '',
email: '', email: '',
password: '', password: '',
passwordConfirm: '',
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
try { try {
createMutation.mutate( createMutation.mutate(
{ data: value }, {
data: {
username: value.username,
email: value.email,
password: value.password,
},
},
{ {
onSuccess: () => setError(undefined), onSuccess: () => setError(undefined),
onError: setError, onError: setError,
@ -113,7 +120,7 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}} }}
> >
<DialogTitle>{t('Register')}</DialogTitle> <DialogTitle>{t('Register')}</DialogTitle>
<DialogContent sx={{ gap: 2 }}> <DialogContent>
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
<form.Field <form.Field
@ -215,6 +222,48 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}} }}
/> />
</Grid> </Grid>
<Grid item xs={12}>
<form.Field
name="passwordConfirm"
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) =>
!value
? t('Password required')
: value !== fieldApi.form.getFieldValue('password')
? t('Password match')
: undefined,
onChangeAsyncDebounceMs: 250,
onChangeAsync: async ({ value, fieldApi }) =>
!value
? t('Password required')
: value !== fieldApi.form.getFieldValue('password')
? t('Password match')
: undefined,
}}
children={(field) => {
return (
<>
<TextField
variant="outlined"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
size="small"
label={t('Password confirm')}
required
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
type="password"
autoComplete="new-password"
fullWidth
/>
</>
);
}}
/>
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<form.Subscribe <form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]} selector={(state) => [state.canSubmit, state.isSubmitting]}

View File

@ -5,6 +5,7 @@ import {
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
Grid,
TextField, TextField,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
@ -98,66 +99,72 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
> >
<DialogTitle>{t('Edit data')}</DialogTitle> <DialogTitle>{t('Edit data')}</DialogTitle>
<DialogContent> <DialogContent>
<form.Field <Grid container spacing={2}>
name="username" <Grid item xs={12}>
validators={{ <form.Field
onChange: ({ value }) => (!value ? t('Username required') : undefined), name="username"
onChangeAsyncDebounceMs: 250, validators={{
onChangeAsync: async ({ value }) => { onChange: ({ value }) => (!value ? t('Username required') : undefined),
return !value && t('Username required'); onChangeAsyncDebounceMs: 250,
}, onChangeAsync: async ({ value }) => {
}} return !value && t('Username required');
children={(field) => { },
return ( }}
<> children={(field) => {
<TextField return (
name={field.name} <>
value={field.state.value} <TextField
onBlur={field.handleBlur} name={field.name}
onChange={(e) => field.handleChange(e.target.value)} value={field.state.value}
size="small" onBlur={field.handleBlur}
label={t('Username')} onChange={(e) => field.handleChange(e.target.value)}
required size="small"
margin="dense" label={t('Username')}
autoComplete="new-username" required
fullWidth margin="dense"
error={field.state.meta.isTouched && field.state.meta.errors.length > 0} autoComplete="new-username"
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''} fullWidth
/> error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
</> helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
); />
}} </>
/> );
<form.Field }}
name="email" />
validators={{ </Grid>
onChange: ({ value }) => (!value ? t('Email required') : undefined), <Grid item xs={12}>
onChangeAsyncDebounceMs: 250, <form.Field
onChangeAsync: async ({ value }) => { name="email"
return !value && t('Email required'); validators={{
}, onChange: ({ value }) => (!value ? t('Email required') : undefined),
}} onChangeAsyncDebounceMs: 250,
children={(field) => { onChangeAsync: async ({ value }) => {
return ( return !value && t('Email required');
<> },
<TextField }}
name={field.name} children={(field) => {
value={field.state.value} return (
onBlur={field.handleBlur} <>
onChange={(e) => field.handleChange(e.target.value)} <TextField
size="small" name={field.name}
label={t('Email')} value={field.state.value}
required onBlur={field.handleBlur}
margin="dense" onChange={(e) => field.handleChange(e.target.value)}
autoComplete="new-email" size="small"
fullWidth label={t('Email')}
error={field.state.meta.isTouched && field.state.meta.errors.length > 0} required
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''} margin="dense"
/> autoComplete="new-email"
</> fullWidth
); error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
}} helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
/> />
</>
);
}}
/>
</Grid>
</Grid>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<form.Subscribe <form.Subscribe

View File

@ -0,0 +1,215 @@
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
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, useEffect, useState } from 'react';
import { useApi } from '../../../api/Api';
import { User, UserUpdate } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
interface Props {
user: User;
open: boolean;
onClose: () => void;
}
const UserPasswordDialog: FC<Props> = ({ user, open, onClose }) => {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const [error, setError] = useState<any>();
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
const queryClient = useQueryClient();
const Api = useApi();
const updateMutation = useMutation({
mutationFn: ({ data, id }: { data: UserUpdate; id?: number }) => {
return Api.updateUser(data, id);
},
});
const form = useForm<UserUpdate & { passwordConfirm: string }>({
defaultValues: {
password: '',
passwordConfirm: '',
},
onSubmit: async ({ value }) => {
try {
updateMutation.mutate(
{ data: { password: value.password }, id: Api.authenticatedUser?.id === user.id ? undefined : user.id },
{
onSuccess: () => {
handleClose();
const queryKey = Api.authenticatedUser?.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 handleClose = () => {
form.reset();
setError(undefined);
onClose();
};
useEffect(() => {
if (!Api.hasAuth) handleClose();
}, [Api.hasAuth]); //eslint-disable-line react-hooks/exhaustive-deps
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>
<Grid container spacing={2}>
<Grid item xs={12}>
<form.Field
name="password"
validators={{
onChange: ({ value }) => (!value ? t('Password required') : undefined),
onChangeAsyncDebounceMs: 250,
onChangeAsync: async ({ value }) => {
return !value && t('Password required');
},
}}
children={(field) => {
return (
<>
<TextField
variant="outlined"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
size="small"
label={t('Password')}
required
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
type="password"
autoComplete="new-password"
fullWidth
/>
</>
);
}}
/>
</Grid>
<Grid item xs={12}>
<form.Field
name="passwordConfirm"
validators={{
onChangeListenTo: ['password'],
onChange: ({ value, fieldApi }) =>
!value
? t('Password required')
: value !== fieldApi.form.getFieldValue('password')
? t('Password match')
: undefined,
onChangeAsyncDebounceMs: 250,
onChangeAsync: async ({ value, fieldApi }) =>
!value
? t('Password required')
: value !== fieldApi.form.getFieldValue('password')
? t('Password match')
: undefined,
}}
children={(field) => {
return (
<>
<TextField
variant="outlined"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
size="small"
label={t('Password confirm')}
required
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
type="password"
autoComplete="new-password"
fullWidth
/>
</>
);
}}
/>
</Grid>
</Grid>
</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 UserPasswordDialog;

View File

@ -17,7 +17,8 @@ import { PostAuth } from '../../types/Post';
import { User } from '../../types/User'; import { User } from '../../types/User';
import convertDate from '../../utils/date'; import convertDate from '../../utils/date';
import UserEditDialog from '../Dialogs/UserEdit/UserEditDialog'; import UserEditDialog from '../Dialogs/UserEdit/UserEditDialog';
import UserImageDialog from '../Dialogs/UserImage/UserImageDialog'; import UserImageDialog from '../Dialogs/UserEdit/UserImageDialog';
import UserPasswordDialog from '../Dialogs/UserEdit/UserPasswordDialog';
import Post from '../Post/Post'; import Post from '../Post/Post';
interface Props { interface Props {
@ -29,6 +30,7 @@ interface Props {
const Profile: FC<Props> = ({ user, posts, canEdit }) => { const Profile: FC<Props> = ({ user, posts, canEdit }) => {
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [imageOpen, setImageOpen] = useState(false); const [imageOpen, setImageOpen] = useState(false);
const [passwordOpen, setPasswordOpen] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
@ -65,8 +67,12 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
<Button size="small" onClick={() => setEditOpen(true)}> <Button size="small" onClick={() => setEditOpen(true)}>
{t('Edit')} {t('Edit')}
</Button> </Button>
<Button size="small" color="secondary" onClick={() => setPasswordOpen(true)}>
{t('Change password')}
</Button>
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} /> <UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
<UserImageDialog user={user} open={imageOpen} onClose={() => setImageOpen(false)} /> <UserImageDialog user={user} open={imageOpen} onClose={() => setImageOpen(false)} />
<UserPasswordDialog user={user} open={passwordOpen} onClose={() => setPasswordOpen(false)} />
</> </>
)} )}
</CardActions> </CardActions>