mirror of
https://github.com/jakejarvis/jarv.is.git
synced 2026-10-15 11:15:45 -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";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { toast } from "@/components/ui/toast";
|
||||
import { useSession } from "@/lib/auth-client";
|
||||
import type { CommentWithUser } from "@/lib/data/comments";
|
||||
import type { CommentUser, CommentWithUser } from "@/lib/data/comments";
|
||||
import { deleteComment } from "@/lib/server/comments";
|
||||
|
||||
import { EditCommentForm, ReplyForm } from "./comment-form";
|
||||
@@ -35,12 +34,16 @@ type ActionMode =
|
||||
| { type: "confirming-delete" }
|
||||
| { type: "deleting" };
|
||||
|
||||
const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
const CommentActions = ({
|
||||
comment,
|
||||
currentUser,
|
||||
}: {
|
||||
comment: CommentWithUser;
|
||||
currentUser: CommentUser | null;
|
||||
}) => {
|
||||
const [mode, setMode] = useState<ActionMode>({ type: "idle" });
|
||||
|
||||
const { data: session } = useSession();
|
||||
|
||||
if (!session) return null;
|
||||
if (!currentUser) return null;
|
||||
|
||||
const handleDelete = async () => {
|
||||
setMode({ type: "deleting" });
|
||||
@@ -87,7 +90,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
Reply
|
||||
</Button>
|
||||
|
||||
{session.user.id === comment.user.id && (
|
||||
{currentUser.id === comment.user.id && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
|
||||
<IconDots />
|
||||
@@ -117,6 +120,7 @@ const CommentActions = ({ comment }: { comment: CommentWithUser }) => {
|
||||
<ReplyForm
|
||||
slug={comment.pageSlug}
|
||||
parentId={comment.id}
|
||||
currentUser={currentUser}
|
||||
onCancel={() => 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 { Textarea } from "@/components/ui/textarea";
|
||||
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 { CommentAvatar } from "./comment-avatar";
|
||||
@@ -94,18 +94,12 @@ const CommentTextarea = ({
|
||||
/>
|
||||
);
|
||||
|
||||
// Current user's avatar (uses session)
|
||||
const CurrentUserAvatar = () => {
|
||||
const { data: session } = useSession();
|
||||
|
||||
if (!session?.user) return null;
|
||||
|
||||
return (
|
||||
<div className="shrink-0">
|
||||
<CommentAvatar name={session.user.name} image={session.user.image} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
// Current user's avatar
|
||||
const CurrentUserAvatar = ({ user }: { user: CommentUser }) => (
|
||||
<div className="shrink-0">
|
||||
<CommentAvatar name={user.name} image={user.image} />
|
||||
</div>
|
||||
);
|
||||
|
||||
// Submit button with pending state
|
||||
const SubmitButton = ({
|
||||
@@ -196,7 +190,7 @@ const MarkdownHelp = () => (
|
||||
);
|
||||
|
||||
// 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 handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@@ -225,7 +219,7 @@ const NewCommentForm = ({ slug }: { slug: string }) => {
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
||||
<div className="flex gap-4">
|
||||
<CurrentUserAvatar />
|
||||
<CurrentUserAvatar user={currentUser} />
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-4">
|
||||
<CommentTextarea
|
||||
@@ -257,11 +251,13 @@ const NewCommentForm = ({ slug }: { slug: string }) => {
|
||||
const ReplyForm = ({
|
||||
slug,
|
||||
parentId,
|
||||
currentUser,
|
||||
onCancel,
|
||||
onSuccess,
|
||||
}: {
|
||||
slug: string;
|
||||
parentId: string;
|
||||
currentUser: CommentUser;
|
||||
onCancel: () => void;
|
||||
onSuccess?: () => void;
|
||||
}) => {
|
||||
@@ -294,7 +290,7 @@ const ReplyForm = ({
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4" data-intent="create">
|
||||
<div className="flex gap-4">
|
||||
<CurrentUserAvatar />
|
||||
<CurrentUserAvatar user={currentUser} />
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-4">
|
||||
<CommentTextarea
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
import Markdown from "react-markdown";
|
||||
|
||||
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 { remarkGfm, remarkSmartypants } from "@/lib/remark";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -10,7 +10,13 @@ import { cn } from "@/lib/utils";
|
||||
import { CommentActions } from "./comment-actions";
|
||||
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)}`;
|
||||
|
||||
return (
|
||||
@@ -62,7 +68,7 @@ const CommentSingle = ({ comment }: { comment: CommentWithUser }) => {
|
||||
</Markdown>
|
||||
</div>
|
||||
|
||||
<CommentActions comment={comment} />
|
||||
<CommentActions comment={comment} currentUser={currentUser} />
|
||||
</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 { CommentSingle } from "./comment-single";
|
||||
@@ -10,15 +10,17 @@ const CommentThread = ({
|
||||
comment,
|
||||
replies,
|
||||
allComments,
|
||||
currentUser,
|
||||
level = 0,
|
||||
}: {
|
||||
comment: CommentWithUser;
|
||||
replies: CommentWithUser[];
|
||||
allComments: Record<string, CommentWithUser[]>;
|
||||
currentUser: CommentUser | null;
|
||||
level?: number;
|
||||
}) => (
|
||||
<>
|
||||
<CommentSingle comment={comment} />
|
||||
<CommentSingle comment={comment} currentUser={currentUser} />
|
||||
|
||||
{replies.length > 0 && (
|
||||
<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}
|
||||
replies={allComments[reply.id] || []}
|
||||
allComments={allComments}
|
||||
currentUser={currentUser}
|
||||
level={level + 1}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { headers } from "next/headers";
|
||||
|
||||
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 { CommentThread } from "./comment-thread";
|
||||
import { SignIn } from "./sign-in";
|
||||
|
||||
const Comments = async ({ slug }: { slug: string }) => {
|
||||
const session = await auth.api.getSession({
|
||||
headers: await headers(),
|
||||
});
|
||||
// independent queries -- run them in parallel
|
||||
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(
|
||||
(acc, comment) => {
|
||||
@@ -30,8 +37,8 @@ const Comments = async ({ slug }: { slug: string }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{session ? (
|
||||
<NewCommentForm slug={slug} />
|
||||
{currentUser ? (
|
||||
<NewCommentForm slug={slug} currentUser={currentUser} />
|
||||
) : (
|
||||
<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>
|
||||
@@ -47,6 +54,7 @@ const Comments = async ({ slug }: { slug: string }) => {
|
||||
comment={comment}
|
||||
replies={commentsByParentId[comment.id] || []}
|
||||
allComments={commentsByParentId}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,11 @@ import { desc, eq, sql } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
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 & {
|
||||
user: Pick<typeof schema.user.$inferSelect, "id" | "name" | "image">;
|
||||
user: CommentUser;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user