mirror of
https://github.com/lukevella/rallly.git
synced 2025-05-22 13:26:20 +02:00
164 lines
5.1 KiB
TypeScript
164 lines
5.1 KiB
TypeScript
import { trpc } from "@rallly/backend";
|
|
import { DotsHorizontalIcon, TrashIcon } from "@rallly/icons";
|
|
import clsx from "clsx";
|
|
import { useTranslation } from "next-i18next";
|
|
import * as React from "react";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
|
|
import { usePostHog } from "@/utils/posthog";
|
|
|
|
import { useDayjs } from "../../utils/dayjs";
|
|
import { requiredString } from "../../utils/form-validation";
|
|
import { Button } from "../button";
|
|
import CompactButton from "../compact-button";
|
|
import Dropdown, { DropdownItem } from "../dropdown";
|
|
import NameInput from "../name-input";
|
|
import TruncatedLinkify from "../poll/truncated-linkify";
|
|
import UserAvatar from "../poll/user-avatar";
|
|
import { usePoll } from "../poll-context";
|
|
import { isUnclaimed, useUser } from "../user-provider";
|
|
|
|
interface CommentForm {
|
|
authorName: string;
|
|
content: string;
|
|
}
|
|
|
|
const Discussion: React.FunctionComponent = () => {
|
|
const { dayjs } = useDayjs();
|
|
const { t } = useTranslation("app");
|
|
const { poll, admin } = usePoll();
|
|
|
|
const pollId = poll.id;
|
|
|
|
const { data: comments } = trpc.polls.comments.list.useQuery({ pollId });
|
|
const posthog = usePostHog();
|
|
|
|
const queryClient = trpc.useContext();
|
|
|
|
const addComment = trpc.polls.comments.add.useMutation({
|
|
onSuccess: () => {
|
|
queryClient.polls.comments.invalidate();
|
|
posthog?.capture("created comment");
|
|
},
|
|
});
|
|
|
|
const deleteComment = trpc.polls.comments.delete.useMutation({
|
|
onMutate: ({ commentId }) => {
|
|
queryClient.polls.comments.list.setData(
|
|
{ pollId },
|
|
(existingComments = []) => {
|
|
return [...existingComments].filter(({ id }) => id !== commentId);
|
|
},
|
|
);
|
|
},
|
|
onSuccess: () => {
|
|
posthog?.capture("deleted comment");
|
|
},
|
|
});
|
|
|
|
const session = useUser();
|
|
|
|
const { register, reset, control, handleSubmit, formState } =
|
|
useForm<CommentForm>({
|
|
defaultValues: {
|
|
authorName: "",
|
|
content: "",
|
|
},
|
|
});
|
|
|
|
if (!comments) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div className="overflow-hidden rounded-md border shadow-sm">
|
|
<div className="border-b bg-white p-3">
|
|
<div className="font-medium">{t("comments")}</div>
|
|
</div>
|
|
<div
|
|
className={clsx({
|
|
"bg-pattern space-y-3 border-b p-3": comments.length > 0,
|
|
})}
|
|
>
|
|
{comments.map((comment) => {
|
|
const canDelete =
|
|
admin || session.ownsObject(comment) || isUnclaimed(comment);
|
|
|
|
return (
|
|
<div className="flex" key={comment.id}>
|
|
<div
|
|
data-testid="comment"
|
|
className="w-fit rounded-md border bg-white px-3 py-2 shadow-sm"
|
|
>
|
|
<div className="flex items-center space-x-2">
|
|
<UserAvatar
|
|
name={comment.authorName}
|
|
showName={true}
|
|
isYou={session.ownsObject(comment)}
|
|
/>
|
|
<div className="mb-1">
|
|
<span className="mr-1 text-slate-500">•</span>
|
|
<span className="text-sm text-slate-500">
|
|
{dayjs(new Date(comment.createdAt)).fromNow()}
|
|
</span>
|
|
</div>
|
|
{canDelete && (
|
|
<Dropdown
|
|
placement="bottom-start"
|
|
trigger={<CompactButton icon={DotsHorizontalIcon} />}
|
|
>
|
|
<DropdownItem
|
|
icon={TrashIcon}
|
|
label={t("deleteComment")}
|
|
onClick={() => {
|
|
deleteComment.mutate({
|
|
commentId: comment.id,
|
|
});
|
|
}}
|
|
/>
|
|
</Dropdown>
|
|
)}
|
|
</div>
|
|
<div className="w-fit whitespace-pre-wrap">
|
|
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
<form
|
|
className="bg-white p-3"
|
|
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
|
await addComment.mutateAsync({ authorName, content, pollId });
|
|
reset({ authorName, content: "" });
|
|
})}
|
|
>
|
|
<textarea
|
|
id="comment"
|
|
placeholder={t("commentPlaceholder")}
|
|
className="input w-full py-2 pl-3 pr-4"
|
|
{...register("content", { validate: requiredString })}
|
|
/>
|
|
<div className="mt-1 flex space-x-3">
|
|
<div>
|
|
<Controller
|
|
name="authorName"
|
|
key={session.user?.id}
|
|
control={control}
|
|
rules={{ validate: requiredString }}
|
|
render={({ field }) => (
|
|
<NameInput {...field} className="w-full" />
|
|
)}
|
|
/>
|
|
</div>
|
|
<Button htmlType="submit" loading={formState.isSubmitting}>
|
|
{t("comment")}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default React.memo(Discussion);
|