256 lines
6.9 KiB
TypeScript
256 lines
6.9 KiB
TypeScript
import { LoaderFunctionArgs, redirect, useLoaderData } from "react-router";
|
|
import { createDiscordServerPageURL, createProfilePageURL } from "#lib/urls";
|
|
import { parseOffset } from "#lib/pagination";
|
|
import { ElementType } from "#lib/types";
|
|
import { fetchProfilePosts } from "#api/profiles";
|
|
import { FooterAd, SliderAd } from "#components/advs";
|
|
import { Paginator } from "#components/pagination";
|
|
import { CardList, PostCard } from "#components/cards";
|
|
import { ProfilePageSkeleton } from "#components/pages";
|
|
import { ButtonSubmit, FormRouter } from "#components/forms";
|
|
import {
|
|
ProfileHeader,
|
|
Tabs,
|
|
IArtistDetails,
|
|
getArtist,
|
|
} from "#entities/profiles";
|
|
import { paysites } from "#entities/paysites";
|
|
import { IPost } from "#entities/posts";
|
|
import { findFavouritePosts } from "#entities/account";
|
|
import { useRef, useState } from "react";
|
|
|
|
interface IProps {
|
|
profile: IArtistDetails;
|
|
postsData?: {
|
|
count: number;
|
|
offset?: number;
|
|
posts: (IPost & { isFavourite: boolean })[];
|
|
};
|
|
|
|
query?: string;
|
|
tags?: string[];
|
|
dmCount?: number;
|
|
hasLinks?: boolean;
|
|
}
|
|
|
|
export function ProfilePage() {
|
|
const { profile, postsData, query, tags, dmCount, hasLinks } =
|
|
useLoaderData() as IProps;
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { service, id, name } = profile;
|
|
const paysite = paysites[service];
|
|
const title = `Posts of "${name}" from "${paysite.title}"`;
|
|
|
|
return (
|
|
<ProfilePageSkeleton name="user" title={title} profile={profile}>
|
|
<SliderAd />
|
|
|
|
<ProfileHeader service={service} profileID={id} profileName={name} />
|
|
|
|
<div className="paginator" id="paginator-top">
|
|
<Tabs
|
|
currentPage="posts"
|
|
service={service}
|
|
artistID={id}
|
|
dmCount={dmCount}
|
|
hasLinks={hasLinks}
|
|
/>
|
|
|
|
{!(postsData && (postsData?.count !== 0 || query)) ? undefined : (
|
|
<>
|
|
<SearchForm
|
|
query={query}
|
|
onLoadingChange={(loading) => setIsLoading(loading)}
|
|
/>
|
|
<Paginator
|
|
offset={postsData.offset}
|
|
count={postsData.count}
|
|
constructURL={(offset) =>
|
|
String(
|
|
createProfilePageURL({
|
|
service,
|
|
profileID: id,
|
|
offset,
|
|
query,
|
|
tags,
|
|
})
|
|
)
|
|
}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{!postsData ? (
|
|
<div className="no-results">
|
|
<h2 className="site-section__subheading">
|
|
Nobody here but us chickens!
|
|
</h2>
|
|
<p className="subtitle">There are no posts for your query.</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<CardList className={isLoading ? "card-list--loading" : ""}>
|
|
{postsData.posts.map((post) => (
|
|
<PostCard
|
|
key={`${post.id}-${post.service}-${post.user}`}
|
|
post={post}
|
|
isServiceIconsDisabled={true}
|
|
isFavourite={post.isFavourite}
|
|
/>
|
|
))}
|
|
</CardList>
|
|
|
|
<FooterAd />
|
|
|
|
<div className="paginator" id="paginator-bottom">
|
|
<Paginator
|
|
offset={postsData.offset}
|
|
count={postsData.count}
|
|
constructURL={(offset) =>
|
|
String(
|
|
createProfilePageURL({
|
|
service,
|
|
profileID: id,
|
|
offset,
|
|
query,
|
|
tags,
|
|
})
|
|
)
|
|
}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</ProfilePageSkeleton>
|
|
);
|
|
}
|
|
|
|
interface ISearchFormProps
|
|
extends Pick<IProps, "query"> {
|
|
onLoadingChange: (loading: boolean) => void;
|
|
}
|
|
|
|
function SearchForm({ query, onLoadingChange }: ISearchFormProps) {
|
|
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
|
|
const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
|
|
const target = e.currentTarget as HTMLInputElement;
|
|
|
|
onLoadingChange(true);
|
|
|
|
timeoutRef.current = setTimeout(() => {
|
|
if (target.form) target.form.requestSubmit();
|
|
onLoadingChange(false);
|
|
}, 1000);
|
|
};
|
|
|
|
return (
|
|
<FormRouter
|
|
id="search-form"
|
|
className="search-form"
|
|
method="GET"
|
|
autoComplete="off"
|
|
noValidate={true}
|
|
acceptCharset="UTF-8"
|
|
statusSection={null}
|
|
submitButton={undefined}
|
|
style={{ maxWidth: "fit-content" }}
|
|
>
|
|
{(state) => (
|
|
<>
|
|
<div className="wrapper">
|
|
<input
|
|
type="text"
|
|
name="q"
|
|
id="q"
|
|
autoComplete="off"
|
|
defaultValue={query}
|
|
placeholder="Search..."
|
|
onChange={onInputChange}
|
|
/>
|
|
<ButtonSubmit
|
|
disabled={state === "loading" || state === "submitting"}
|
|
className="search-button"
|
|
onClick={() => {
|
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
onLoadingChange(false);
|
|
}}
|
|
>
|
|
<img src='/static/menu/search.svg' />
|
|
</ButtonSubmit>
|
|
</div>
|
|
</>
|
|
)}
|
|
</FormRouter>
|
|
);
|
|
}
|
|
|
|
export async function loader({
|
|
params,
|
|
request,
|
|
}: LoaderFunctionArgs): Promise<IProps | Response> {
|
|
const searchParams = new URL(request.url).searchParams;
|
|
const service = params.service?.trim() || "";
|
|
const profileID = params.creator_id?.trim() || "";
|
|
|
|
if (service === "discord") {
|
|
return redirect(String(createDiscordServerPageURL(profileID)));
|
|
}
|
|
|
|
const offsetParam = searchParams.get("o")?.trim();
|
|
const offset = offsetParam ? parseOffset(offsetParam) : undefined;
|
|
const query = searchParams.get("q")?.trim();
|
|
const tags = searchParams.getAll("tag");
|
|
|
|
const profile = await getArtist(service, profileID);
|
|
|
|
if (profileID == profile.public_id && profile.public_id != profile.id) {
|
|
return redirect(String(createProfilePageURL({ service, profileID: profile.id })));
|
|
}
|
|
|
|
const { props, results: posts } = await fetchProfilePosts(
|
|
service,
|
|
profileID,
|
|
offset,
|
|
query,
|
|
tags
|
|
);
|
|
const { count, dm_count, has_links } = props;
|
|
const hasLinks = !has_links || has_links === "0" ? false : true;
|
|
const favPostData = await findFavouritePosts(
|
|
posts.map(({ service, user, id }) => {
|
|
return {
|
|
service,
|
|
user,
|
|
id,
|
|
};
|
|
})
|
|
);
|
|
const finalPosts = posts.map<
|
|
ElementType<Required<IProps>["postsData"]["posts"]>
|
|
>((post) => {
|
|
const match = favPostData.find(
|
|
({ service, user, id }) =>
|
|
service === post.service && user === post.user && id === post.id
|
|
);
|
|
|
|
return { ...post, isFavourite: !match ? false : true };
|
|
});
|
|
|
|
return {
|
|
profile,
|
|
query,
|
|
tags,
|
|
postsData: {
|
|
count,
|
|
offset,
|
|
posts: finalPosts,
|
|
},
|
|
dmCount: dm_count,
|
|
hasLinks,
|
|
};
|
|
}
|