Skip to content

Commit 3ed45d7

Browse files
authored
Merge pull request #39 from techdiary-dev/comment
Comment
2 parents 1b2d405 + 1a83a38 commit 3ed45d7

File tree

16 files changed

+573
-71
lines changed

16 files changed

+573
-71
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"lint": "next lint",
1010
"db:generate": "npx drizzle-kit generate",
1111
"db:push": "npx drizzle-kit push",
12-
"db:studio": "npx drizzle-kit studio"
12+
"db:studio": "npx drizzle-kit studio",
13+
"play": "npx tsx ./src/backend/play.ts"
1314
},
1415
"dependencies": {
1516
"@cloudinary/react": "^1.14.1",

src/app/[username]/[articleHandle]/_components/ArticleReaction.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client";
22

3-
import ReactionStatus from "@/components/render-props/ReactionStatus";
3+
import { ResourceReactionable } from "@/components/render-props/ResourceReactionable";
44
import clsx from "clsx";
55
import React from "react";
66

@@ -10,7 +10,7 @@ interface Props {
1010

1111
const ArticleReaction: React.FC<Props> = ({ article_id }) => {
1212
return (
13-
<ReactionStatus
13+
<ResourceReactionable
1414
resource_type="ARTICLE"
1515
resource_id={article_id}
1616
render={({ reactions, toggle }) => {

src/app/[username]/[articleHandle]/page.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import HomeLeftSidebar from "@/app/(home)/_components/HomeLeftSidebar";
22
import { persistenceRepository } from "@/backend/persistence/persistence-repositories";
33
import * as articleActions from "@/backend/services/article.actions";
44
import AppImage from "@/components/AppImage";
5+
import { CommentSection } from "@/components/comment-section";
56
import HomepageLayout from "@/components/layout/HomepageLayout";
67
import { readingTime, removeMarkdownSyntax } from "@/lib/utils";
78
import getFileUrl from "@/utils/getFileUrl";
@@ -12,10 +13,9 @@ import Link from "next/link";
1213
import { notFound } from "next/navigation";
1314
import type { Article, WithContext } from "schema-dts";
1415
import { eq } from "sqlkit";
15-
import ArticleSidebar from "./_components/ArticleSidebar";
16-
import ReactionStatus from "@/components/render-props/ReactionStatus";
17-
import clsx from "clsx";
1816
import ArticleReaction from "./_components/ArticleReaction";
17+
import ArticleSidebar from "./_components/ArticleSidebar";
18+
import ResourceReaction from "@/components/ResourceReaction";
1919

2020
interface ArticlePageProps {
2121
params: Promise<{
@@ -153,10 +153,12 @@ const Page: NextPage<ArticlePageProps> = async ({ params }) => {
153153
<h1 className="text-2xl font-bold">{article?.title ?? ""}</h1>
154154
</div>
155155

156-
<ArticleReaction article_id={article.id} />
156+
<ResourceReaction resource_type="ARTICLE" resource_id={article.id} />
157157

158158
<div className="mx-auto content-typography">{parsedHTML}</div>
159159
</div>
160+
161+
<CommentSection resource_type="ARTICLE" resource_id={article.id} />
160162
</HomepageLayout>
161163
</>
162164
);

src/app/api/play/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1+
import { getComments } from "@/backend/services/comment.action";
12
import { NextResponse } from "next/server";
2-
// import * as reactionActions from "@/backend/services/reaction.actions";
3-
import * as searchService from "@/backend/services/search.service";
43

54
export async function GET() {
6-
const response = await searchService.syncAllArticles();
5+
const comments = await getComments({
6+
resource_id: "16196e73-275a-4af5-9186-39a5fec4244e",
7+
resource_type: "ARTICLE",
8+
});
79

8-
return NextResponse.json(response, {
10+
return NextResponse.json(comments, {
911
status: 200,
1012
headers: { "Content-Type": "application/json" },
1113
});

src/backend/models/domain-models.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,20 @@ export interface Comment {
139139
created_at: Date;
140140
}
141141

142+
export interface CommentPresentation {
143+
id: string;
144+
body?: string;
145+
level?: number;
146+
created_at?: Date;
147+
author?: {
148+
id: string;
149+
name: string;
150+
username: string;
151+
email: string;
152+
};
153+
replies?: CommentPresentation[];
154+
}
155+
142156
export type REACTION_TYPE =
143157
| "LOVE"
144158
| "UNICORN"

src/backend/persistence/persistence-contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum DatabaseTableName {
55
user_sessions = "user_sessions",
66
series = "series",
77
series_items = "series_items",
8+
comments = "comments",
89
bookmarks = "bookmarks",
910
reactions = "reactions",
1011
tags = "tags",

src/backend/persistence/persistence-repositories.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Tag,
1010
User,
1111
UserSession,
12+
Comment,
1213
UserSocial,
1314
} from "../models/domain-models";
1415
import { pgClient } from "./clients";
@@ -73,12 +74,19 @@ const reactionRepository = new Repository<Reaction>(
7374
repositoryConfig
7475
);
7576

77+
const commentRepository = new Repository<Comment>(
78+
DatabaseTableName.comments,
79+
pgClient,
80+
repositoryConfig
81+
);
82+
7683
export const persistenceRepository = {
7784
user: userRepository,
7885
userSocial: userSocialRepository,
7986
userSession: userSessionRepository,
8087
article: articleRepository,
8188
bookmark: bookmarkRepository,
89+
comment: commentRepository,
8290
reaction: reactionRepository,
8391
articleTagPivot: articleTagRepository,
8492
tags: tagRepository,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
-- Thanks claude 🥰 for the help with this function
2+
-- Function to recursively fetch comments and their replies up to a certain level
3+
-- This function retrieves comments for a given resource and organizes them in a nested structure
4+
-- It uses recursion to fetch replies to comments, limiting the depth to 3 levels
5+
-- Function: public.get_comments(p_resource_id uuid, p_resource_type character varying, p_parent_id uuid DEFAULT NULL::uuid, p_current_level integer DEFAULT 0)
6+
7+
8+
CREATE OR REPLACE FUNCTION public.get_comments(
9+
p_resource_id uuid,
10+
p_resource_type character varying,
11+
p_parent_id uuid DEFAULT NULL::uuid,
12+
p_current_level integer DEFAULT 0
13+
)
14+
RETURNS json
15+
LANGUAGE plpgsql
16+
AS $function$
17+
DECLARE
18+
result JSON;
19+
BEGIN
20+
-- Base case: get comments for the given parent
21+
IF p_parent_id IS NULL THEN
22+
-- Get root comments
23+
SELECT json_agg(
24+
json_build_object(
25+
'id', c.id,
26+
'body', c.body,
27+
'level', 0,
28+
'created_at', c.created_at,
29+
'parent_id', NULL,
30+
'author', json_build_object(
31+
'id', u.id,
32+
'name', u.name,
33+
'email', u.email,
34+
'username', u.username
35+
),
36+
'replies', get_comments(p_resource_id, p_resource_type, c.id, 1)
37+
) ORDER BY c.created_at DESC -- Changed to DESC
38+
) INTO result
39+
FROM comments c
40+
JOIN users u ON c.user_id = u.id
41+
WHERE c.resource_id = p_resource_id
42+
AND c.resource_type = p_resource_type;
43+
ELSE
44+
-- Get replies to a specific comment
45+
IF p_current_level >= 3 THEN
46+
-- Stop recursion at level 3
47+
RETURN '[]'::json;
48+
END IF;
49+
50+
SELECT json_agg(
51+
json_build_object(
52+
'id', c.id,
53+
'body', c.body,
54+
'level', p_current_level,
55+
'created_at', c.created_at,
56+
'parent_id', p_parent_id,
57+
'author', json_build_object(
58+
'id', u.id,
59+
'name', u.name,
60+
'email', u.email,
61+
'username', u.username
62+
),
63+
'replies', get_comments(p_resource_id, p_resource_type, c.id, p_current_level + 1)
64+
) ORDER BY c.created_at DESC -- Changed to DESC
65+
) INTO result
66+
FROM comments c
67+
JOIN users u ON c.user_id = u.id
68+
WHERE c.resource_id = p_parent_id
69+
AND c.resource_type = 'COMMENT';
70+
END IF;
71+
72+
RETURN COALESCE(result, '[]'::json);
73+
END;
74+
$function$

src/backend/play.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import * as commentActions from "./services/comment.action";
2+
3+
async function main() {
4+
// Comment on a resource
5+
const newComment = await commentActions.createMyComment({
6+
body: "This is a comment on a resource",
7+
resource_id: "14fced36-31a7-42b3-9811-8887fc1331db",
8+
resource_type: "ARTICLE",
9+
});
10+
11+
console.log(newComment);
12+
}
13+
14+
main();

src/backend/services/comment.action.ts

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,77 @@
1+
"use server";
2+
13
import z from "zod";
24
import { CommentActionInput } from "./inputs/comment.input";
5+
import { authID } from "./session.actions";
6+
import { ActionException } from "./RepositoryException";
7+
import { persistenceRepository } from "../persistence/persistence-repositories";
8+
import { eq } from "sqlkit";
9+
import { CommentPresentation } from "../models/domain-models";
10+
11+
const sql = String.raw;
312

413
export const getComments = async (
5-
resourceId: string,
6-
resourceType: "ARTICLE" | "COMMENT"
7-
) => {
8-
// Fetch comments from the database based on resourceId and resourceType
9-
// const comments = await db
10-
// .select()
11-
// .from(commentsTable)
12-
// .where(commentsTable.resource_id.eq(resourceId))
13-
// .and(commentsTable.resource_type.eq(resourceType))
14-
// .orderBy(commentsTable.created_at.desc());
15-
// return comments;
16-
return []; // Placeholder for actual database query
14+
_input: z.infer<typeof CommentActionInput.getComments>
15+
): Promise<CommentPresentation[]> => {
16+
const input = CommentActionInput.getComments.parse(_input);
17+
18+
const query = sql`
19+
SELECT get_comments($1, $2) as comments
20+
`;
21+
22+
const execution_response: any = await pgClient?.executeSQL(query, [
23+
input.resource_id,
24+
input.resource_type,
25+
]);
26+
return execution_response?.rows?.[0]?.comments || [];
1727
};
1828

19-
export const createComment = async (
29+
export const createMyComment = async (
2030
input: z.infer<typeof CommentActionInput.create>
2131
) => {
32+
const sessionId = await authID();
33+
if (!sessionId) {
34+
throw new ActionException("Unauthorized: No session ID found");
35+
}
2236
const { resource_id, resource_type, body } = input;
2337

24-
// Create the comment in the database
38+
switch (resource_type) {
39+
case "ARTICLE":
40+
// Validate that the resource exists
41+
const [exists] = await persistenceRepository.article.find({
42+
where: eq("id", resource_id),
43+
limit: 1,
44+
columns: ["id"],
45+
});
46+
if (!exists) {
47+
throw new ActionException("Resource not found");
48+
}
49+
break;
50+
case "COMMENT":
51+
// Validate that the parent comment exists
52+
const [parentExists] = await persistenceRepository.comment.find({
53+
where: eq("id", resource_id),
54+
limit: 1,
55+
columns: ["id"],
56+
});
57+
if (!parentExists) {
58+
throw new ActionException("Parent comment not found");
59+
}
60+
break;
61+
default:
62+
throw new ActionException("Invalid resource type");
63+
}
64+
65+
const created = await persistenceRepository.comment.insert([
66+
{
67+
body,
68+
resource_id,
69+
resource_type,
70+
user_id: sessionId,
71+
},
72+
]);
2573

26-
// return newComment[0];
74+
return created?.rows?.[0];
2775
};
2876

2977
export const deleteComment = async (

0 commit comments

Comments
 (0)