Profile Image edit
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
@@ -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-CG0WySTu.js"></script>
|
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-B5V9Za9P.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-v3E5hT34.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-CxHUbSMi.js">
|
||||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-yMrSYl0u.js">
|
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-xmxrKlZO.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">
|
||||||
|
|||||||
+12
-1
@@ -60,5 +60,16 @@
|
|||||||
|
|
||||||
"Recent posts": "Letzte Posts",
|
"Recent posts": "Letzte Posts",
|
||||||
|
|
||||||
"Post comment": "Beitrag posten"
|
"Comment": "Beitrag",
|
||||||
|
"Content required": "Beitrag darf nicht leer sein",
|
||||||
|
"Post comment": "Beitrag posten",
|
||||||
|
"Edit Post": "Post editieren",
|
||||||
|
|
||||||
|
"Edit image": "Profilbild bearbeiten",
|
||||||
|
"Predefined": "Vorgefertigt",
|
||||||
|
"Upload image": "Bild hochladen",
|
||||||
|
"Upload named": "{{name}} gewählt",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"or": "oder"
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -61,5 +61,16 @@
|
|||||||
|
|
||||||
"Recent posts": "Last posts",
|
"Recent posts": "Last posts",
|
||||||
|
|
||||||
"Post omment": "Post comment"
|
"Comment": "Comment",
|
||||||
|
"Content required": "Content required",
|
||||||
|
"Post comment": "Post comment",
|
||||||
|
"Edit post": "Edit post",
|
||||||
|
|
||||||
|
"Edit image": "Edit image",
|
||||||
|
"Predefined": "Predefined",
|
||||||
|
"Upload image": "Upload image",
|
||||||
|
"Upload named": "{{name}} chosen",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"or": "or"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -60,5 +60,16 @@
|
|||||||
|
|
||||||
"Recent posts": "Letzte Posts",
|
"Recent posts": "Letzte Posts",
|
||||||
|
|
||||||
"Post comment": "Beitrag posten"
|
"Comment": "Beitrag",
|
||||||
|
"Content required": "Beitrag darf nicht leer sein",
|
||||||
|
"Post comment": "Beitrag posten",
|
||||||
|
"Edit Post": "Post editieren",
|
||||||
|
|
||||||
|
"Edit image": "Profilbild bearbeiten",
|
||||||
|
"Predefined": "Vorgefertigt",
|
||||||
|
"Upload image": "Bild hochladen",
|
||||||
|
"Upload named": "{{name}} gewählt",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Entfernen",
|
||||||
|
"or": "oder"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,5 +61,16 @@
|
|||||||
|
|
||||||
"Recent posts": "Last posts",
|
"Recent posts": "Last posts",
|
||||||
|
|
||||||
"Post omment": "Post comment"
|
"Comment": "Comment",
|
||||||
|
"Content required": "Content required",
|
||||||
|
"Post comment": "Post comment",
|
||||||
|
"Edit post": "Edit post",
|
||||||
|
|
||||||
|
"Edit image": "Edit image",
|
||||||
|
"Predefined": "Predefined",
|
||||||
|
"Upload image": "Upload image",
|
||||||
|
"Upload named": "{{name}} chosen",
|
||||||
|
"Avatar": "Avatar {{name}}",
|
||||||
|
"Remove": "Remove",
|
||||||
|
"or": "or"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { POST_LIMIT } from '../constanst';
|
import { POST_LIMIT } from '../constanst';
|
||||||
import { PostAuth, PostListAuth, PostListNonAuth, PostUpdate } from '../types/Post';
|
import { PostAuth, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||||
import { User, UserUpdate } from '../types/User';
|
import { User, UserImageUpdate, UserUpdate } from '../types/User';
|
||||||
|
|
||||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||||
|
|
||||||
@@ -51,8 +51,8 @@ class ApiImpl {
|
|||||||
return await (await this.get(url)).json();
|
return await (await this.get(url)).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
public deletePost = async (id: number): Promise<PostAuth> => {
|
public deletePost = async (id: number): Promise<PostDelete> => {
|
||||||
return await (await this.delete(`posts/${id}`)).json();
|
return await (await this.delete(`posts/${id}?l=${POST_LIMIT}`)).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
public user = async (id?: number): Promise<User> => {
|
public user = async (id?: number): Promise<User> => {
|
||||||
@@ -63,8 +63,15 @@ class ApiImpl {
|
|||||||
return await (await this.patch(`users/${id ?? 'self'}`, data as Record<string, unknown>)).json();
|
return await (await this.patch(`users/${id ?? 'self'}`, data as Record<string, unknown>)).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
public newPost = async (data: PostUpdate): Promise<PostAuth> => {
|
public updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
||||||
return await (await this.postAuth(`posts`, data as Record<string, unknown>)).json();
|
const formData = new FormData();
|
||||||
|
if (data.image) formData.append('image', data.image);
|
||||||
|
if (!data.image && data.predefined) formData.append('predefined', data.predefined);
|
||||||
|
return await (await this.postAuthRaw(`users/${id ?? 'self'}/image`, formData)).json();
|
||||||
|
};
|
||||||
|
|
||||||
|
public newPost = async (data: PostUpdate): Promise<PostNew> => {
|
||||||
|
return await (await this.postAuth(`posts?l=${POST_LIMIT}`, { ...data } as Record<string, unknown>)).json();
|
||||||
};
|
};
|
||||||
|
|
||||||
public updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
public updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
|
||||||
@@ -95,6 +102,17 @@ class ApiImpl {
|
|||||||
throw await response.json();
|
throw await response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private postAuthRaw = async (endpoint: string, body?: FormData, headers?: HeadersInit) => {
|
||||||
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
|
mode: 'cors',
|
||||||
|
method: 'post',
|
||||||
|
headers: { token: this.token ?? '', ...headers },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
if (response.ok) return response;
|
||||||
|
throw await response.json();
|
||||||
|
};
|
||||||
|
|
||||||
private get = async (endpoint: string, headers?: HeadersInit) => {
|
private get = async (endpoint: string, headers?: HeadersInit) => {
|
||||||
const response = await fetch(`${BASE}${endpoint}`, {
|
const response = await fetch(`${BASE}${endpoint}`, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import { CloudUpload, Delete, Person } from '@mui/icons-material';
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
TextField,
|
||||||
|
Typography,
|
||||||
|
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, useState } from 'react';
|
||||||
|
import Api from '../../../api/Api';
|
||||||
|
import { User, UserImageUpdate } from '../../../types/User';
|
||||||
|
import ErrorComponent from '../../Error/ErrorComponent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const [error, setError] = useState<any>();
|
||||||
|
|
||||||
|
const updateMutation = useMutation({
|
||||||
|
mutationFn: ({ data, id }: { data: UserImageUpdate; id?: number }) => {
|
||||||
|
return Api.updateUserImage(data, id);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const form = useForm<UserImageUpdate>({
|
||||||
|
onSubmit: async ({ value }) => {
|
||||||
|
try {
|
||||||
|
updateMutation.mutate(
|
||||||
|
{ data: value, id: Api.getAuthenticatedUser()?.id === user.id ? undefined : user.id },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
handleClose();
|
||||||
|
|
||||||
|
const queryKey = Api.getAuthenticatedUser()?.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 theme = useTheme();
|
||||||
|
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const formState = form.useStore((state) => ({ image: state.values.image, predefined: state.values.predefined }));
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
form.reset();
|
||||||
|
setError(undefined);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onClose={handleClose}
|
||||||
|
fullWidth
|
||||||
|
fullScreen={fullScreen}
|
||||||
|
PaperProps={{
|
||||||
|
component: 'form',
|
||||||
|
encType: 'multipart/form-data',
|
||||||
|
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 image')}</DialogTitle>
|
||||||
|
<DialogContent sx={{ gap: 2 }}>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||||
|
<Avatar
|
||||||
|
alt={user.username}
|
||||||
|
src={
|
||||||
|
formState.image
|
||||||
|
? URL.createObjectURL(formState.image)
|
||||||
|
: formState.predefined
|
||||||
|
? `storage/profilbilder/default/${formState.predefined}.svg`
|
||||||
|
: `storage/${user.image}`
|
||||||
|
}
|
||||||
|
sx={{ width: '100px', height: '100px' }}
|
||||||
|
>
|
||||||
|
<Person sx={{ width: '60px', height: '60px' }} />
|
||||||
|
</Avatar>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
component="label"
|
||||||
|
role={undefined}
|
||||||
|
variant="contained"
|
||||||
|
tabIndex={-1}
|
||||||
|
startIcon={<CloudUpload />}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
<form.Field
|
||||||
|
name="image"
|
||||||
|
children={(field) => (
|
||||||
|
<>
|
||||||
|
<Box sx={{ textOverflow: 'ellipsis', textWrap: 'nowrap', overflow: 'hidden' }}>
|
||||||
|
{!field.state.value ? t('Upload image') : t('Upload named', { name: field.state.value?.name })}
|
||||||
|
</Box>
|
||||||
|
<TextField
|
||||||
|
name={field.name}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
value={!field.state.value ? '' : undefined}
|
||||||
|
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
onChange={(e) => field.handleChange((e.target as any).files[0])}
|
||||||
|
size="small"
|
||||||
|
type="file"
|
||||||
|
required
|
||||||
|
sx={{ display: 'none' }}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<IconButton color="error" onClick={() => form.setFieldValue('image', undefined)}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Divider variant="middle">
|
||||||
|
<Typography sx={{ opacity: 0.36 }}>{t('or')}</Typography>
|
||||||
|
</Divider>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<form.Field
|
||||||
|
name="predefined"
|
||||||
|
children={(field) => (
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel size="small">{t('Predefined')}</InputLabel>
|
||||||
|
<Select
|
||||||
|
name={field.name}
|
||||||
|
value={field.state.value}
|
||||||
|
onBlur={field.handleBlur}
|
||||||
|
onChange={(e) => field.handleChange(e.target.value)}
|
||||||
|
size="small"
|
||||||
|
label={t('Predefined')}
|
||||||
|
autoComplete="off"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{[...Array(10).keys()].map((i) => (
|
||||||
|
<MenuItem key={`avatar-${i + 1}`} value={`avatar-${i + 1}`}>
|
||||||
|
{t('Avatar', { name: i + 1 })}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</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 UserImageDialog;
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Box, Button, CircularProgress, LinearProgress, TextField } from '@mui/material';
|
import { Box, Button, CircularProgress, LinearProgress, TextField } from '@mui/material';
|
||||||
import { useForm } from '@tanstack/react-form';
|
import { useForm } from '@tanstack/react-form';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from '@tanstack/react-router';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../../api/Api';
|
import Api from '../../../api/Api';
|
||||||
@@ -14,6 +15,7 @@ const PostForm: FC = () => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const newMutation = useMutation({
|
const newMutation = useMutation({
|
||||||
mutationFn: ({ data }: { data: PostUpdate }) => {
|
mutationFn: ({ data }: { data: PostUpdate }) => {
|
||||||
@@ -30,8 +32,10 @@ const PostForm: FC = () => {
|
|||||||
newMutation.mutate(
|
newMutation.mutate(
|
||||||
{ data: value },
|
{ data: value },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: async (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
form.reset();
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ['posts'] });
|
||||||
|
navigate({ to: '/', search: { page: data.pages - 1 } });
|
||||||
},
|
},
|
||||||
onError: setError,
|
onError: setError,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link } from '@tanstack/react-router';
|
import { Link, useNavigate } from '@tanstack/react-router';
|
||||||
import { FC, useState } from 'react';
|
import { FC, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../../api/Api';
|
import Api from '../../api/Api';
|
||||||
@@ -42,9 +42,9 @@ const Post: FC<Props> = ({ post }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@@ -121,10 +121,11 @@ const Post: FC<Props> = ({ post }) => {
|
|||||||
color="error"
|
color="error"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteMutation.mutate(post.id, {
|
deleteMutation.mutate(post.id, {
|
||||||
onSuccess: () => {
|
onSuccess: async (data) => {
|
||||||
queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: ['posts'],
|
queryKey: ['posts'],
|
||||||
});
|
});
|
||||||
|
navigate({ to: '/', search: { page: data.pages - 1 } });
|
||||||
},
|
},
|
||||||
onError: setError,
|
onError: setError,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,22 @@
|
|||||||
import { Person } from '@mui/icons-material';
|
import { Person } from '@mui/icons-material';
|
||||||
import { Avatar, Box, Button, Card, CardActions, CardContent, Divider, Grid, Typography } from '@mui/material';
|
import {
|
||||||
|
Avatar,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
CardActions,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
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';
|
||||||
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';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
user: User;
|
user: User;
|
||||||
@@ -13,6 +25,7 @@ interface Props {
|
|||||||
|
|
||||||
const Profile: FC<Props> = ({ user, canEdit }) => {
|
const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||||
const [editOpen, setEditOpen] = useState(false);
|
const [editOpen, setEditOpen] = useState(false);
|
||||||
|
const [imageOpen, setImageOpen] = useState(false);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -23,9 +36,11 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
||||||
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
<IconButton onClick={() => setImageOpen(true)}>
|
||||||
<Person sx={{ width: '60px', height: '60px' }} />
|
<Avatar alt={user.username} src={`storage/>${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||||
</Avatar>
|
<Person sx={{ width: '60px', height: '60px' }} />
|
||||||
|
</Avatar>
|
||||||
|
</IconButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
||||||
@@ -43,12 +58,15 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardActions>
|
<CardActions>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
<>
|
||||||
{t('Edit')}
|
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||||
</Button>
|
{t('Edit')}
|
||||||
|
</Button>
|
||||||
|
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||||
|
<UserImageDialog user={user} open={imageOpen} onClose={() => setImageOpen(false)} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</CardActions>
|
</CardActions>
|
||||||
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
|
|
||||||
</Card>
|
</Card>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Divider, Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
import { Divider, Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
||||||
import { useSuspenseQuery } from '@tanstack/react-query';
|
import { useSuspenseQuery } from '@tanstack/react-query';
|
||||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import Api from '../api/Api';
|
import Api from '../api/Api';
|
||||||
import PostForm from '../components/Forms/Post/PostForm';
|
import PostForm from '../components/Forms/Post/PostForm';
|
||||||
@@ -12,6 +13,13 @@ const Home = () => {
|
|||||||
const { page } = Route.useSearch();
|
const { page } = Route.useSearch();
|
||||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((page ?? 0) >= postsQuery.pages) {
|
||||||
|
navigate({ to: '/', search: { page: postsQuery.pages - 1 } });
|
||||||
|
}
|
||||||
|
}, [page]); //eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ export interface PostAuth {
|
|||||||
postedAt: Timestamp;
|
postedAt: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PostNew {
|
||||||
|
pages: number;
|
||||||
|
post: PostAuth;
|
||||||
|
}
|
||||||
|
export interface PostDelete extends PostNew {}
|
||||||
|
|
||||||
export interface PostListAuth {
|
export interface PostListAuth {
|
||||||
pages: number;
|
pages: number;
|
||||||
data: PostNonAuth[];
|
data: PostNonAuth[];
|
||||||
|
|||||||
@@ -21,3 +21,8 @@ export interface Login {
|
|||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserImageUpdate {
|
||||||
|
image?: File;
|
||||||
|
predefined?: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user