1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-06-27 17:05:42 -04:00
Files
jarv.is/components/comments/comment-form.tsx
2025-05-14 09:49:55 -04:00

195 lines
6.8 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { getImageProps } from "next/image";
import { InfoIcon, Loader2Icon } from "lucide-react";
import { toast } from "sonner";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import Button from "@/components/ui/button";
import Textarea from "@/components/ui/textarea";
import Link from "@/components/link";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { MarkdownIcon } from "@/components/icons";
import { useSession } from "@/lib/auth-client";
import { createComment, updateComment } from "@/lib/server/comments";
import type { FormEvent } from "react";
const CommentForm = ({
slug,
parentId,
commentId,
initialContent = "",
onCancel,
onSuccess,
}: {
slug: string;
parentId?: string;
commentId?: string;
initialContent?: string;
onCancel?: () => void;
onSuccess?: () => void;
}) => {
const [content, setContent] = useState(initialContent);
const [isPending, startTransition] = useTransition();
const isEditing = !!commentId;
const isReplying = !!parentId;
const { data: session } = useSession();
const handleSubmit = (e: FormEvent) => {
e.preventDefault();
if (!content.trim()) {
toast.error("Comment cannot be empty.");
return;
}
startTransition(async () => {
try {
if (isEditing) {
await updateComment(commentId, content);
toast.success("Comment updated!");
} else {
await createComment({
content,
parentId,
pageSlug: slug,
});
toast.success("Comment posted!");
}
// Reset form if not editing
if (!isEditing) {
setContent("");
}
// Call success callback if provided
onSuccess?.();
} catch (error) {
console.error("Error submitting comment:", error);
toast.error("Failed to submit comment. Please try again.");
}
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4" data-intent={isEditing ? "edit" : "create"}>
<div className="flex gap-4">
{!isEditing && (
<div className="shrink-0">
<Avatar className="size-10">
{session?.user.image && (
<AvatarImage
{...getImageProps({
src: session.user.image,
alt: `@${session.user.name}'s avatar`,
width: 40,
height: 40,
}).props}
width={undefined}
height={undefined}
/>
)}
<AvatarFallback>{session?.user.name.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
</div>
)}
<div className="min-w-0 flex-1 space-y-4">
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder={isReplying ? "Reply to this comment..." : "Write your thoughts..."}
className="min-h-[4lh] w-full"
disabled={isPending}
/>
<div className="flex justify-between gap-4">
<div>
{/* Only show the markdown help text if the comment is new */}
{!isEditing && !isReplying && (
<p className="text-muted-foreground text-[0.8rem] leading-relaxed">
<MarkdownIcon className="mr-1.5 inline-block size-4 align-text-top" />
<span className="max-md:hidden">Basic&nbsp;</span>
<Popover>
<PopoverTrigger>
<span className="text-primary decoration-primary/40 cursor-pointer font-semibold no-underline decoration-2 underline-offset-4 hover:underline">
<span>Markdown</span>
<span className="max-md:hidden">&nbsp;syntax</span>
</span>
</PopoverTrigger>
<PopoverContent align="start">
<p className="text-sm leading-loose">
<InfoIcon className="mr-1.5 inline size-4.5 align-text-top" />
Examples:
</p>
<ul className="[&>li::marker]:text-muted-foreground my-2 list-inside list-disc pl-1 text-sm [&>li]:my-1.5 [&>li]:pl-1 [&>li]:text-nowrap [&>li::marker]:font-normal">
<li>
<span className="font-bold">**bold**</span>
</li>
<li>
<span className="italic">_italics_</span>
</li>
<li>
[
<Link href="https://jarv.is" className="hover:no-underline">
links
</Link>
](https://jarv.is)
</li>
<li>
<span className="bg-muted rounded-sm px-[0.3rem] py-[0.2rem] font-mono text-sm font-medium">
`code`
</span>
</li>
<li>
~~<span className="line-through">strikethrough</span>~~
</li>
</ul>
<p className="text-sm leading-loose">
<Link href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax">
Learn more.
</Link>
</p>
</PopoverContent>
</Popover>
<span>&nbsp;is supported</span>
<span className="max-md:hidden">&nbsp;here</span>
<span>.</span>
</p>
)}
</div>
<div className="flex justify-end gap-2">
{(onCancel || isEditing) && (
<Button type="button" variant="outline" onClick={onCancel} disabled={isPending}>
Cancel
</Button>
)}
<Button type="submit" disabled={isPending || !content.trim()}>
{isPending ? (
<>
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
{isEditing ? "Updating..." : "Posting..."}
</>
) : isEditing ? (
"Edit"
) : isReplying ? (
"Reply"
) : (
"Comment"
)}
</Button>
</div>
</div>
</div>
</div>
</form>
);
};
export default CommentForm;