mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
⬆️ v3.0.0 (#704)
This commit is contained in:
parent
735056f25f
commit
c22b3abc4d
385 changed files with 19912 additions and 5250 deletions
|
@ -1,17 +1,26 @@
|
|||
import { trpc } from "@rallly/backend";
|
||||
import { DotsHorizontalIcon, TrashIcon } from "@rallly/icons";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
MessageCircleIcon,
|
||||
MoreHorizontalIcon,
|
||||
TrashIcon,
|
||||
} from "@rallly/icons";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@rallly/ui/dropdown-menu";
|
||||
import { Textarea } from "@rallly/ui/textarea";
|
||||
import dayjs from "dayjs";
|
||||
import { useTranslation } from "next-i18next";
|
||||
import * as React from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { Trans } from "@/components/trans";
|
||||
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";
|
||||
|
@ -24,13 +33,17 @@ interface CommentForm {
|
|||
}
|
||||
|
||||
const Discussion: React.FunctionComponent = () => {
|
||||
const { dayjs } = useDayjs();
|
||||
const { t } = useTranslation();
|
||||
const { poll, admin } = usePoll();
|
||||
|
||||
const pollId = poll.id;
|
||||
|
||||
const { data: comments } = trpc.polls.comments.list.useQuery({ pollId });
|
||||
const { data: comments } = trpc.polls.comments.list.useQuery(
|
||||
{ pollId },
|
||||
{
|
||||
staleTime: 1000 * 5,
|
||||
},
|
||||
);
|
||||
const posthog = usePostHog();
|
||||
|
||||
const queryClient = trpc.useContext();
|
||||
|
@ -66,97 +79,123 @@ const Discussion: React.FunctionComponent = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const [isWriting, setIsWriting] = React.useState(false);
|
||||
|
||||
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 className="divide-y">
|
||||
<div className="flex items-center gap-2 bg-gray-50 px-4 py-3 font-semibold">
|
||||
<MessageCircleIcon className="h-5 w-5" /> {t("comments")} (
|
||||
{comments.length})
|
||||
</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);
|
||||
{comments.length ? (
|
||||
<div className="space-y-4 p-4">
|
||||
{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>
|
||||
return (
|
||||
<div className="" key={comment.id}>
|
||||
<div data-testid="comment">
|
||||
<div className="mb-1 flex items-center space-x-2">
|
||||
<UserAvatar
|
||||
name={comment.authorName}
|
||||
showName={true}
|
||||
isYou={session.ownsObject(comment)}
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-sm ">
|
||||
<div className="text-gray-500">
|
||||
{dayjs(comment.createdAt).fromNow()}
|
||||
</div>
|
||||
{canDelete && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild={true}>
|
||||
<button className="hover:text-foreground text-gray-500">
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
deleteComment.mutate({
|
||||
commentId: comment.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="mr-2 h-4 w-4" />
|
||||
<Trans i18nKey="delete" />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-fit whitespace-pre-wrap pl-8 leading-relaxed">
|
||||
<TruncatedLinkify>{comment.content}</TruncatedLinkify>
|
||||
</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>
|
||||
) : null}
|
||||
<div className="p-3">
|
||||
{isWriting ? (
|
||||
<form
|
||||
className="space-y-2.5"
|
||||
onSubmit={handleSubmit(async ({ authorName, content }) => {
|
||||
await addComment.mutateAsync({ authorName, content, pollId });
|
||||
reset({ authorName, content: "" });
|
||||
setIsWriting(false);
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
<Textarea
|
||||
id="comment"
|
||||
autoFocus={true}
|
||||
placeholder={t("commentPlaceholder")}
|
||||
{...register("content", { validate: requiredString })}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<Controller
|
||||
name="authorName"
|
||||
key={session.user?.id}
|
||||
control={control}
|
||||
rules={{ validate: requiredString }}
|
||||
render={({ field }) => <NameInput {...field} />}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-between gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
reset();
|
||||
setIsWriting(false);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
loading={formState.isSubmitting}
|
||||
>
|
||||
<Trans defaults="Add Comment" i18nKey="addComment" />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<button
|
||||
className="border-input text-muted-foreground flex w-full rounded border bg-transparent px-3 py-2 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1"
|
||||
onClick={() => setIsWriting(true)}
|
||||
>
|
||||
<Trans i18nKey="commentPlaceholder" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue