PW Change

This commit is contained in:
2024-07-30 00:13:55 +02:00
parent f6a10c8133
commit bf3cebcf86
14 changed files with 373 additions and 80 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -23,7 +23,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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/mui-BnAUJOoN.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-BqkrhB-y.js">
+5 -1
View File
@@ -97,5 +97,9 @@
"Session expired": "Deine Sitzung ist abgelaufen.",
"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"
}
+5 -1
View File
@@ -98,5 +98,9 @@
"Session expired": "Your session has expired.",
"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"
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "react",
"private": true,
"version": "1.0.2",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -97,5 +97,9 @@
"Session expired": "Deine Sitzung ist abgelaufen.",
"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"
}
@@ -98,5 +98,9 @@
"Session expired": "Your session has expired.",
"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"
}
@@ -40,16 +40,23 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
},
});
const form = useForm<UserCreate>({
const form = useForm<UserCreate & { passwordConfirm: string }>({
defaultValues: {
username: '',
email: '',
password: '',
passwordConfirm: '',
},
onSubmit: async ({ value }) => {
try {
createMutation.mutate(
{ data: value },
{
data: {
username: value.username,
email: value.email,
password: value.password,
},
},
{
onSuccess: () => setError(undefined),
onError: setError,
@@ -113,7 +120,7 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}}
>
<DialogTitle>{t('Register')}</DialogTitle>
<DialogContent sx={{ gap: 2 }}>
<DialogContent>
<Grid container spacing={2}>
<Grid item xs={12}>
<form.Field
@@ -215,6 +222,48 @@ const RegisterDialog: FC<Props> = ({ open, onClose }) => {
}}
/>
</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}>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
@@ -5,6 +5,7 @@ import {
DialogActions,
DialogContent,
DialogTitle,
Grid,
TextField,
useMediaQuery,
useTheme,
@@ -98,66 +99,72 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
>
<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(',') : ''}
/>
</>
);
}}
/>
<form.Field
name="email"
validators={{
onChange: ({ value }) => (!value ? t('Email required') : undefined),
onChangeAsyncDebounceMs: 250,
onChangeAsync: async ({ value }) => {
return !value && t('Email 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('Email')}
required
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 container spacing={2}>
<Grid item xs={12}>
<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(',') : ''}
/>
</>
);
}}
/>
</Grid>
<Grid item xs={12}>
<form.Field
name="email"
validators={{
onChange: ({ value }) => (!value ? t('Email required') : undefined),
onChangeAsyncDebounceMs: 250,
onChangeAsync: async ({ value }) => {
return !value && t('Email 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('Email')}
required
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>
<DialogActions>
<form.Subscribe
@@ -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;
@@ -17,7 +17,8 @@ import { PostAuth } from '../../types/Post';
import { User } from '../../types/User';
import convertDate from '../../utils/date';
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';
interface Props {
@@ -29,6 +30,7 @@ interface Props {
const Profile: FC<Props> = ({ user, posts, canEdit }) => {
const [editOpen, setEditOpen] = useState(false);
const [imageOpen, setImageOpen] = useState(false);
const [passwordOpen, setPasswordOpen] = useState(false);
const { t } = useTranslation();
@@ -65,8 +67,12 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
<Button size="small" onClick={() => setEditOpen(true)}>
{t('Edit')}
</Button>
<Button size="small" color="secondary" onClick={() => setPasswordOpen(true)}>
{t('Change password')}
</Button>
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
<UserImageDialog user={user} open={imageOpen} onClose={() => setImageOpen(false)} />
<UserPasswordDialog user={user} open={passwordOpen} onClose={() => setPasswordOpen(false)} />
</>
)}
</CardActions>