Recent posts

This commit is contained in:
2024-07-27 23:02:17 +02:00
parent a950f6770a
commit 2c9f8caff4
11 changed files with 62 additions and 27 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
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">
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -1
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) => {
+14 -12
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')}
@@ -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>
); );
}; };
+2
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;
+6
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),
});
+10 -5
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 });
+13 -5
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 });
}, },