Skip to content

Commit 056c7d7

Browse files
authored
Merge pull request #50 from techdiary-dev/kingrayhan/bookmark
Kingrayhan/bookmark
2 parents be6aea0 + 5eb33af commit 056c7d7

File tree

6 files changed

+223
-50
lines changed

6 files changed

+223
-50
lines changed

src/app/api/play/route.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
1-
import { getComments } from "@/backend/services/comment.action";
21
import { NextResponse } from "next/server";
32

43
export async function GET() {
5-
const comments = await getComments({
6-
resource_id: "16196e73-275a-4af5-9186-39a5fec4244e",
7-
resource_type: "ARTICLE",
8-
});
9-
10-
return NextResponse.json(comments, {
11-
status: 200,
12-
headers: { "Content-Type": "application/json" },
13-
});
4+
return NextResponse.json(
5+
{ play: true },
6+
{
7+
status: 200,
8+
headers: { "Content-Type": "application/json" },
9+
}
10+
);
1411
}

src/app/dashboard/_components/DashboardSidebar.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ const DashboardSidebar = () => {
4141
// url: "/notifications",
4242
// icon: BellIcon,
4343
// },
44-
// {
45-
// title: _t("Bookmarks"),
46-
// url: "/bookmarks",
47-
// icon: Bookmark,
48-
// },
44+
{
45+
title: _t("Bookmarks"),
46+
url: "/bookmarks",
47+
icon: Bookmark,
48+
},
4949
{
5050
title: _t("Settings"),
5151
url: "/settings",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use client";
2+
3+
import {
4+
myBookmarks,
5+
toggleResourceBookmark,
6+
} from "@/backend/services/bookmark.action";
7+
import { useAppConfirm } from "@/components/app-confirm";
8+
import { Button } from "@/components/ui/button";
9+
import VisibilitySensor from "@/components/VisibilitySensor";
10+
import { useTranslation } from "@/i18n/use-translation";
11+
import { formattedTime } from "@/lib/utils";
12+
import { ChatBubbleIcon } from "@radix-ui/react-icons";
13+
import { useInfiniteQuery } from "@tanstack/react-query";
14+
import { RemoveFormatting, Trash } from "lucide-react";
15+
import Link from "next/link";
16+
import { useMemo } from "react";
17+
18+
interface BookmarkMeta {
19+
totalCount: number;
20+
currentPage: number;
21+
hasNextPage: boolean;
22+
totalPages: number;
23+
}
24+
25+
interface BookmarkData {
26+
nodes: any[];
27+
meta: BookmarkMeta;
28+
}
29+
30+
const BookmarksPage = () => {
31+
const { _t } = useTranslation();
32+
const feedInfiniteQuery = useInfiniteQuery({
33+
queryKey: ["dashboard-articles"],
34+
queryFn: ({ pageParam }) =>
35+
myBookmarks({ limit: 10, page: pageParam, offset: 0 }),
36+
initialPageParam: 1,
37+
getNextPageParam: (lastPage) => {
38+
const _page = lastPage?.meta?.currentPage ?? 1;
39+
const _totalPages = lastPage?.meta?.totalPages ?? 1;
40+
return _page + 1 <= _totalPages ? _page + 1 : null;
41+
},
42+
});
43+
44+
const hasItems = useMemo(() => {
45+
const length = feedInfiniteQuery.data?.pages.flat()[0]?.nodes.length ?? 0;
46+
return length > 0;
47+
}, [feedInfiniteQuery]);
48+
49+
const appConfirm = useAppConfirm();
50+
return (
51+
<div>
52+
<h3 className="text-xl font-semibold">{_t("Bookmarks")}</h3>
53+
54+
{!hasItems && (
55+
<div className=" min-h-30 border border-dashed border-muted grid place-content-center mt-4">
56+
<h3 className="text-xl">
57+
{_t("You didn't bookmark any article yet")}
58+
</h3>
59+
</div>
60+
)}
61+
62+
<div className="flex flex-col divide-y divide-dashed divide-border-color mt-2">
63+
{feedInfiniteQuery.isFetching &&
64+
Array.from({ length: 10 }).map((_, i) => (
65+
<article key={i} className=" bg-muted h-20 animate-pulse" />
66+
))}
67+
68+
{feedInfiniteQuery.data?.pages.map((page) => {
69+
return page?.nodes.map((bookmark) => (
70+
<article
71+
key={bookmark.id}
72+
className="flex justify-between flex-col md:flex-row py-3 space-y-2"
73+
>
74+
<div className="flex flex-col">
75+
<Link
76+
className="text-forground text-lg"
77+
href={`/@${bookmark?.article?.path}`}
78+
>
79+
{bookmark?.article?.title}
80+
</Link>
81+
{bookmark?.created_at && (
82+
<p className="text-sm text-muted-foreground">
83+
{_t("Bookmarked on")} {formattedTime(bookmark?.created_at!)}
84+
</p>
85+
)}
86+
</div>
87+
88+
<div className="flex items-center gap-10 justify-between">
89+
<div className="flex gap-4 items-center">
90+
<div className="text-forground-muted flex items-center gap-1">
91+
<Button
92+
variant={"destructive"}
93+
size={"sm"}
94+
onClick={() =>
95+
appConfirm.show({
96+
title: _t("Sure to remove from bookmark?"),
97+
labels: { confirm: _t("Remove") },
98+
onConfirm() {
99+
toggleResourceBookmark({
100+
resource_id: bookmark.article.id,
101+
resource_type: "ARTICLE",
102+
}).finally(() => feedInfiniteQuery.refetch());
103+
},
104+
})
105+
}
106+
>
107+
<Trash className="h-4 w-4" />
108+
{_t("Remove")}
109+
</Button>
110+
</div>
111+
</div>
112+
</div>
113+
</article>
114+
));
115+
})}
116+
</div>
117+
{feedInfiniteQuery.hasNextPage && (
118+
<VisibilitySensor onLoadmore={feedInfiniteQuery.fetchNextPage} />
119+
)}
120+
</div>
121+
);
122+
};
123+
124+
export default BookmarksPage;

src/backend/models/domain-models.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,28 @@ export interface Bookmark {
130130
created_at: Date;
131131
}
132132

133+
export interface BookmarkArticlePresentation {
134+
id: string;
135+
resource_id: string;
136+
resource_type: "ARTICLE" | "COMMENT";
137+
user_id: string;
138+
created_at: Date;
139+
140+
article: {
141+
id: string;
142+
title: string;
143+
path: string;
144+
cover_image?: IServerFile | null;
145+
author: {
146+
id: string;
147+
name: string;
148+
username: string;
149+
email: string;
150+
profile_photo: string;
151+
};
152+
};
153+
}
154+
133155
export interface Comment {
134156
id: string;
135157
resource_id: string;

src/backend/services/bookmark.action.ts

Lines changed: 62 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"use server";
22

3+
import { and, eq } from "sqlkit";
34
import z from "zod";
5+
import { persistenceRepository } from "../persistence/persistence-repositories";
6+
import { pgClient } from "../persistence/clients";
47
import { BookmarkActionInput } from "./inputs/bookmark.input";
5-
import { authID } from "./session.actions";
68
import { ActionException, handleActionException } from "./RepositoryException";
7-
import { persistenceRepository } from "../persistence/persistence-repositories";
8-
import { and, eq } from "sqlkit";
9+
import { authID } from "./session.actions";
10+
import { BookmarkArticlePresentation } from "../models/domain-models";
11+
12+
const sql = String.raw;
913

1014
export async function toggleResourceBookmark(
1115
_input: z.infer<typeof BookmarkActionInput.toggleBookmarkInput>
@@ -56,47 +60,71 @@ export async function toggleResourceBookmark(
5660
}
5761

5862
export async function myBookmarks(
59-
_input: z.infer<typeof BookmarkActionInput.toggleBookmarkInput>
63+
_input: z.infer<typeof BookmarkActionInput.myBookmarks>
6064
) {
6165
try {
6266
const sessionUserId = await authID();
6367
if (!sessionUserId) {
6468
throw new ActionException("Unauthorized");
6569
}
66-
const input =
67-
await BookmarkActionInput.toggleBookmarkInput.parseAsync(_input);
6870

69-
// -----------
70-
const [existingBookmark] = await persistenceRepository.bookmark.find({
71-
limit: 1,
72-
where: and(
73-
eq("resource_id", input.resource_id),
74-
eq("resource_type", input.resource_type),
75-
eq("user_id", sessionUserId)
76-
),
77-
});
71+
const input = await BookmarkActionInput.myBookmarks.parseAsync(_input);
72+
const resourceType = "ARTICLE";
73+
const offset = input.page > 1 ? (input.page - 1) * input.limit : 0;
7874

79-
if (existingBookmark) {
80-
// If bookmark exists, delete it
81-
await persistenceRepository.bookmark.delete({
82-
where: and(
83-
eq("resource_id", input.resource_id),
84-
eq("resource_type", input.resource_type),
85-
eq("user_id", sessionUserId)
86-
),
87-
});
88-
return { bookmarked: false };
89-
}
75+
const countQuery = sql`
76+
SELECT COUNT(*) AS totalCount
77+
FROM bookmarks
78+
WHERE user_id = $1 AND resource_type = $2
79+
`;
9080

91-
// If bookmark does not exist, create it
92-
await persistenceRepository.bookmark.insert([
93-
{
94-
resource_id: input.resource_id,
95-
resource_type: input.resource_type,
96-
user_id: sessionUserId,
97-
},
81+
const countResult: any = await pgClient?.executeSQL(countQuery, [
82+
sessionUserId,
83+
resourceType,
9884
]);
99-
return { bookmarked: true };
85+
const totalCount = countResult?.rows[0]?.totalcount;
86+
const totalPages = Math.ceil(totalCount / input.limit);
87+
88+
const bookmarksQuery = sql`
89+
SELECT
90+
bookmarks.*,
91+
json_build_object(
92+
'id', articles.id,
93+
'title', articles.title,
94+
'cover_image', articles.cover_image,
95+
'path', concat(users.username, '/', articles.handle),
96+
'author', json_build_object(
97+
'id', users.id,
98+
'name', users.name,
99+
'username', users.username,
100+
'email', users.email,
101+
'profile_photo', users.profile_photo
102+
)
103+
) AS article
104+
FROM bookmarks
105+
LEFT JOIN articles ON articles.id = bookmarks.resource_id
106+
LEFT JOIN users ON users.id = articles.author_id
107+
WHERE bookmarks.user_id = $1 AND bookmarks.resource_type = $2
108+
ORDER BY bookmarks.created_at DESC
109+
LIMIT $3 OFFSET $4
110+
`;
111+
112+
const bookmarks = await pgClient?.executeSQL(bookmarksQuery, [
113+
sessionUserId,
114+
resourceType,
115+
input.limit,
116+
offset,
117+
]);
118+
119+
return {
120+
nodes: bookmarks?.rows as BookmarkArticlePresentation[],
121+
meta: {
122+
totalCount: Number(totalCount),
123+
currentPage: input.page,
124+
hasNextPage: input.page < totalPages,
125+
totalPages,
126+
},
127+
};
100128
} catch (error) {
101129
handleActionException(error);
102130
}

src/backend/services/inputs/bookmark.input.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ export const BookmarkActionInput = {
66
resource_type: z.enum(["ARTICLE", "COMMENT"]),
77
}),
88
myBookmarks: z.object({
9-
resource_type: z.enum(["ARTICLE", "COMMENT"]).optional(),
9+
limit: z.number().min(1).max(100).default(2),
10+
offset: z.number().min(0).default(0),
11+
page: z.number().min(1).default(1),
1012
}),
1113
bookmarkStatusInput: z.object({
1214
resource_id: z.string(),

0 commit comments

Comments
 (0)