134 lines
5.7 KiB
TypeScript
134 lines
5.7 KiB
TypeScript
import { useLocation, useSubmit, useNavigation } from "react-router";
|
|
import clsx from "clsx";
|
|
import { useTranslation } from "react-i18next";
|
|
import ApiKeyBadge from "~/components/core/users/ApiKeyBadge";
|
|
import UserAvatarBadge from "~/components/core/users/UserAvatarBadge";
|
|
import UserBadge from "~/components/core/users/UserBadge";
|
|
import ChatAltIcon from "~/components/ui/icons/ChatAltIcon";
|
|
import ThumbsUpEmptyIcon from "~/components/ui/icons/ThumbsUpEmptyIcon";
|
|
import ThumbsUpFilledIcon from "~/components/ui/icons/ThumbsUpFilledIcon";
|
|
import TrashEmptyIcon from "~/components/ui/icons/TrashEmptyIcon";
|
|
import { useAppOrAdminData } from "~/utils/data/useAppOrAdminData";
|
|
import { LogWithDetails } from "~/utils/db/logs.db.server";
|
|
import { appendCSRFToken } from "~/utils/csrf";
|
|
import { renderSanitizedMarkdown } from "~/utils/html-sanitizer";
|
|
import { useLocaleFormatters } from "~/locale/format";
|
|
|
|
interface Props {
|
|
item: LogWithDetails;
|
|
}
|
|
export default function RowLogComment({ item }: Props) {
|
|
const { t } = useTranslation();
|
|
const formatters = useLocaleFormatters();
|
|
const submit = useSubmit();
|
|
const { user } = useAppOrAdminData();
|
|
const navigation = useNavigation();
|
|
const location = useLocation();
|
|
const isAdding = navigation.state === "submitting" && navigation.formData?.get("action") === "comment-reaction";
|
|
const isDeleting = navigation.state === "submitting" && navigation.formData?.get("action") === "comment-delete";
|
|
|
|
function onReacted(reaction: string) {
|
|
const form = new FormData();
|
|
appendCSRFToken(form);
|
|
form.set("action", "comment-reaction");
|
|
form.set("comment-id", item.comment?.id ?? "");
|
|
form.set("reaction", reaction);
|
|
submit(form, {
|
|
method: "post",
|
|
action: location.pathname + location.search,
|
|
});
|
|
}
|
|
|
|
function onDeleted() {
|
|
const form = new FormData();
|
|
appendCSRFToken(form);
|
|
form.set("action", "comment-delete");
|
|
form.set("comment-id", item.comment?.id ?? "");
|
|
submit(form, {
|
|
method: "post",
|
|
action: location.pathname + location.search,
|
|
});
|
|
}
|
|
|
|
function getReactions(reaction: string) {
|
|
return item.comment?.reactions.filter((f) => f.reaction === reaction) ?? [];
|
|
}
|
|
function hasMyReaction(reaction: string) {
|
|
return item.comment?.reactions.find((f) => f.createdByUserId === user?.id && f.reaction === reaction);
|
|
}
|
|
return (
|
|
<>
|
|
{item.comment && (
|
|
<>
|
|
<div className="relative px-1">
|
|
<UserAvatarBadge className="h-9 w-9" avatar={item.user?.avatar} />
|
|
|
|
<span className="bg-secondary absolute -bottom-0.5 -right-1 rounded-tl px-0.5 py-px">
|
|
<ChatAltIcon className="text-muted-foreground h-4 w-4" aria-hidden="true" />
|
|
</span>
|
|
</div>
|
|
<div className="group min-w-0 flex-1 space-y-0.5">
|
|
<div>
|
|
<div className="text-sm">
|
|
<div className="text-foreground font-medium">
|
|
{item.user && <UserBadge item={item.user} withEmail={false} />}
|
|
{item.apiKey && <ApiKeyBadge item={item.apiKey} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-muted-foreground text-sm font-normal">
|
|
{item.comment.isDeleted ? (
|
|
<p className="text-muted-foreground italic">{t("shared.commentDeleted")}</p>
|
|
) : (
|
|
<div className="prose border-border rounded-md border border-dashed p-2 text-sm">
|
|
<div dangerouslySetInnerHTML={{ __html: renderSanitizedMarkdown(item.comment.value) }} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="text-muted-foreground flex items-center justify-between space-x-1 font-medium">
|
|
<time dateTime={new Date(item.createdAt).toISOString()} title={formatters.dateTime(item.createdAt, { timeStyle: "medium" })}>
|
|
{formatters.relativeDate(item.createdAt)}
|
|
</time>
|
|
<div className="group flex items-center space-x-2">
|
|
{!item.comment.isDeleted && item.comment.createdByUserId === user?.id && (
|
|
<button
|
|
type="button"
|
|
disabled={isDeleting}
|
|
onClick={() => onDeleted()}
|
|
className="text-muted-foreground hover:text-muted-foreground invisible p-0.5 focus:outline-hidden group-hover:visible"
|
|
>
|
|
<div className="flex items-center space-x-1">
|
|
<TrashEmptyIcon className="h-4 w-4" />
|
|
</div>
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
disabled={isAdding}
|
|
onClick={() => onReacted("like")}
|
|
className={clsx(
|
|
"text-muted-foreground hover:text-muted-foreground p-0.5 focus:outline-hidden",
|
|
getReactions("like").length === 0 && "invisible group-hover:visible"
|
|
)}
|
|
>
|
|
<div
|
|
className="flex items-center space-x-1"
|
|
title={
|
|
getReactions("like")
|
|
?.map((f) => `${f.createdByUser?.firstName} ${f.createdByUser?.lastName}`)
|
|
.join(", ") ?? ""
|
|
}
|
|
>
|
|
{(getReactions("like")?.length ?? 0) > 0 && <div className="">{getReactions("like")?.length ?? 0}</div>}
|
|
{hasMyReaction("like") ? <ThumbsUpFilledIcon className="h-4 w-4" /> : <ThumbsUpEmptyIcon className="h-4 w-4" />}
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
}
|