Profile Image edit
This commit is contained in:
parent
15ca7e8879
commit
627654e0a7
1
exam/dist/assets/index-B5V9Za9P.js
vendored
Normal file
1
exam/dist/assets/index-B5V9Za9P.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
exam/dist/assets/index-CG0WySTu.js
vendored
1
exam/dist/assets/index-CG0WySTu.js
vendored
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
6
exam/dist/index.html
vendored
6
exam/dist/index.html
vendored
@ -5,10 +5,10 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/phpCourse/exam/dist/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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/mui-v3E5hT34.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-yMrSYl0u.js">
|
||||
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-CxHUbSMi.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="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/mui-CKDNpdid.css">
|
||||
<link rel="stylesheet" crossorigin href="/phpCourse/exam/dist/assets/index-D83Ey19k.css">
|
||||
|
||||
13
exam/dist/locales/de/translation.json
vendored
13
exam/dist/locales/de/translation.json
vendored
@ -60,5 +60,16 @@
|
||||
|
||||
"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"
|
||||
}
|
||||
|
||||
13
exam/dist/locales/en/translation.json
vendored
13
exam/dist/locales/en/translation.json
vendored
@ -61,5 +61,16 @@
|
||||
|
||||
"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"
|
||||
}
|
||||
|
||||
2
exam/dist/stats.html
vendored
2
exam/dist/stats.html
vendored
File diff suppressed because one or more lines are too long
@ -60,5 +60,16 @@
|
||||
|
||||
"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",
|
||||
|
||||
"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 { PostAuth, PostListAuth, PostListNonAuth, PostUpdate } from '../types/Post';
|
||||
import { User, UserUpdate } from '../types/User';
|
||||
import { PostAuth, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
|
||||
import { User, UserImageUpdate, UserUpdate } from '../types/User';
|
||||
|
||||
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
|
||||
|
||||
@ -51,8 +51,8 @@ class ApiImpl {
|
||||
return await (await this.get(url)).json();
|
||||
};
|
||||
|
||||
public deletePost = async (id: number): Promise<PostAuth> => {
|
||||
return await (await this.delete(`posts/${id}`)).json();
|
||||
public deletePost = async (id: number): Promise<PostDelete> => {
|
||||
return await (await this.delete(`posts/${id}?l=${POST_LIMIT}`)).json();
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
public newPost = async (data: PostUpdate): Promise<PostAuth> => {
|
||||
return await (await this.postAuth(`posts`, data as Record<string, unknown>)).json();
|
||||
public updateUserImage = async (data: UserImageUpdate, id?: number): Promise<User> => {
|
||||
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> => {
|
||||
@ -95,6 +102,17 @@ class ApiImpl {
|
||||
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) => {
|
||||
const response = await fetch(`${BASE}${endpoint}`, {
|
||||
mode: 'cors',
|
||||
|
||||
226
exam/react/src/components/Dialogs/UserImage/UserImageDialog.tsx
Normal file
226
exam/react/src/components/Dialogs/UserImage/UserImageDialog.tsx
Normal file
@ -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 { useForm } from '@tanstack/react-form';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { FC, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Api from '../../../api/Api';
|
||||
@ -14,6 +15,7 @@ const PostForm: FC = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const newMutation = useMutation({
|
||||
mutationFn: ({ data }: { data: PostUpdate }) => {
|
||||
@ -30,8 +32,10 @@ const PostForm: FC = () => {
|
||||
newMutation.mutate(
|
||||
{ data: value },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['posts'] });
|
||||
onSuccess: async (data) => {
|
||||
form.reset();
|
||||
await queryClient.invalidateQueries({ queryKey: ['posts'] });
|
||||
navigate({ to: '/', search: { page: data.pages - 1 } });
|
||||
},
|
||||
onError: setError,
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ import {
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import Api from '../../api/Api';
|
||||
@ -42,9 +42,9 @@ const Post: FC<Props> = ({ post }) => {
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -121,10 +121,11 @@ const Post: FC<Props> = ({ post }) => {
|
||||
color="error"
|
||||
onClick={() => {
|
||||
deleteMutation.mutate(post.id, {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
onSuccess: async (data) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ['posts'],
|
||||
});
|
||||
navigate({ to: '/', search: { page: data.pages - 1 } });
|
||||
},
|
||||
onError: setError,
|
||||
});
|
||||
|
||||
@ -1,10 +1,22 @@
|
||||
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 { useTranslation } from 'react-i18next';
|
||||
import { User } from '../../types/User';
|
||||
import convertDate from '../../utils/date';
|
||||
import UserEditDialog from '../Dialogs/UserEdit/UserEditDialog';
|
||||
import UserImageDialog from '../Dialogs/UserImage/UserImageDialog';
|
||||
|
||||
interface Props {
|
||||
user: User;
|
||||
@ -13,6 +25,7 @@ interface Props {
|
||||
|
||||
const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [imageOpen, setImageOpen] = useState(false);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -23,9 +36,11 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
<CardContent>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
|
||||
<Avatar alt={user.username} src={`storage/${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||
<Person sx={{ width: '60px', height: '60px' }} />
|
||||
</Avatar>
|
||||
<IconButton onClick={() => setImageOpen(true)}>
|
||||
<Avatar alt={user.username} src={`storage/>${user.image}`} sx={{ width: '100px', height: '100px' }}>
|
||||
<Person sx={{ width: '60px', height: '60px' }} />
|
||||
</Avatar>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: '120px 1fr', columnGap: 1 }}>
|
||||
@ -43,12 +58,15 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
|
||||
</CardContent>
|
||||
<CardActions>
|
||||
{canEdit && (
|
||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
<>
|
||||
<Button size="small" onClick={() => setEditOpen(true)}>
|
||||
{t('Edit')}
|
||||
</Button>
|
||||
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||
<UserImageDialog user={user} open={imageOpen} onClose={() => setImageOpen(false)} />
|
||||
</>
|
||||
)}
|
||||
</CardActions>
|
||||
<UserEditDialog user={user} open={editOpen} onClose={() => setEditOpen(false)} />
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { Divider, Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
|
||||
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 Api from '../api/Api';
|
||||
import PostForm from '../components/Forms/Post/PostForm';
|
||||
@ -12,6 +13,13 @@ const Home = () => {
|
||||
const { page } = Route.useSearch();
|
||||
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
|
||||
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 (
|
||||
<>
|
||||
|
||||
@ -23,6 +23,12 @@ export interface PostAuth {
|
||||
postedAt: Timestamp;
|
||||
}
|
||||
|
||||
export interface PostNew {
|
||||
pages: number;
|
||||
post: PostAuth;
|
||||
}
|
||||
export interface PostDelete extends PostNew {}
|
||||
|
||||
export interface PostListAuth {
|
||||
pages: number;
|
||||
data: PostNonAuth[];
|
||||
|
||||
@ -21,3 +21,8 @@ export interface Login {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UserImageUpdate {
|
||||
image?: File;
|
||||
predefined?: string;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user