You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
the code below is built in nextjs 15 using TypeScript
prisma
My focus in this code is to master Nested Comments, reply to a reply. I myself have tried main ways but I'm falling to add nested comments, so please how can I update this code and to make sure that I can comment on a post and also to leave a reply to a comment. any help, I will be thankful
This is my Prisma schema file
model Media {
id String @id@default(cuid())
postId String?
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
type MediaType
url String
model Like {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
model Bookmark {
id String @id@default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
import { useSession } from "@/app/(main)/SessionProvider";
import { CommentData } from "@/lib/types";
import { formatRelativeDate } from "@/lib/utils";
import Link from "next/link";
import UserAvatar from "../UserAvatar";
import UserTooltip from "../UserTooltip";
import CommentMoreButton from "./CommentMoreButton";
interface CommentProps {
comment: CommentData;
}
export default function Comment({ comment }: CommentProps) {
const { user } = useSession();
the code below is built in nextjs 15 using TypeScript
prisma
My focus in this code is to master Nested Comments, reply to a reply. I myself have tried main ways but I'm falling to add nested comments, so please how can I update this code and to make sure that I can comment on a post and also to leave a reply to a comment. any help, I will be thankful
This is my Prisma schema file
generator client {
provider = "prisma-client-js"
previewFeatures = ["fullTextSearch"]
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}
enum VoteType {
UP
DOWN
}
model User {
id String @id
username String @unique
displayName String
email String? @unique
passwordHash String?
googleId String? @unique
avatarUrl String?
bio String?
sessions Session[]
posts Post[]
following Follow[] @relation("Following")
followers Follow[] @relation("Followers")
likes Like[]
bookmarks Bookmark[]
comments Comment[]
votes CommentVote[]
reply Reply[]
receivedNotifications Notification[] @relation("Recipient")
issuedNotifications Notification[] @relation("Issuer")
createdAt DateTime @default(now())
@@Map("users")
}
model Session {
id String @id @default(cuid())
sessionToken String? @unique
userId String
expiresAt DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@Map("sessions")
}
model Follow {
followerId String
follower User @relation("Following", fields: [followerId], references: [id], onDelete: Cascade)
followingId String
following User @relation("Followers", fields: [followingId], references: [id], onDelete: Cascade)
@@unique([followerId, followingId])
@@Map("follows")
}
model Post {
id String @id @default(cuid())
content String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
attachments Media[]
likes Like[]
bookmarks Bookmark[]
comments Comment[]
linkedNotifications Notification[]
createdAt DateTime @default(now())
@@Map("posts")
}
model Media {
id String @id @default(cuid())
postId String?
post Post? @relation(fields: [postId], references: [id], onDelete: SetNull)
type MediaType
url String
createdAt DateTime @default(now())
@@Map("post_media")
}
enum MediaType {
IMAGE
VIDEO
}
model Comment {
id String @id @default(cuid())
content String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
parentId String?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id])
replies Comment[] @relation("CommentReplies")
@@Map("comments")
}
model CommentVote {
user User @relation(fields: [userId], references: [id])
userId String
commentId String
type VoteType
@@id([userId, commentId])
}
model Reply {
id String @id @default(cuid())
content String
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
commentId String?
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@Map("reply")
}
model Like {
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
@@unique([userId, postId])
@@Map("likes")
}
model Bookmark {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
postId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, postId])
@@Map("bookmarks")
}
model Notification {
id String @id @default(cuid())
recipientId String
recipient User @relation("Recipient", fields: [recipientId], references: [id], onDelete: Cascade)
issuerId String
issuer User @relation("Issuer", fields: [issuerId], references: [id], onDelete: Cascade)
postId String?
post Post? @relation(fields: [postId], references: [id], onDelete: Cascade)
type NotificationType
read Boolean @default(false)
createdAt DateTime @default(now())
@@Map("notifications")
}
enum NotificationType {
LIKE
FOLLOW
COMMENT
}
src/lib/types.ts
import { Prisma } from "@prisma/client";
export function getUserDataSelect(loggedInUserId: string) {
return {
id: true,
username: true,
displayName: true,
avatarUrl: true,
bio: true,
createdAt: true,
followers: {
where: {
followerId: loggedInUserId,
},
select: {
followerId: true,
},
},
_count: {
select: {
posts: true,
followers: true,
},
},
} satisfies Prisma.UserSelect;
}
export type UserData = Prisma.UserGetPayload<{
select: ReturnType;
}>;
export function getPostDataInclude(loggedInUserId: string) {
return {
user: {
select: getUserDataSelect(loggedInUserId),
},
attachments: true,
likes: {
where: {
userId: loggedInUserId,
},
select: {
userId: true,
},
},
bookmarks: {
where: {
userId: loggedInUserId,
},
select: {
userId: true,
},
},
_count: {
select: {
likes: true,
comments: true,
},
},
} satisfies Prisma.PostInclude;
}
export type PostData = Prisma.PostGetPayload<{
include: ReturnType;
}>;
export interface PostsPage {
posts: PostData[];
nextCursor: string | null;
}
export function getCommentDataInclude(loggedInUserId: string) {
return {
user: {
select: getUserDataSelect(loggedInUserId),
},
} satisfies Prisma.CommentInclude;
}
export type CommentData = Prisma.CommentGetPayload<{
include: ReturnType;
}>;
export interface CommentsPage {
comments: CommentData[];
previousCursor: string | null;
}
export const notificationsInclude = {
issuer: {
select: {
username: true,
displayName: true,
avatarUrl: true,
},
},
post: {
select: {
content: true,
},
},
} satisfies Prisma.NotificationInclude;
export type NotificationData = Prisma.NotificationGetPayload<{
include: typeof notificationsInclude;
}>;
export interface NotificationsPage {
notifications: NotificationData[];
nextCursor: string | null;
}
export interface FollowerInfo {
followers: number;
isFollowedByUser: boolean;
}
export interface LikeInfo {
likes: number;
isLikedByUser: boolean;
}
export interface BookmarkInfo {
isBookmarkedByUser: boolean;
}
export interface NotificationCountInfo {
unreadCount: number;
}
export interface MessageCountInfo {
unreadCount: number;
}
src/app/api/posts/[postId]/comments/route.ts
import { validateRequest } from "@/auth";
import prisma from "@/lib/prisma";
import { CommentsPage, getCommentDataInclude } from "@/lib/types";
import { NextRequest } from "next/server";
export async function GET(
req: NextRequest,
{ params: { postId } }: { params: { postId: string } },
) {
try {
const cursor = req.nextUrl.searchParams.get("cursor") || undefined;
} catch (error) {
console.error(error);
return Response.json({ error: "Internal server error" }, { status: 500 });
}
}
src/lib/validation.ts
import { z } from "zod";
const requiredString = z.string().trim().min(1, "Required");
export const signUpSchema = z.object({
email: requiredString.email("Invalid email address"),
username: requiredString.regex(
/^[a-zA-Z0-9_-]+$/,
"Only letters, numbers, - and _ allowed",
),
password: requiredString.min(8, "Must be at least 8 characters"),
});
export type SignUpValues = z.infer;
export const loginSchema = z.object({
username: requiredString,
password: requiredString,
});
export type LoginValues = z.infer;
export const createPostSchema = z.object({
content: requiredString,
mediaIds: z.array(z.string()).max(5, "Cannot have more than 5 attachments"),
});
export const updateUserProfileSchema = z.object({
displayName: requiredString,
bio: z.string().max(1000, "Must be at most 1000 characters"),
});
export type UpdateUserProfileValues = z.infer;
export const createCommentSchema = z.object({
content: requiredString,
});
src/lib/validation.ts
export const CommentVoteValidator = z.object({
commentId: z.string(),
voteType: z.enum(['UP', 'DOWN']),
})
export type CommentVoteRequest = z.infer
src/components/comments/actions.ts
"use server";
import { validateRequest } from "@/auth";
import prisma from "@/lib/prisma";
import { getCommentDataInclude, PostData } from "@/lib/types";
import { createCommentSchema } from "@/lib/validation";
export async function submitComment({
post,
content,
}: {
post: PostData;
content: string;
}) {
const { user } = await validateRequest();
if (!user) throw new Error("Unauthorized");
const { content: contentValidated } = createCommentSchema.parse({ content });
const [newComment] = await prisma.$transaction([
prisma.comment.create({
data: {
content: contentValidated,
postId: post.id,
userId: user.id,
},
include: getCommentDataInclude(user.id),
}),
...(post.user.id !== user.id
? [
prisma.notification.create({
data: {
issuerId: user.id,
recipientId: post.user.id,
postId: post.id,
type: "COMMENT",
},
}),
]
: []),
]);
return newComment;
}
export async function deleteComment(id: string) {
const { user } = await validateRequest();
if (!user) throw new Error("Unauthorized");
const comment = await prisma.comment.findUnique({
where: { id },
});
if (!comment) throw new Error("Comment not found");
if (comment.userId !== user.id) throw new Error("Unauthorized");
const deletedComment = await prisma.comment.delete({
where: { id },
include: getCommentDataInclude(user.id),
});
return deletedComment;
}
src/components/comments/mutations.ts
import { CommentsPage } from "@/lib/types";
import {
InfiniteData,
QueryKey,
useMutation,
useQueryClient,
} from "@tanstack/react-query";
import { useToast } from "../ui/use-toast";
import { deleteComment, submitComment } from "./actions";
export function useSubmitCommentMutation(postId: string) {
const { toast } = useToast();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: submitComment,
onSuccess: async (newComment) => {
const queryKey: QueryKey = ["comments", postId];
});
return mutation;
}
export function useDeleteCommentMutation() {
const { toast } = useToast();
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: deleteComment,
onSuccess: async (deletedComment) => {
const queryKey: QueryKey = ["comments", deletedComment.postId];
});
return mutation;
}
src/components/comments/DeleteCommentDialog.tsx
import { CommentData } from "@/lib/types";
import LoadingButton from "../LoadingButton";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "../ui/dialog";
import { useDeleteCommentMutation } from "./mutations";
interface DeleteCommentDialogProps {
comment: CommentData;
open: boolean;
onClose: () => void;
}
export default function DeleteCommentDialog({
comment,
open,
onClose,
}: DeleteCommentDialogProps) {
const mutation = useDeleteCommentMutation();
function handleOpenChange(open: boolean) {
if (!open || !mutation.isPending) {
onClose();
}
}
return (
Delete comment?
Are you sure you want to delete this comment? This action cannot be
undone.
<LoadingButton
variant="destructive"
onClick={() => mutation.mutate(comment.id, { onSuccess: onClose })}
loading={mutation.isPending}
>
Delete
Cancel
);
}
src/components/comments/Comment.tsx
import { useSession } from "@/app/(main)/SessionProvider";
import { CommentData } from "@/lib/types";
import { formatRelativeDate } from "@/lib/utils";
import Link from "next/link";
import UserAvatar from "../UserAvatar";
import UserTooltip from "../UserTooltip";
import CommentMoreButton from "./CommentMoreButton";
interface CommentProps {
comment: CommentData;
}
export default function Comment({ comment }: CommentProps) {
const { user } = useSession();
return (
<Link href={
/users/${comment.user.username}
}><Link
href={
/users/${comment.user.username}
}className="font-medium hover:underline"
>
{comment.user.displayName}
{formatRelativeDate(comment.createdAt)}
{comment.user.id === user.id && (
)}
);
}
src/components/comments/CommentInput.tsx
import { PostData } from "@/lib/types";
import { Loader2, SendHorizonal } from "lucide-react";
import { useState } from "react";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { useSubmitCommentMutation } from "./mutations";
interface CommentInputProps {
post: PostData;
}
export default function CommentInput({ post }: CommentInputProps) {
const [input, setInput] = useState("");
const mutation = useSubmitCommentMutation(post.id);
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
}
return (
<Input
placeholder="Write a comment..."
value={input}
onChange={(e) => setInput(e.target.value)}
autoFocus
/>
<Button
type="submit"
variant="ghost"
size="icon"
disabled={!input.trim() || mutation.isPending}
>
{!mutation.isPending ? (
) : (
)}
);
}
src/components/comments/CommentMoreButton.tsx
import { CommentData } from "@/lib/types";
import { MoreHorizontal, Trash2 } from "lucide-react";
import { useState } from "react";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import DeleteCommentDialog from "./DeleteCommentDialog";
interface CommentMoreButtonProps {
comment: CommentData;
className?: string;
}
export default function CommentMoreButton({
comment,
className,
}: CommentMoreButtonProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
return (
<>
<DropdownMenuItem onClick={() => setShowDeleteDialog(true)}>
Delete
<DeleteCommentDialog
comment={comment}
open={showDeleteDialog}
onClose={() => setShowDeleteDialog(false)}
/>
</>
);
}
The text was updated successfully, but these errors were encountered: