Compare commits

..

No commits in common. "45778d83d04d0403f8a918ae8a1c1d6db9d24c33" and "478f2429f5146c68092a3bae7f5589cab49c299b" have entirely different histories.

14 changed files with 80 additions and 373 deletions

File diff suppressed because one or more lines are too long

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

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-BFREkcQ0.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-DBCDWqoJ.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,9 +97,5 @@
"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,9 +98,5 @@
"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.1.0", "version": "1.0.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

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

@ -1,215 +0,0 @@
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,8 +17,7 @@ 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/UserEdit/UserImageDialog'; import UserImageDialog from '../Dialogs/UserImage/UserImageDialog';
import UserPasswordDialog from '../Dialogs/UserEdit/UserPasswordDialog';
import Post from '../Post/Post'; import Post from '../Post/Post';
interface Props { interface Props {
@ -30,7 +29,6 @@ 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();
@ -67,12 +65,8 @@ 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>