Email Edit

This commit is contained in:
2024-07-27 03:39:52 +02:00
parent 12f7176467
commit d71eaf2ef2
15 changed files with 229 additions and 175 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-125
View File
File diff suppressed because one or more lines are too long
+125
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -3
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">
+5 -2
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"
} }
+5 -2
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"
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -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"
} }
@@ -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"
} }
@@ -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>
)); ));
} }
@@ -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
@@ -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 });
+41 -32
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>
); );
}; };