kemono2/client/src/entities/posts/overview/footer.tsx
2025-04-11 00:54:15 +02:00

187 lines
5.3 KiB
TypeScript

import clsx from "clsx";
import { Suspense } from "react";
import { Await } from "react-router";
import MicroModal from "micromodal";
import { ICONS_PREPEND } from "#env/env-vars";
import { Image } from "#components/images";
import { KemonoLink, LocalLink } from "#components/links";
import { Timestamp } from "#components/dates";
import { Preformatted } from "#components/formatting";
import { IComment } from "#entities/posts";
import * as styles from "./overview.module.scss";
import { IPostOverviewProps } from "./types";
interface IPostFooterProps extends Pick<IPostOverviewProps, "comments"> {
service: string;
profileID: string;
profileName?: string;
}
export function PostFooter({
comments,
service,
profileID,
profileName,
}: IPostFooterProps) {
return (
<footer className="post__footer">
<h2 className="site-section__subheading">Comments</h2>
{/* TODO: comment filters */}
<Suspense fallback={<p>Loading comments...</p>}>
<Await resolve={comments} errorElement={<></>}>
{(comments: IComment[]) => (
<div
className={clsx(
"post__comments",
!comments && "post__comments--no-comments"
)}
>
{!comments ? (
<p className="subtitle">No comments found for this post.</p>
) : (
comments.map((comment) => (
<PostComment
comment={comment}
service={service}
postProfileID={profileID}
postProfileName={profileName}
/>
))
)}
</div>
)}
</Await>
</Suspense>
</footer>
);
}
interface IPostCommentProps {
comment: IComment;
postProfileID: string;
postProfileName?: string;
service: string;
}
function PostComment({
comment,
postProfileID,
postProfileName,
service,
}: IPostCommentProps) {
const {
id,
commenter,
commenter_name,
revisions,
parent_id,
content,
published,
} = comment;
const isProfileComment = commenter === postProfileID;
const modalID = `comment-revisions-${id}`;
return (
<article
className={clsx("comment", isProfileComment && "comment--user")}
id={id}
>
<header className="comment__header">
{!isProfileComment ? (
<LocalLink id={id} className="comment__name">
{commenter_name ?? "Anonymous"}
</LocalLink>
) : (
<>
{/* TODO: a proper local link */}
<KemonoLink url={id} className="comment__icon">
<Image
src={`${ICONS_PREPEND}/icons/${service}/${postProfileID}`}
/>
</KemonoLink>
<LocalLink id={id} className="comment__name">
{postProfileName ?? "Post's profile"}
</LocalLink>
</>
)}
{revisions && revisions.length !== 0 && (
<>
<span
className="comment-has-revisions fancy-link"
data-micromodal-trigger={modalID}
onClick={() => MicroModal.show(modalID)}
>
(
<a className="fancy-link" href={`#${id}`}>
edited
</a>
)
</span>
<div className="comment-revisions-dialog" id={modalID}>
<div tabIndex={-1} data-micromodal-close>
<div
role="dialog"
aria-modal="true"
aria-labelledby={`comment-revisions-${id}-title`}
>
<section>
<header>
<h3 id={`comment-revisions-${id}-title`}>
Comment edits
</h3>
<button
data-micromodal-close
onClick={() => MicroModal.close(modalID)}
></button>
</header>
<div id={`comment-revisions-${id}-content`}>
{[...revisions, comment].map((revision) => (
<article
id={`comment-${comment.id}-revision-${revision.id}`}
>
<span className="timestamp">
{revision.published ?? revision.added}
</span>
<span className="prose">{revision.content}</span>
</article>
))}
</div>
</section>
</div>
</div>
</div>
</>
)}
</header>
<section className="comment__body">
{parent_id && (
<div className="comment__reply">
<a href={`#${parent_id}`}>
{">>"}
{parent_id}
</a>
</div>
)}
<Preformatted>
<p className="comment__message">
{service !== "boosty" ? (
content
) : (
<Preformatted className={styles.content} dangerouslySetInnerHTML={{__html: content}}/>
)}
</p>
</Preformatted>
</section>
<footer className="comment__footer">
<Timestamp time={published} />
</footer>
</article>
);
}