Email Edit

This commit is contained in:
Kilian Hofmann 2024-07-27 03:39:52 +02:00
parent 12f7176467
commit d71eaf2ef2
15 changed files with 229 additions and 175 deletions

1
exam/dist/assets/index-BhgwoArk.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

File diff suppressed because one or more lines are too long

125
exam/dist/assets/mui-CsmJ6if2.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

@ -5,10 +5,10 @@
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title> <title>Vite + React + TS</title>
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-DNzu9OIf.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-BhgwoArk.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-DXd9vB-a.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-DXd9vB-a.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-BZej3Yg3.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-CsmJ6if2.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DeUNQvBN.js"> <link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-DXVkKUs1.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DJgSTqOl.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/mui-CKDNpdid.css">
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css"> <link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">

View File

@ -9,7 +9,8 @@
"Unauthorized_userUpdate": "Keine Berechtigung", "Unauthorized_userUpdate": "Keine Berechtigung",
"NotFound_user:userUpdate": "Benutzer nicht gefunden", "NotFound_user:userUpdate": "Benutzer nicht gefunden",
"FailedUpdate_userUpdate": "{{name}} konnte nicht aktualisiert werden", "FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
"username": "Benutzername", "username": "Benutzername",
"email": "E-Mail", "email": "E-Mail",
@ -47,5 +48,7 @@
"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...", "Deleting": "Löscht...",
"Edit data": "Daten ändern" "Edit data": "Daten ändern",
"Recent posts": "Letzten Posts"
} }

View File

@ -9,7 +9,8 @@
"Unauthorized_userUpdate": "Unauthorized", "Unauthorized_userUpdate": "Unauthorized",
"NotFound_user:userUpdate": "User not found", "NotFound_user:userUpdate": "User not found",
"FailedUpdate_userUpdate": "Failed to update {{name}}", "FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
"username": "username", "username": "username",
"email": "email", "email": "email",
@ -48,5 +49,7 @@
"Deleting": "Deleting...", "Deleting": "Deleting...",
"Edit data": "Edit date" "Edit data": "Edit date",
"Recent posts": "Recent posts"
} }

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,8 @@
"Unauthorized_userUpdate": "Keine Berechtigung", "Unauthorized_userUpdate": "Keine Berechtigung",
"NotFound_user:userUpdate": "Benutzer nicht gefunden", "NotFound_user:userUpdate": "Benutzer nicht gefunden",
"FailedUpdate_userUpdate": "{{name}} konnte nicht aktualisiert werden", "FailedUpdate_Duplicate:username:userUpdate": "Ein Benutzer mit diesem Benutzernamen existiert schon",
"FailedUpdate_Duplicate:email:userUpdate": "Ein Benutzer mit dieser E-Mail existiert schon",
"username": "Benutzername", "username": "Benutzername",
"email": "E-Mail", "email": "E-Mail",
@ -47,5 +48,7 @@
"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...", "Deleting": "Löscht...",
"Edit data": "Daten ändern" "Edit data": "Daten ändern",
"Recent posts": "Letzten Posts"
} }

View File

@ -9,7 +9,8 @@
"Unauthorized_userUpdate": "Unauthorized", "Unauthorized_userUpdate": "Unauthorized",
"NotFound_user:userUpdate": "User not found", "NotFound_user:userUpdate": "User not found",
"FailedUpdate_userUpdate": "Failed to update {{name}}", "FailedUpdate_Duplicate:userUpdate": "A user with this username already exists",
"FailedUpdate_Duplicate:email:userUpdate": "A user with this email already exists",
"username": "username", "username": "username",
"email": "email", "email": "email",
@ -48,5 +49,7 @@
"Deleting": "Deleting...", "Deleting": "Deleting...",
"Edit data": "Edit date" "Edit data": "Edit date",
"Recent posts": "Recent posts"
} }

View File

@ -22,9 +22,9 @@ const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) =>
case ERRORS.UNAUTHORIZED: case ERRORS.UNAUTHORIZED:
return <Typography color={color}>{t(error.code, { context })}</Typography>; return <Typography color={color}>{t(error.code, { context })}</Typography>;
case ERRORS.FAILEDUPDATE: case ERRORS.FAILEDUPDATE:
return error.fields.map((field: string) => ( return error.fields.map((field: string, index: number) => (
<Typography key={`error_${field}`} color={color}> <Typography key={`error_${field}`} color={color}>
{t(error.code, { context, name: t(field) })} {t(error.code, { context: `${error.reasons[index]}:${field}:${context}` })}
</Typography> </Typography>
)); ));
} }

View File

@ -36,6 +36,7 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
const form = useForm<UserUpdate>({ const form = useForm<UserUpdate>({
defaultValues: { defaultValues: {
username: user.username, username: user.username,
email: user.email,
}, },
onSubmit: async ({ value }) => { onSubmit: async ({ value }) => {
try { try {
@ -121,6 +122,36 @@ const UserEditDialog: FC<Props> = ({ user, open, onClose }) => {
); );
}} }}
/> />
<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-username"
fullWidth
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
/>
</>
);
}}
/>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<form.Subscribe <form.Subscribe

View File

@ -1,5 +1,5 @@
import { Menu, MenuItem } from '@mui/material'; import { Menu, MenuItem } from '@mui/material';
import { useNavigate, useRouter } from '@tanstack/react-router'; import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
import { t } from 'i18next'; import { t } from 'i18next';
import { FC } from 'react'; import { FC } from 'react';
import Api from '../../../api/Api'; import Api from '../../../api/Api';
@ -14,6 +14,7 @@ interface Props {
const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => { const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const router = useRouter(); const router = useRouter();
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
const user = Api.getAuthenticatedUser(); const user = Api.getAuthenticatedUser();
@ -40,6 +41,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
{user ? ( {user ? (
[ [
<MenuItem <MenuItem
selected={!!match}
key="profile" key="profile"
onClick={() => { onClick={() => {
navigate({ to: ROUTES.PROFILE }); navigate({ to: ROUTES.PROFILE });

View File

@ -1,5 +1,5 @@
import { Person } from '@mui/icons-material'; import { Person } from '@mui/icons-material';
import { Avatar, Box, Button, Card, CardActions, CardContent, Grid, Typography } from '@mui/material'; import { Avatar, Box, Button, Card, CardActions, CardContent, Divider, Grid, Typography } from '@mui/material';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { User } from '../../types/User'; import { User } from '../../types/User';
@ -17,37 +17,46 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Card> <Grid container sx={{ justifyContent: 'center' }} spacing={2}>
<CardContent> <Grid item>
<Grid container spacing={2}> <Card>
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}> <CardContent>
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: '100px', height: '100px' }}> <Grid container spacing={2}>
<Person sx={{ width: '60px', height: '60px' }} /> <Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
</Avatar> <Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: '100px', height: '100px' }}>
</Grid> <Person sx={{ width: '60px', height: '60px' }} />
<Grid item sx={{ display: 'flex', alignItems: 'center' }}> </Avatar>
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}> </Grid>
<Typography fontWeight="bold">{t('Username')}:</Typography> <Grid item sx={{ display: 'flex', alignItems: 'center' }}>
<Typography>{user.username}</Typography> <Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
<Typography fontWeight="bold">{t('Email')}:</Typography> <Typography fontWeight="bold">{t('Username')}:</Typography>
<Typography>{user.email}</Typography> <Typography>{user.username}</Typography>
<Typography fontWeight="bold">{t('Member since')}:</Typography> <Typography fontWeight="bold">{t('Email')}:</Typography>
<Typography>{convertDate(user.memberSince)}</Typography> <Typography>{user.email}</Typography>
<Typography fontWeight="bold">{t('Post count')}:</Typography> <Typography fontWeight="bold">{t('Member since')}:</Typography>
<Typography>{user.postCount}</Typography> <Typography>{convertDate(user.memberSince)}</Typography>
</Box> <Typography fontWeight="bold">{t('Post count')}:</Typography>
</Grid> <Typography>{user.postCount}</Typography>
</Grid> </Box>
</CardContent> </Grid>
<CardActions> </Grid>
{canEdit && ( </CardContent>
<Button size="small" onClick={() => setEditOpen(true)}> <CardActions>
{t('Edit')} {canEdit && (
</Button> <Button size="small" onClick={() => setEditOpen(true)}>
)} {t('Edit')}
</CardActions> </Button>
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} /> )}
</Card> </CardActions>
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
</Card>
</Grid>
<Grid item xs={12}>
<Divider variant="middle">
<Typography sx={{ opacity: 0.36 }}>{t('Recent posts')}</Typography>
</Divider>
</Grid>
</Grid>
); );
}; };