mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-10-16 13:55:44 -04:00
perf: fetch session and comments in parallel, pass current user to comment components instead of useSession
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
6 files changed
+56
-36
No files matched your search
@@ -22,8 +22,7 @@ import {
|
|||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { toast } from "@/components/ui/toast";
|
import { toast } from "@/components/ui/toast";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import type { CommentUser, CommentWithUser } from "@/lib/data/comments";
|
||||||
import type { CommentWithUser } from "@/lib/data/comments";
|
|
||||||
import { deleteComment } from "@/lib/server/comments";
|
import { deleteComment } from "@/lib/server/comments";
|
||||||
|
|
||||||
import { EditCommentForm, ReplyForm } from "./comment-form";
|
import { EditCommentForm, ReplyForm } from "./comment-form";
|
||||||
@@ -35,12 +34,16 @@ type ActionMode =
|
|||||||
| { type: "confirming-delete" }
|
| { type: "confirming-delete" }
|
||||||
| { type: "deleting" };
|
| { type: "deleting" };
|
||||||
|
|
||||||
const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
const CommentActions = ({
|
||||||
|
comment,
|
||||||
|
currentUser,
|
||||||
|
}: {
|
||||||
|
comment: CommentWithUser;
|
||||||
|
currentUser: CommentUser | null;
|
||||||
|
}) => {
|
||||||
const [mode, setMode] = useState<ActionMode>({ type: "idle" });
|
const [mode, setMode] = useState<ActionMode>({ type: "idle" });
|
||||||
|
|
||||||
const { data: session } = useSession();
|
if (!currentUser) return null;
|
||||||
|
|
||||||
if (!session) return null;
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
setMode({ type: "deleting" });
|
setMode({ type: "deleting" });
|
||||||
@@ -87,7 +90,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
Reply
|
Reply
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{session.user.id === comment.user.id && (
|
{currentUser.id === comment.user.id && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
|
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
|
||||||
<IconDots />
|
<IconDots />
|
||||||
@@ -117,6 +120,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
<ReplyForm
|
<ReplyForm
|
||||||
slug={comment.pageSlug}
|
slug={comment.pageSlug}
|
||||||
parentId={comment.id}
|
parentId={comment.id}
|
||||||
|
currentUser={currentUser}
|
||||||
onCancel={() => setMode({ type: "idle" })}
|
onCancel={() => setMode({ type: "idle" })}
|
||||||
onSuccess={() => setMode({ type: "idle" })}
|
onSuccess={() => setMode({ type: "idle" })}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
|||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { toast } from "@/components/ui/toast";
|
import { toast } from "@/components/ui/toast";
|
||||||
import { useSession } from "@/lib/auth-client";
|
import type { CommentUser } from "@/lib/data/comments";
|
||||||
import { createComment, updateComment } from "@/lib/server/comments";
|
import { createComment, updateComment } from "@/lib/server/comments";
|
||||||
|
|
||||||
import { CommentAvatar } from "./comment-avatar";
|
import { CommentAvatar } from "./comment-avatar";
|
||||||
@@ -94,18 +94,12 @@ const CommentTextarea = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Current user's avatar (uses session)
|
// Current user's avatar
|
||||||
const CurrentUserAvatar = () => {
|
const CurrentUserAvatar = ({ user }: { user: CommentUser }) => (
|
||||||
const { data: session } = useSession();
|
<div className="shrink-0">
|
||||||
|
<CommentAvatar name={user.name} image={user.image} />
|
||||||
if (!session?.user) return null;
|
</div>
|
||||||
|
);
|
||||||
return (
|
|
||||||
<div className="shrink-0">
|
|
||||||
<CommentAvatar name={session.user.name} image={session.user.image} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit button with pending state
|
// Submit button with pending state
|
||||||
const SubmitButton = ({
|
const SubmitButton = ({
|
||||||
@@ -196,7 +190,7 @@ const MarkdownHelp = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
// New comment form - for creating top-level comments
|
// New comment form - for creating top-level comments
|
||||||
const NewCommentForm = ({ slug }: { slug: string }) => {
|
const NewCommentForm = ({ slug, currentUser }: { slug: string; currentUser: CommentUser }) => {
|
||||||
const { content, setContent, isPending, startTransition } = useCommentFormState();
|
const { content, setContent, isPending, startTransition } = useCommentFormState();
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
@@ -225,7 +219,7 @@ const NewCommentForm = ({ slug }: { slug: string }) => {
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<CurrentUserAvatar />
|
<CurrentUserAvatar user={currentUser} />
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 space-y-4">
|
<div className="min-w-0 flex-1 space-y-4">
|
||||||
<CommentTextarea
|
<CommentTextarea
|
||||||
@@ -257,11 +251,13 @@ const NewCommentForm = ({ slug }: { slug: string }) => {
|
|||||||
const ReplyForm = ({
|
const ReplyForm = ({
|
||||||
slug,
|
slug,
|
||||||
parentId,
|
parentId,
|
||||||
|
currentUser,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSuccess,
|
onSuccess,
|
||||||
}: {
|
}: {
|
||||||
slug: string;
|
slug: string;
|
||||||
parentId: string;
|
parentId: string;
|
||||||
|
currentUser: CommentUser;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSuccess?: () => void;
|
onSuccess?: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -294,7 +290,7 @@ const ReplyForm = ({
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<CurrentUserAvatar />
|
<CurrentUserAvatar user={currentUser} />
|
||||||
|
|
||||||
<div className="min-w-0 flex-1 space-y-4">
|
<div className="min-w-0 flex-1 space-y-4">
|
||||||
<CommentTextarea
|
<CommentTextarea
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
|||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
import { RelativeTime } from "@/components/relative-time";
|
import { RelativeTime } from "@/components/relative-time";
|
||||||
import type { CommentWithUser } from "@/lib/data/comments";
|
import type { CommentUser, CommentWithUser } from "@/lib/data/comments";
|
||||||
import { rehypeExternalLinks } from "@/lib/rehype";
|
import { rehypeExternalLinks } from "@/lib/rehype";
|
||||||
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
import { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -10,7 +10,13 @@ import { cn } from "@/lib/utils";
|
|||||||
import { CommentActions } from "./comment-actions";
|
import { CommentActions } from "./comment-actions";
|
||||||
import { CommentAvatar } from "./comment-avatar";
|
import { CommentAvatar } from "./comment-avatar";
|
||||||
|
|
||||||
const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
const CommentSingle = ({
|
||||||
|
comment,
|
||||||
|
currentUser,
|
||||||
|
}: {
|
||||||
|
comment: CommentWithUser;
|
||||||
|
currentUser: CommentUser | null;
|
||||||
|
}) => {
|
||||||
const divId = `comment-${comment.id.substring(0, 8)}`;
|
const divId = `comment-${comment.id.substring(0, 8)}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -62,7 +68,7 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommentActions comment={comment} />
|
<CommentActions comment={comment} currentUser={currentUser} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CommentWithUser } from "@/lib/data/comments";
|
import type { CommentUser, CommentWithUser } from "@/lib/data/comments";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
import { CommentSingle } from "./comment-single";
|
import { CommentSingle } from "./comment-single";
|
||||||
@@ -10,15 +10,17 @@ const CommentThread = ({
|
|||||||
comment,
|
comment,
|
||||||
replies,
|
replies,
|
||||||
allComments,
|
allComments,
|
||||||
|
currentUser,
|
||||||
level = 0,
|
level = 0,
|
||||||
}: {
|
}: {
|
||||||
comment: CommentWithUser;
|
comment: CommentWithUser;
|
||||||
replies: CommentWithUser[];
|
replies: CommentWithUser[];
|
||||||
allComments: Record<string, CommentWithUser[]>;
|
allComments: Record<string, CommentWithUser[]>;
|
||||||
|
currentUser: CommentUser | null;
|
||||||
level?: number;
|
level?: number;
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
<CommentSingle comment={comment} />
|
<CommentSingle comment={comment} currentUser={currentUser} />
|
||||||
|
|
||||||
{replies.length > 0 && (
|
{replies.length > 0 && (
|
||||||
<div className={cn("mt-6 space-y-6", level < MAX_NESTING_LEVEL && "ml-6 border-l-2 pl-6")}>
|
<div className={cn("mt-6 space-y-6", level < MAX_NESTING_LEVEL && "ml-6 border-l-2 pl-6")}>
|
||||||
@@ -28,6 +30,7 @@ const CommentThread = ({
|
|||||||
comment={reply}
|
comment={reply}
|
||||||
replies={allComments[reply.id] || []}
|
replies={allComments[reply.id] || []}
|
||||||
allComments={allComments}
|
allComments={allComments}
|
||||||
|
currentUser={currentUser}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { type CommentWithUser, getComments } from "@/lib/data/comments";
|
import { type CommentUser, type CommentWithUser, getComments } from "@/lib/data/comments";
|
||||||
|
|
||||||
import { NewCommentForm } from "./comment-form";
|
import { NewCommentForm } from "./comment-form";
|
||||||
import { CommentThread } from "./comment-thread";
|
import { CommentThread } from "./comment-thread";
|
||||||
import { SignIn } from "./sign-in";
|
import { SignIn } from "./sign-in";
|
||||||
|
|
||||||
const Comments = async ({ slug }: { slug: string }) => {
|
const Comments = async ({ slug }: { slug: string }) => {
|
||||||
const session = await auth.api.getSession({
|
// independent queries -- run them in parallel
|
||||||
headers: await headers(),
|
const [session, comments] = await Promise.all([
|
||||||
});
|
auth.api.getSession({
|
||||||
|
headers: await headers(),
|
||||||
|
}),
|
||||||
|
getComments(slug),
|
||||||
|
]);
|
||||||
|
|
||||||
const comments = await getComments(slug);
|
// only pass the public fields down to client components
|
||||||
|
const currentUser: CommentUser | null = session
|
||||||
|
? { id: session.user.id, name: session.user.name, image: session.user.image ?? null }
|
||||||
|
: null;
|
||||||
|
|
||||||
const commentsByParentId = comments.reduce(
|
const commentsByParentId = comments.reduce(
|
||||||
(acc, comment) => {
|
(acc, comment) => {
|
||||||
@@ -30,8 +37,8 @@ const Comments = async ({ slug }: { slug: string }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{session ? (
|
{currentUser ? (
|
||||||
<NewCommentForm slug={slug} />
|
<NewCommentForm slug={slug} currentUser={currentUser} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center gap-y-4 rounded-lg bg-muted/40 p-6">
|
<div className="flex flex-col items-center justify-center gap-y-4 rounded-lg bg-muted/40 p-6">
|
||||||
<p className="text-center font-medium">Join the discussion by signing in:</p>
|
<p className="text-center font-medium">Join the discussion by signing in:</p>
|
||||||
@@ -47,6 +54,7 @@ const Comments = async ({ slug }: { slug: string }) => {
|
|||||||
comment={comment}
|
comment={comment}
|
||||||
replies={commentsByParentId[comment.id] || []}
|
replies={commentsByParentId[comment.id] || []}
|
||||||
allComments={commentsByParentId}
|
allComments={commentsByParentId}
|
||||||
|
currentUser={currentUser}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { desc, eq, sql } from "drizzle-orm";
|
|||||||
import { db } from "@/lib/db";
|
import { db } from "@/lib/db";
|
||||||
import * as schema from "@/lib/db/schema";
|
import * as schema from "@/lib/db/schema";
|
||||||
|
|
||||||
|
/** The public profile fields of a commenter (also used for the signed-in user in comment UI) */
|
||||||
|
export type CommentUser = Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
|
||||||
|
|
||||||
export type CommentWithUser = typeof schema.comment.$inferSelect & {
|
export type CommentWithUser = typeof schema.comment.$inferSelect & {
|
||||||
user: Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
|
user: CommentUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user