Recent posts

This commit is contained in:
Kilian Hofmann 2024-07-27 23:02:17 +02:00
parent a950f6770a
commit 2c9f8caff4
11 changed files with 62 additions and 27 deletions

1
exam/dist/assets/index-B1n77CT9.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,7 +5,7 @@
<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-Bw9FCd3M.js"></script> <script type="module" crossorigin src="/phpCourse/exam/dist/assets/index-B1n77CT9.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-CxHUbSMi.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/tanstack-xmxrKlZO.js">

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
import { POST_LIMIT } from '../constanst'; import { POST_LIMIT, PROFILE_POST_LIMIT } from '../constanst';
import { PostAuth, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post'; import { PostAuth, PostDelete, PostListAuth, PostListNonAuth, PostNew, PostUpdate } from '../types/Post';
import { User, UserImageUpdate, UserUpdate } from '../types/User'; import { User, UserImageUpdate, UserUpdate } from '../types/User';
@ -96,6 +96,10 @@ class ApiImpl {
return await (await this.patch(`posts/${id}`, data as Record<string, unknown>)).json(); return await (await this.patch(`posts/${id}`, data as Record<string, unknown>)).json();
}; };
public userPosts = async (id?: number): Promise<PostListAuth> => {
return await (await this.getAuth(`users/${id}/posts?l=${PROFILE_POST_LIMIT}&s=desc`)).json();
};
/* Internal */ /* Internal */
private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => { private post = async (endpoint: string, body?: Record<string, unknown>, headers?: HeadersInit) => {

View File

@ -28,9 +28,10 @@ import ErrorComponent from '../Error/ErrorComponent';
interface Props { interface Props {
post: PostNonAuth | PostAuth; post: PostNonAuth | PostAuth;
disableActions?: boolean;
} }
const Post: FC<Props> = ({ post }) => { const Post: FC<Props> = ({ post, disableActions }) => {
const [deleteOpen, setDeleteOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
@ -50,7 +51,7 @@ const Post: FC<Props> = ({ post }) => {
<Card> <Card>
<CardHeader <CardHeader
avatar={ avatar={
'id' in post.user ? ( !disableActions && 'id' in post.user ? (
post.user.id !== Api.getAuthenticatedUser()?.id ? ( post.user.id !== Api.getAuthenticatedUser()?.id ? (
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}> <MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
<Avatar alt={post.user.username} src={`storage/${post.user.image}`}> <Avatar alt={post.user.username} src={`storage/${post.user.image}`}>
@ -71,7 +72,7 @@ const Post: FC<Props> = ({ post }) => {
) )
} }
title={ title={
'id' in post.user ? ( !disableActions && 'id' in post.user ? (
post.user.id !== Api.getAuthenticatedUser()?.id ? ( post.user.id !== Api.getAuthenticatedUser()?.id ? (
<MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}> <MUILink component={Link} to="/profile/$id" params={{ id: post.user.id }}>
{post.user.username} {post.user.username}
@ -94,15 +95,16 @@ const Post: FC<Props> = ({ post }) => {
</CardContent> </CardContent>
<CardActions> <CardActions>
{(Api.isAdmin() || ('id' in post.user && post.user.id === Api.getAuthenticatedUser()?.id)) && ( {!disableActions &&
<> (Api.isAdmin() || ('id' in post.user && post.user.id === Api.getAuthenticatedUser()?.id)) && (
<Button size="small" onClick={() => setEditOpen(true)}> <>
{t('Edit')} <Button size="small" onClick={() => setEditOpen(true)}>
</Button> {t('Edit')}
<PostEditDialog post={post as PostAuth} open={editOpen} onClose={() => setEditOpen(false)} /> </Button>
</> <PostEditDialog post={post as PostAuth} open={editOpen} onClose={() => setEditOpen(false)} />
)} </>
{Api.isAdmin() && ( )}
{!disableActions && Api.isAdmin() && (
<> <>
<Button size="small" color="error" onClick={() => setDeleteOpen(true)}> <Button size="small" color="error" onClick={() => setDeleteOpen(true)}>
{t('Delete')} {t('Delete')}

View File

@ -13,17 +13,20 @@ import {
} from '@mui/material'; } from '@mui/material';
import { FC, useState } from 'react'; import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PostAuth } from '../../types/Post';
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'; import UserImageDialog from '../Dialogs/UserImage/UserImageDialog';
import Post from '../Post/Post';
interface Props { interface Props {
user: User; user: User;
posts: PostAuth[];
canEdit?: boolean; canEdit?: boolean;
} }
const Profile: FC<Props> = ({ user, canEdit }) => { const Profile: FC<Props> = ({ user, posts, canEdit }) => {
const [editOpen, setEditOpen] = useState(false); const [editOpen, setEditOpen] = useState(false);
const [imageOpen, setImageOpen] = useState(false); const [imageOpen, setImageOpen] = useState(false);
@ -74,6 +77,11 @@ const Profile: FC<Props> = ({ user, canEdit }) => {
<Typography sx={{ opacity: 0.36 }}>{t('Recent posts')}</Typography> <Typography sx={{ opacity: 0.36 }}>{t('Recent posts')}</Typography>
</Divider> </Divider>
</Grid> </Grid>
{posts.map((post) => (
<Grid item xs={12}>
<Post post={post} disableActions />
</Grid>
))}
</Grid> </Grid>
); );
}; };

View File

@ -1 +1,3 @@
export const POST_LIMIT = 15; export const POST_LIMIT = 15;
export const PROFILE_POST_LIMIT = 3;
export const POST_CHAR_LIMIT = 250;

View File

@ -11,3 +11,9 @@ export const profileQueryOptions = (id?: number) =>
queryKey: ['profile', { id }], queryKey: ['profile', { id }],
queryFn: () => Api.user(id), queryFn: () => Api.user(id),
}); });
export const profilePostsQueryOptions = (id: number) =>
queryOptions({
queryKey: ['profilePosts', { id }],
queryFn: () => Api.userPosts(id),
});

View File

@ -4,17 +4,19 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
import { t } from 'i18next'; import { t } from 'i18next';
import Api from '../../api/Api'; import Api from '../../api/Api';
import Profile from '../../components/Profile/Profile'; import Profile from '../../components/Profile/Profile';
import { profileQueryOptions } from '../../queries/profileQuery'; import { profilePostsQueryOptions, profileQueryOptions } from '../../queries/profileQuery';
import { PostAuth } from '../../types/Post';
import { ROUTES } from '../../types/Routes'; import { ROUTES } from '../../types/Routes';
const ProfilePage = () => { const ProfilePage = () => {
const { id } = Route.useParams(); const { id } = Route.useParams();
const { data: profileQuery, isFetching } = useSuspenseQuery(profileQueryOptions(id)); const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileQueryOptions(id));
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(profilePostsQueryOptions(id));
return ( return (
<> <>
<Snackbar open={isFetching} message={t('Updating')} /> <Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
<Profile user={profileQuery} canEdit={Api.isAdmin()} /> <Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={Api.isAdmin()} />
</> </>
); );
}; };
@ -24,7 +26,10 @@ export const Route = createFileRoute(`${ROUTES.PROFILE}/$id`)({
parse: ({ id }) => ({ id: parseInt(id) }), parse: ({ id }) => ({ id: parseInt(id) }),
stringify: ({ id }) => ({ id: id.toString() }), stringify: ({ id }) => ({ id: id.toString() }),
}, },
loader: ({ context: { queryClient }, params: { id } }) => queryClient.ensureQueryData(profileQueryOptions(id)), loader: ({ context: { queryClient }, params: { id } }) => {
queryClient.ensureQueryData(profileQueryOptions(id));
queryClient.ensureQueryData(profilePostsQueryOptions(id));
},
beforeLoad: ({ params: { id } }) => { beforeLoad: ({ params: { id } }) => {
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX }); if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE }); if (id === Api.getAuthenticatedUser()?.id) throw redirect({ to: ROUTES.PROFILE });

View File

@ -4,22 +4,30 @@ import { createFileRoute, redirect } from '@tanstack/react-router';
import { t } from 'i18next'; import { t } from 'i18next';
import Api from '../../api/Api'; import Api from '../../api/Api';
import Profile from '../../components/Profile/Profile'; import Profile from '../../components/Profile/Profile';
import { profileSelfQueryOptions } from '../../queries/profileQuery'; import { profilePostsQueryOptions, profileSelfQueryOptions } from '../../queries/profileQuery';
import { PostAuth } from '../../types/Post';
import { ROUTES } from '../../types/Routes'; import { ROUTES } from '../../types/Routes';
const ProfilePage = () => { const ProfilePage = () => {
const { data: profileQuery, isFetching } = useSuspenseQuery(profileSelfQueryOptions); const { data: profileQuery, isFetching: isFetchingProfile } = useSuspenseQuery(profileSelfQueryOptions);
const { data: profilePostsQuery, isFetching: isFetchingPosts } = useSuspenseQuery(
profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0)
);
console.log(profilePostsQuery.data);
return ( return (
<> <>
<Snackbar open={isFetching} message={t('Updating')} /> <Snackbar open={isFetchingProfile || isFetchingPosts} message={t('Updating')} />
<Profile user={profileQuery} canEdit={true} /> <Profile user={profileQuery} posts={profilePostsQuery.data as PostAuth[]} canEdit={true} />
</> </>
); );
}; };
export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({ export const Route = createFileRoute(`${ROUTES.PROFILE}/`)({
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData(profileSelfQueryOptions), loader: ({ context: { queryClient } }) => {
queryClient.ensureQueryData(profileSelfQueryOptions);
queryClient.ensureQueryData(profilePostsQueryOptions(Api.getAuthenticatedUser()?.id ?? 0));
},
beforeLoad: () => { beforeLoad: () => {
if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX }); if (!Api.hasAuth()) throw redirect({ to: ROUTES.INDEX });
}, },