kemono2/client/src/pages/posts.tsx
2025-04-02 16:32:47 +02:00

162 lines
4.3 KiB
TypeScript

import { LoaderFunctionArgs, useLoaderData } from "react-router";
import { parseOffset } from "#lib/pagination";
import { createPostsPageURL } from "#lib/urls";
import { fetchPosts } from "#api/posts";
import { PageSkeleton } from "#components/pages";
import { Paginator } from "#components/pagination";
import { FooterAd, HeaderAd, SliderAd } from "#components/advs";
import { CardList, PostCard } from "#components/cards";
import { FormRouter, FormSection } from "#components/forms";
import { IPost } from "#entities/posts";
import { findFavouritePosts, findFavouriteProfiles } from "#entities/account";
interface IProps {
count: number;
trueCount: number;
offset?: number;
posts: IPost[];
query?: string;
tags?: string[];
}
export function PostsPage() {
const { count, trueCount, offset, query, posts, tags } =
useLoaderData() as IProps;
const title = "Posts";
const heading = "Posts";
return (
<PageSkeleton name="posts" title={title} heading={heading}>
<div className="paginator" id="paginator-top">
<Paginator
count={count}
true_count={trueCount}
offset={offset}
constructURL={(offset) =>
String(createPostsPageURL(offset, query, tags))
}
/>
<FormRouter method="GET">
<FormSection>
<input
id="q"
className="search-input"
type="text"
name="q"
aria-autocomplete="none"
defaultValue={query}
minLength={2}
placeholder="search for posts..."
/>
</FormSection>
</FormRouter>
</div>
<SliderAd />
<HeaderAd />
<CardList>
{count === 0 ? (
<div className="card-list__item--no-results">
<h2 className="subtitle">Nobody here but us chickens!</h2>
<p className="subtitle">There are no posts for your query.</p>
</div>
) : (
posts.map((post) => (
<PostCard
key={`${post.id}-${post.service}`}
post={post}
isFavourite={post.isFavourite}
isFavouriteProfile={post.isFavouriteProfile}
/>
))
)}
</CardList>
<FooterAd />
<div className="paginator" id="paginator-bottom">
<Paginator
count={count}
true_count={trueCount}
offset={offset}
constructURL={(offset) =>
String(createPostsPageURL(offset, query, tags))
}
/>
</div>
</PageSkeleton>
);
}
export async function loader({ request }: LoaderFunctionArgs): Promise<IProps> {
const searchParams = new URL(request.url).searchParams;
let offset: number | undefined = undefined;
{
const parsedOffset = searchParams.get("o")?.trim();
if (parsedOffset) {
offset = parseOffset(parsedOffset);
}
}
const query = searchParams.get("q")?.trim();
const tags = searchParams.getAll("tag");
const { count, true_count, posts } = await fetchPosts(offset, query, tags);
const postsData = posts.map(({ service, user, id }) => ({
service,
user,
id,
}));
const profilesData = posts.reduce<{ service: string; id: string }[]>(
(profilesData, post) => {
const match = profilesData.find(
(profileData) =>
profileData.id === post.user && profileData.service === post.service
);
if (!match) {
profilesData.push({ service: post.service, id: post.user });
}
return profilesData;
},
[]
);
const favPosts = await findFavouritePosts(postsData);
const favProfiles = await findFavouriteProfiles(profilesData);
const postsWithFavs = posts.map<IPost>((post) => {
const isFavPost = Boolean(
favPosts.find(
({ service, user, id }) =>
id === post.id && user === post.user && service === post.service
)
);
const isFavProfile = Boolean(
favProfiles.find(
({ service, id }) => id === post.user && service === post.service
)
);
if (!isFavPost && !isFavProfile) {
return post;
}
return {
...post,
isFavourite: isFavPost,
isFavouriteProfile: isFavProfile,
};
});
return {
offset,
query,
tags,
count,
trueCount: true_count,
posts: postsWithFavs,
};
}