Skip to content

Commit

Permalink
feat: implement MVP of repository comments feature
Browse files Browse the repository at this point in the history
  • Loading branch information
SwiichyCode committed Feb 25, 2024
1 parent 269b55a commit 8b2cc6c
Show file tree
Hide file tree
Showing 16 changed files with 207 additions and 25 deletions.
31 changes: 22 additions & 9 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ model User {
posts Post[]
repositories Repository[]
likes Like[]
Comment Comment[]
}

model VerificationToken {
Expand All @@ -80,7 +81,7 @@ model VerificationToken {
}

model Repository {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
url String
description String?
repositoryId Int
Expand All @@ -89,25 +90,37 @@ model Repository {
repositoryStargazers Int
repositoryCreatedAt DateTime
repositoryUpdatedAt DateTime
repositoryLicenseName String @default("No license")
repositoryLicenseUrl String @default("No license url")
is_template Boolean @default(false)
is_visible Boolean @default(true)
repositoryLicenseName String @default("No license")
repositoryLicenseUrl String @default("No license url")
is_template Boolean @default(false)
is_visible Boolean @default(true)
ownerId Int
ownerUsername String
ownerAvatarUrl String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdById String
languageId Int
language Language @relation(fields: [languageId], references: [id])
language Language @relation(fields: [languageId], references: [id])
topics Topic[]
likes Like[]
comments Comment[]
@@index([url])
}

model Comment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdBy User @relation(fields: [createdById], references: [id])
createdById String
repository Repository @relation(fields: [repositoryId], references: [id])
repositoryId Int
}

model Like {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
Expand Down
24 changes: 24 additions & 0 deletions src/actions/addcomment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use server";
import { revalidatePath } from "next/cache";
import { userAction } from "@/lib/next-safe-action";
import repositoryService from "@/services/repository.service";
import * as z from "zod";

const schema = z.object({
repositoryId: z.number(),
content: z.string().min(1, "Comment must be at least 1 character long"),
});

export const addComment = userAction(schema, async (data, ctx) => {
try {
await repositoryService.addCommentToRepository(
data.repositoryId,
data.content,
ctx.session.user.id,
);
} catch (error) {
if (error instanceof Error) return { error: error.message };
}

revalidatePath(`/repositories/${data.repositoryId}`);
});
25 changes: 25 additions & 0 deletions src/app/repositories/[repositoryId]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { AddCommentForm } from "@/components/organisms/_forms/addcomment.form";
import repositoryService from "@/services/repository.service";
import React from "react";

export default async function RepositoryCommentPage({
params,
}: {
params: { repositoryId: number };
}) {
const comments = await repositoryService.getCommentsByRepositoryId(
Number(params.repositoryId),
);

return (
<>
<h1>Comments</h1>
{comments.map((comment) => (
<div key={comment.id}>
<p>{comment.content}</p>
</div>
))}
<AddCommentForm repositoryId={Number(params.repositoryId)} />
</>
);
}
1 change: 0 additions & 1 deletion src/app/repositories/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ type Props = {
const queryParser = parseAsString.withDefault("");

export default async function RepositoriesPage({ searchParams }: Props) {
// const query = searchParams?.query ?? "";
const query = queryParser.parseServerSide(searchParams?.query);
const limit = 20;

Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/InputForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
control: any;
name: string;
label: string;
label?: string;
description?: string;
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/molecules/TextAreaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface TextAreaFormProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
control: any;
name: string;
label: string;
label?: string;
}

const TextAreaForm = React.forwardRef<HTMLTextAreaElement, TextAreaFormProps>(
Expand Down
File renamed without changes.
19 changes: 19 additions & 0 deletions src/components/organisms/RepositoryCard/RepositoryCardComment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Link from "next/link";
import { CommentDiscussionIcon } from "@primer/octicons-react";
import type { Repository } from "@/types/prisma.type";

type Props = {
repository: Repository;
};

export const RepositoryCardComment = ({ repository }: Props) => {
return (
<Link
href={`repositories/${repository.id}`}
className="flex items-center space-x-1 hover:text-[#2F81F7]"
>
<CommentDiscussionIcon className="h-4 w-4" />
<span>0</span>
</Link>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { RepositoryCardLike } from "./RepositoryCardLike";
import { RepositoryCardLicense } from "./RepositoryCardLicense";
import { RepositoryCardStars } from "./RepositoryCardStars";
import { RepositoryCardLanguage } from "./RepositoryCardLanguage";
import { RepositoryCardComment } from "./RepositoryCardComment";
import type { Repository } from "@/types/prisma.type";
import type { User } from "@/types/prisma.type";
import type { Like } from "@prisma/client";
Expand Down Expand Up @@ -29,7 +30,10 @@ export const RepositoryCardFooter = ({
/>
<RepositoryCardLicense repository={repository} />
</div>
<RepositoryCardLike user={user} repository={repository} likes={likes} />
<div className="flex space-x-4">
<RepositoryCardComment repository={repository} />
<RepositoryCardLike user={user} repository={repository} likes={likes} />
</div>
</div>
);
};
15 changes: 11 additions & 4 deletions src/components/organisms/RepositoryCard/RepositoryCardHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ import { displayNameOrUsername } from "@/lib/utils";
type Props = {
user: User | null;
repository: Repository;
isComment?: boolean;
};

export const RepositoryCardHeader = ({ user, repository }: Props) => {
export const RepositoryCardHeader = ({
user,
repository,
isComment = false,
}: Props) => {
return (
<div className="flex justify-between">
<div className="flex items-center gap-2">
Expand All @@ -32,9 +37,11 @@ export const RepositoryCardHeader = ({ user, repository }: Props) => {
</span>
</div>
</div>
<AdminWrapper role={user?.role}>
<RepositoryCardHide repository={repository} />
</AdminWrapper>
{!isComment && (
<AdminWrapper role={user?.role}>
<RepositoryCardHide repository={repository} />
</AdminWrapper>
)}
</div>
);
};
10 changes: 3 additions & 7 deletions src/components/organisms/RepositoryCard/RepositoryCardLike.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,18 @@ export const RepositoryCardLike = ({ user, repository, likes }: Props) => {
return (
<form
onSubmit={handleLikeRepository}
className="flex items-center space-x-1"
className="flex items-center space-x-1 hover:text-[#FF3E6C]"
>
<button type="submit">
{optimisticLikes.some(
(like) =>
like.userId === user?.id && like.repositoryId === repository.id,
) ? (
<HeartFillIcon
className={cn(
"h-4 w-4 text-[#FF3E6C] hover:cursor-pointer hover:text-[#FF3E6C]",
)}
className={cn("h-4 w-4 text-[#FF3E6C] hover:cursor-pointer ")}
/>
) : (
<HeartIcon
className={cn("h-4 w-4 hover:cursor-pointer hover:text-[#FF3E6C]")}
/>
<HeartIcon className={cn("h-4 w-4 hover:cursor-pointer ")} />
)}
</button>
<span>{formatNumber(handleLikeCount(optimisticLikes, repository))}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { TrashIcon } from "@primer/octicons-react";
import { DialogWrapper } from "./DialogWrapper";
import { DialogWrapper } from "@/components/organisms/DialogWrapper";
import { RemoveStaredRepositoryForm } from "../_forms/removeStaredRepository.form";

export const SidebarRemoveStarredRepositories = () => {
Expand Down
50 changes: 50 additions & 0 deletions src/components/organisms/_forms/addcomment.form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import { useTransition } from "react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Form } from "@/components/atoms/form";
import { SubmitButton } from "@/components/molecules/SubmitButton";
import { TextAreaForm } from "@/components/molecules/TextAreaForm";
import { addComment } from "@/actions/addcomment";
import { formAddCommentSchema } from "./addcomment.schema";
import type { z } from "zod";

type Props = {
repositoryId: number;
};

export const AddCommentForm = ({ repositoryId }: Props) => {
const [isPending, startTransition] = useTransition();

const form = useForm<z.infer<typeof formAddCommentSchema>>({
resolver: zodResolver(formAddCommentSchema),
defaultValues: {
content: "",
},
});

function onSubmit(data: z.infer<typeof formAddCommentSchema>) {
startTransition(async () => {
const payload = {
repositoryId,
content: data.content,
};

await addComment(payload);
});
}

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<TextAreaForm
control={form.control}
name="content"
placeholder="Leave a comment..."
/>
<SubmitButton isPending={isPending}>Comment</SubmitButton>
</form>
</Form>
);
};
5 changes: 5 additions & 0 deletions src/components/organisms/_forms/addcomment.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import * as z from "zod";

export const formAddCommentSchema = z.object({
content: z.string().min(1, "Comment must be at least 1 character long"),
});
36 changes: 36 additions & 0 deletions src/services/repository.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,42 @@ class RepositoryService {
});
}
}

async addCommentToRepository(
repositoryId: number,
content: string,
createdBy: string,
) {
return await db.comment.create({
data: {
content,
createdBy: {
connect: {
id: createdBy,
},
},
repository: {
connect: {
id: repositoryId,
},
},
},
});
}

async getCommentsByRepositoryId(repositoryId: number) {
return await db.comment.findMany({
where: {
repositoryId,
},
include: {
createdBy: true,
},
orderBy: {
id: "desc",
},
});
}
}

const repositoryService = new RepositoryService();
Expand Down
4 changes: 4 additions & 0 deletions src/types/prisma.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export type Repository = Prisma.RepositoryGetPayload<{
export type User = Prisma.UserGetPayload<{
include: { likes: true };
}>;

export type Comment = Prisma.CommentGetPayload<{
include: { createdBy: true };
}>;

0 comments on commit 8b2cc6c

Please sign in to comment.