New picture link output, register modal

This commit is contained in:
Kilian Hofmann 2024-07-28 01:46:16 +02:00
parent 72a0ad6364
commit 215ed1bc7f
30 changed files with 402 additions and 59 deletions

File diff suppressed because one or more lines are too long

2
exam/dist/assets/i18n-DyW0LrNj.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

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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import"./react-DXd9vB-a.js";

1
exam/dist/assets/zustand-DA8f69qW.js vendored Normal file
View File

@ -0,0 +1 @@
import"./react-C_FdcE2X.js";

10
exam/dist/index.html vendored
View File

@ -5,11 +5,11 @@
<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-DAvRi6Od.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-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">
<script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-tFNBNNKb.js"></script>
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/react-C_FdcE2X.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/mui-C4H8cxTH.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/tanstack-Duf7jkFs.js">
<link rel="modulepreload" crossorigin href="/phpCourse/exam/dist/assets/i18n-DyW0LrNj.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">
</head>

View File

@ -20,6 +20,8 @@
"Unauthorized_postUpdate": "Keine Berechtigung",
"NotFound_post:postUpdate": "Post nicht gefunden",
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
"username": "Benutzername",
"email": "E-Mail",
"password": "Passwort",
@ -71,5 +73,14 @@
"Upload named": "{{name}} gewählt",
"Avatar": "Avatar {{name}}",
"Remove": "Entfernen",
"or": "oder"
"or": "oder",
"Leave comment header": "Du benötigst ein Konto um einen Kommentar zu hinterlassen.",
"Leave comment action": "Klicke oben rechts auf das Icon um dich anzumelden oder zu registrieren.",
"Register prompt": "<0>Noch kein Konto?</0> <1>Registriere</1> <2>dich jetzt!</2>",
"Register": "Konto anlegen",
"Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
"Close": "Schließen"
}

View File

@ -20,6 +20,8 @@
"Unauthorized_postUpdate": "Unauthorized",
"NotFound_post:postUpdate": "Post not found",
"Duplicate_user:register": "A user with this username or email already exists",
"username": "username",
"email": "email",
"password": "password",
@ -72,5 +74,14 @@
"Upload named": "{{name}} chosen",
"Avatar": "Avatar {{name}}",
"Remove": "Remove",
"or": "or"
"or": "or",
"Leave comment header": "You need an account to leave a Comment.",
"Leave comment action": "Click on the icon in the top right corner to log in or register.",
"Register prompt": "<0>No account yet?</0> <1>Register</1> <2>now!</2>",
"Register": "Create account",
"Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.",
"Close": "Close"
}

File diff suppressed because one or more lines are too long

View File

@ -20,6 +20,8 @@
"Unauthorized_postUpdate": "Keine Berechtigung",
"NotFound_post:postUpdate": "Post nicht gefunden",
"Duplicate_user:register": "Ein Benutzer mit diesem Benutzernamen oder E-Mail existiert schon",
"username": "Benutzername",
"email": "E-Mail",
"password": "Passwort",
@ -71,5 +73,14 @@
"Upload named": "{{name}} gewählt",
"Avatar": "Avatar {{name}}",
"Remove": "Entfernen",
"or": "oder"
"or": "oder",
"Leave comment header": "Du benötigst ein Konto um einen Kommentar zu hinterlassen.",
"Leave comment action": "Klicke oben rechts auf das Icon um dich anzumelden oder zu registrieren.",
"Register prompt": "<0>Noch kein Konto?</0> <1>Registriere</1> <2>dich jetzt!</2>",
"Register": "Konto anlegen",
"Confirm header": "Fast geschafft!",
"Confirm mail": "Prüfe dein E-Mail Postfach auf eine Bestätigungsmail.",
"Close": "Schließen"
}

View File

@ -20,6 +20,8 @@
"Unauthorized_postUpdate": "Unauthorized",
"NotFound_post:postUpdate": "Post not found",
"Duplicate_user:register": "A user with this username or email already exists",
"username": "username",
"email": "email",
"password": "password",
@ -72,5 +74,14 @@
"Upload named": "{{name}} chosen",
"Avatar": "Avatar {{name}}",
"Remove": "Remove",
"or": "or"
"or": "or",
"Leave comment header": "You need an account to leave a Comment.",
"Leave comment action": "Click on the icon in the top right corner to log in or register.",
"Register prompt": "<0>No account yet?</0> <1>Register</1> <2>now!</2>",
"Register": "Create account",
"Confirm header": "Almost there!",
"Confirm mail": "Check your email for a confirmation mail.",
"Close": "Close"
}

View File

@ -1,6 +1,6 @@
import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import { PostAuth, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserImageUpdate, UserUpdate } from '../types/User';
import { PostAuth, PostCreate, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserCreate, UserImageUpdate, UserUpdate } from '../types/User';
const BASE = 'https://khofmann.userpage.fu-berlin.de/phpCourse/exam/api/';
@ -88,8 +88,8 @@ class ApiImpl {
return user;
};
public newPost = async (data: PostUpdate): Promise<PostNew> => {
return await (await this.postAuth(`posts?l=${POST_LIMIT}`, { ...data } as Record<string, unknown>)).json();
public newPost = async (data: PostCreate): Promise<PostNew> => {
return await (await this.postAuth(`posts?l=${POST_LIMIT}`, data as unknown as Record<string, unknown>)).json();
};
public updatePost = async (data: PostUpdate, id: number): Promise<PostAuth> => {
@ -100,6 +100,10 @@ class ApiImpl {
return await (await this.getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`)).json();
};
public createUser = async (data: UserCreate): Promise<User> => {
return await (await this.post(`register`, data as unknown as Record<string, unknown>)).json();
};
/* Internal */
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {

View File

@ -0,0 +1,243 @@
import { Done } from '@mui/icons-material';
import {
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Grid,
TextField,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { useForm } from '@tanstack/react-form';
import { useMutation } from '@tanstack/react-query';
import { FC, FormEvent, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../../api/Api';
import { UserCreate } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
interface Props {
open: boolean;
onClose: () => void;
}
const RegisterDialog: FC<Props> = ({ open, onClose }) => {
//eslint-disable-next-line @typescript-eslint/no-explicit-any
const [error, setError] = useState<any>();
const createMutation = useMutation({
mutationFn: ({ data }: { data: UserCreate }) => {
return Api.createUser(data);
},
});
const { t } = useTranslation();
const theme = useTheme();
const fullScreen = useMediaQuery(theme.breakpoints.only('xs'), { noSsr: true });
const form = useForm<UserCreate>({
defaultValues: {
username: '',
email: '',
password: '',
},
onSubmit: async ({ value }) => {
try {
createMutation.mutate(
{ data: value },
{
onSuccess: () => setError(undefined),
onError: setError,
}
);
//eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
setError(error);
}
},
});
const handleClose = () => {
form.reset();
setError(undefined);
onClose();
};
if (createMutation.isSuccess)
return (
<Dialog open={open} onClose={handleClose} fullWidth fullScreen={fullScreen}>
<DialogContent>
<Grid container spacing={2}>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Done color="action" sx={{ fontSize: '200px' }} />
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography variant="h5">{t('Confirm header')}</Typography>
</Grid>
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Typography>{t('Confirm mail')}</Typography>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>{t('Close')}</Button>
</DialogActions>
</Dialog>
);
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('Register')}</DialogTitle>
<DialogContent sx={{ gap: 2 }}>
<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
variant="outlined"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
size="small"
label={t('Username')}
required
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
autoComplete="new-username"
fullWidth
margin="dense"
/>
</>
);
}}
/>
</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
variant="outlined"
name={field.name}
value={field.state.value}
onBlur={field.handleBlur}
onChange={(e) => field.handleChange(e.target.value)}
size="small"
label={t('Email')}
required
error={field.state.meta.isTouched && field.state.meta.errors.length > 0}
helperText={field.state.meta.isTouched ? field.state.meta.errors.join(',') : ''}
type="email"
autoComplete="new-username"
inputMode="email"
fullWidth
/>
</>
);
}}
/>
</Grid>
<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.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
children={([canSubmit]) => (
<>
<Button
type="submit"
disabled={!canSubmit || createMutation.isPending}
variant="contained"
endIcon={createMutation.isPending && <CircularProgress color="inherit" size="20px" />}
>
{t('Register')}
</Button>
</>
)}
/>
</Grid>
<Grid item xs={12}>
{error && <ErrorComponent error={error} context="register" />}
</Grid>
</Grid>
</DialogContent>
</Dialog>
);
};
export default RegisterDialog;

View File

@ -25,7 +25,6 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { FC, FormEvent, useState } from 'react';
import Api from '../../../api/Api';
import { STORAGE_PATH } from '../../../constanst';
import { User, UserImageUpdate } from '../../../types/User';
import ErrorComponent from '../../Error/ErrorComponent';
@ -110,8 +109,8 @@ const UserImageDialog: FC<Props> = ({ user, open, onClose }) => {
formState.image
? URL.createObjectURL(formState.image)
: formState.predefined
? `${STORAGE_PATH}profilbilder/default/${formState.predefined}.svg`
: `${STORAGE_PATH}${user.image}`
? `profilbilder/default/${formState.predefined}.svg`
: `${user.image}`
}
sx={{ width: '100px', height: '100px' }}
>

View File

@ -13,8 +13,6 @@ interface Props {
const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) => {
const { t } = useTranslation();
console.log(error, context);
if (!error) return null;
if (error.code) {
@ -35,6 +33,8 @@ const ErrorComponent: FC<Props> = ({ error, context, color = 'error.main' }) =>
{t(error.code, { context: `${field}:${context}` })}
</Typography>
));
case ERRORS.DUPLICATE:
return <Typography color={color}>{t(error.code, { context: `${error.entity}:${context}` })}</Typography>;
}
}

View File

@ -3,4 +3,5 @@ export enum ERRORS {
UNAUTHORIZED = 'Unauthorized',
FAILED_UPDATE = 'FailedUpdate',
MISSING_FIELD = 'MissingField',
DUPLICATE = 'Duplicate',
}

View File

@ -5,7 +5,7 @@ import { useNavigate } from '@tanstack/react-router';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../../api/Api';
import { PostUpdate } from '../../../types/Post';
import { PostCreate } from '../../../types/Post';
import ErrorComponent from '../../Error/ErrorComponent';
const PostForm: FC = () => {
@ -18,12 +18,12 @@ const PostForm: FC = () => {
const navigate = useNavigate();
const newMutation = useMutation({
mutationFn: ({ data }: { data: PostUpdate }) => {
mutationFn: ({ data }: { data: PostCreate }) => {
return Api.newPost(data);
},
});
const form = useForm<PostUpdate>({
const form = useForm<PostCreate>({
defaultValues: {
content: '',
},

View File

@ -13,7 +13,6 @@ import { Link, useRouterState } from '@tanstack/react-router';
import { cloneElement, FC, ReactElement, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../api/Api';
import { STORAGE_PATH } from '../../constanst';
import { User } from '../../types/User';
import LanguageMenu from '../Menus/Language/LanguageMenu';
import UserMenu from '../Menus/User/UserMenu';
@ -62,7 +61,7 @@ const Header: FC = () => {
</IconButton>
{user ? (
<IconButton onClick={(event) => setAnchorUserMenu(event.currentTarget)} sx={{ p: 0 }}>
<Avatar alt={user.username} src={`${STORAGE_PATH}/${user.image}`}>
<Avatar alt={user.username} src={`${user.image}`}>
<Person />
</Avatar>
</IconButton>

View File

@ -1,9 +1,11 @@
import { Menu, MenuItem } from '@mui/material';
import { Box, Link, Menu, MenuItem, Typography } from '@mui/material';
import { useMatch, useNavigate, useRouter } from '@tanstack/react-router';
import { t } from 'i18next';
import { FC } from 'react';
import { FC, useState } from 'react';
import { Trans } from 'react-i18next/TransWithoutContext';
import Api from '../../../api/Api';
import { ROUTES } from '../../../types/Routes';
import RegisterDialog from '../../Dialogs/Register/RegisterDialog';
import LoginForm from '../../Forms/Login/LoginForm';
interface Props {
@ -12,12 +14,19 @@ interface Props {
}
const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
const [register, setRegister] = useState(false);
const navigate = useNavigate();
const router = useRouter();
const match = useMatch({ from: '/profile/', strict: true, shouldThrow: false });
const user = Api.getAuthenticatedUser();
const _handleClose = () => {
setRegister(false);
handleClose();
};
return (
<Menu
anchorEl={anchorEl}
@ -31,7 +40,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={handleClose}
onClose={_handleClose}
sx={{
'& .MuiMenu-paper': {
minWidth: '240px',
@ -45,7 +54,7 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
key="profile"
onClick={() => {
navigate({ to: ROUTES.PROFILE });
handleClose();
_handleClose();
}}
>
{t('Profile')}
@ -55,14 +64,35 @@ const UserMenu: FC<Props> = ({ anchorEl, handleClose }) => {
onClick={async () => {
await Api.logOut();
router.invalidate();
handleClose();
_handleClose();
}}
>
{t('Log out')}
</MenuItem>,
]
) : register ? (
<>
<RegisterDialog open={register} onClose={() => setRegister(false)} />
</>
) : (
<LoginForm handleClose={handleClose} />
<>
<LoginForm handleClose={_handleClose} />
<Box sx={{ padding: 1 }}>
<Trans i18nKey="Register prompt">
<Typography component="span" />
<Link
sx={{ cursor: 'pointer' }}
variant="body1"
underline="hover"
onClick={() => {
setRegister(true);
handleClose();
}}
/>
<Typography component="span" />
</Trans>
</Box>
</>
)}
</Menu>
);

View File

@ -21,7 +21,6 @@ import { Link, useNavigate } from '@tanstack/react-router';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import Api from '../../api/Api';
import { STORAGE_PATH } from '../../constanst';
import { PostAuth, PostNonAuth } from '../../types/Post';
import convertDate from '../../utils/date';
import PostEditDialog from '../Dialogs/PostEdit/PostEditDialog';
@ -55,19 +54,19 @@ const Post: FC<Props> = ({ post, disableActions }) => {
!disableActions && 'id' in post.user ? (
post.user.id !== Api.getAuthenticatedUser()?.id ? (
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
<Avatar alt={post.user.username} src={`${STORAGE_PATH}${post.user.image}`}>
<Avatar alt={post.user.username} src={`${post.user.image}`}>
<Person />
</Avatar>
</MUILink>
) : (
<MUILink component={Link} to="/profile">
<Avatar alt={post.user.username} src={`${STORAGE_PATH}${post.user.image}`}>
<Avatar alt={post.user.username} src={`${post.user.image}`}>
<Person />
</Avatar>
</MUILink>
)
) : (
<Avatar alt={post.user.username} src={`${STORAGE_PATH}${post.user.image}`}>
<Avatar alt={post.user.username} src={`${post.user.image}`}>
<Person />
</Avatar>
)

View File

@ -19,7 +19,6 @@ import convertDate from '../../utils/date';
import UserEditDialog from '../Dialogs/UserEdit/UserEditDialog';
import UserImageDialog from '../Dialogs/UserImage/UserImageDialog';
import Post from '../Post/Post';
import { STORAGE_PATH } from '../../constanst';
interface Props {
user: User;
@ -41,7 +40,7 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
<Grid container spacing={2}>
<Grid item sx={{ display: 'flex', flexGrow: 1, justifyContent: 'center' }}>
<IconButton onClick={() => setImageOpen(true)}>
<Avatar alt={user.username} src={`${STORAGE_PATH}${user.image}`} sx={{ width: '100px', height: '100px' }}>
<Avatar alt={user.username} src={`${user.image}`} sx={{ width: '100px', height: '100px' }}>
<Person sx={{ width: '60px', height: '60px' }} />
</Avatar>
</IconButton>
@ -79,7 +78,7 @@ const Profile: FC<Props> = ({ user, posts, canEdit }) => {
</Divider>
</Grid>
{posts.map((post) => (
<Grid item xs={12}>
<Grid key={`post_${post.id}`} item xs={12}>
<Post post={post} disableActions />
</Grid>
))}

View File

@ -1,4 +1,3 @@
export const POST_LIMIT = 15;
export const PROFILE_POST_LIMIT = 3;
export const POST_CHAR_LIMIT = 250;
export const STORAGE_PATH = '/phpCourse/exam/storage/';

View File

@ -1,4 +1,13 @@
import { Divider, Grid, Pagination, PaginationItem, Snackbar } from '@mui/material';
import {
Divider,
Grid,
Pagination,
PaginationItem,
Snackbar,
Typography,
useMediaQuery,
useTheme,
} from '@mui/material';
import { useSuspenseQuery } from '@tanstack/react-query';
import { createFileRoute, Link, useNavigate } from '@tanstack/react-router';
import { useEffect } from 'react';
@ -14,6 +23,8 @@ const Home = () => {
const { data: postsQuery, isFetching } = useSuspenseQuery(postsQueryOptions(page));
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
const oneSibling = useMediaQuery(theme.breakpoints.not('xs'), { noSsr: true });
useEffect(() => {
if ((page ?? 0) >= postsQuery.pages) {
@ -33,15 +44,11 @@ const Home = () => {
<Grid item xs={12}>
<Divider variant="middle" />
</Grid>
{Api.hasAuth() && (
<Grid item xs={12}>
<PostForm />
</Grid>
)}
<Grid item xs={12} sx={{ display: 'flex', justifyContent: 'center' }}>
<Pagination
page={(page ?? 0) + 1}
count={postsQuery.pages}
siblingCount={oneSibling ? 1 : 0}
color="primary"
renderItem={(item) => (
<PaginationItem
@ -55,6 +62,16 @@ const Home = () => {
)}
/>
</Grid>
{Api.hasAuth() ? (
<Grid item xs={12}>
<PostForm />
</Grid>
) : (
<Grid item xs={12}>
<Typography variant="h5">{t('Leave comment header')}</Typography>
<Typography>{t('Leave comment action')}</Typography>
</Grid>
)}
</Grid>
</>
);

View File

@ -13,7 +13,6 @@ const ProfilePage = () => {
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(
profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0)
);
console.log(profilePostsQuery.data);
return (
<>

View File

@ -37,3 +37,7 @@ export interface PostListAuth {
export interface PostUpdate {
content?: string;
}
export interface PostCreate {
content: string;
}

View File

@ -17,6 +17,12 @@ export interface UserUpdate {
password?: string;
}
export interface UserCreate {
username: string;
email: string;
password: string;
}
export interface Login {
email: string;
password: string;