aboutsummaryrefslogtreecommitdiffstats
path: root/packages/web
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web')
-rw-r--r--packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts (renamed from packages/web/app/api/v1/links/[linkId]/route.ts)6
-rw-r--r--packages/web/app/api/v1/bookmarks/route.ts (renamed from packages/web/app/api/v1/links/route.ts)22
-rw-r--r--packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx27
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinkCard.tsx15
-rw-r--r--packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx21
-rw-r--r--packages/web/app/dashboard/bookmarks/page.tsx4
-rw-r--r--packages/web/lib/api.ts19
-rw-r--r--packages/web/lib/services/bookmarks.ts92
-rw-r--r--packages/web/lib/services/links.ts78
-rw-r--r--packages/web/lib/types/api/bookmarks.ts37
-rw-r--r--packages/web/lib/types/api/links.ts33
-rw-r--r--packages/web/lib/types/api/tags.ts1
-rw-r--r--packages/web/package.json1
13 files changed, 191 insertions, 165 deletions
diff --git a/packages/web/app/api/v1/links/[linkId]/route.ts b/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
index 39449d6d..6adcf771 100644
--- a/packages/web/app/api/v1/links/[linkId]/route.ts
+++ b/packages/web/app/api/v1/bookmarks/[bookmarkId]/route.ts
@@ -1,5 +1,5 @@
import { authOptions } from "@/lib/auth";
-import { unbookmarkLink } from "@/lib/services/links";
+import { deleteBookmark } from "@/lib/services/bookmarks";
import { Prisma } from "@remember/db";
import { getServerSession } from "next-auth";
@@ -7,7 +7,7 @@ import { NextRequest } from "next/server";
export async function DELETE(
_request: NextRequest,
- { params }: { params: { linkId: string } },
+ { params }: { params: { bookmarkId: string } },
) {
// TODO: We probably should be using an API key here instead of the session;
const session = await getServerSession(authOptions);
@@ -16,7 +16,7 @@ export async function DELETE(
}
try {
- await unbookmarkLink(params.linkId, session.user.id);
+ await deleteBookmark(params.bookmarkId, session.user.id);
} catch (e: unknown) {
if (
e instanceof Prisma.PrismaClientKnownRequestError &&
diff --git a/packages/web/app/api/v1/links/route.ts b/packages/web/app/api/v1/bookmarks/route.ts
index 87541634..b9305ca8 100644
--- a/packages/web/app/api/v1/links/route.ts
+++ b/packages/web/app/api/v1/bookmarks/route.ts
@@ -1,11 +1,11 @@
import { authOptions } from "@/lib/auth";
-import { bookmarkLink, getLinks } from "@/lib/services/links";
+import { bookmarkLink, getBookmarks } from "@/lib/services/bookmarks";
import {
- zNewBookmarkedLinkRequestSchema,
- ZGetLinksResponse,
- ZBookmarkedLink,
-} from "@/lib/types/api/links";
+ zNewBookmarkRequestSchema,
+ ZGetBookmarksResponse,
+ ZBookmark,
+} from "@/lib/types/api/bookmarks";
import { getServerSession } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
@@ -16,9 +16,7 @@ export async function POST(request: NextRequest) {
return new Response(null, { status: 401 });
}
- const linkRequest = zNewBookmarkedLinkRequestSchema.safeParse(
- await request.json(),
- );
+ const linkRequest = zNewBookmarkRequestSchema.safeParse(await request.json());
if (!linkRequest.success) {
return NextResponse.json(
@@ -29,9 +27,9 @@ export async function POST(request: NextRequest) {
);
}
- const link = await bookmarkLink(linkRequest.data.url, session.user.id);
+ const bookmark = await bookmarkLink(linkRequest.data.url, session.user.id);
- let response: ZBookmarkedLink = { ...link };
+ let response: ZBookmark = { ...bookmark };
return NextResponse.json(response, { status: 201 });
}
@@ -42,8 +40,8 @@ export async function GET() {
return new Response(null, { status: 401 });
}
- const links = await getLinks(session.user.id);
+ const bookmarks = await getBookmarks(session.user.id);
- let response: ZGetLinksResponse = { links };
+ let response: ZGetBookmarksResponse = { bookmarks };
return NextResponse.json(response);
}
diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
new file mode 100644
index 00000000..2b6b19b6
--- /dev/null
+++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx
@@ -0,0 +1,27 @@
+import { getServerSession } from "next-auth";
+import { redirect } from "next/navigation";
+import { authOptions } from "@/lib/auth";
+import { getBookmarks } from "@/lib/services/bookmarks";
+import LinkCard from "./LinkCard";
+import { ZBookmark } from "@/lib/types/api/bookmarks";
+
+function renderBookmark(bookmark: ZBookmark) {
+ switch (bookmark.content.type) {
+ case "link":
+ return <LinkCard key={bookmark.id} bookmark={bookmark} />;
+ }
+}
+
+export default async function BookmarksGrid() {
+ const session = await getServerSession(authOptions);
+ if (!session) {
+ redirect("/");
+ }
+ const bookmarks = await getBookmarks(session.user.id);
+
+ return (
+ <div className="container grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
+ {bookmarks.map((b) => renderBookmark(b))}
+ </div>
+ );
+}
diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
index b5a051e8..1cbd8865 100644
--- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
+++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx
@@ -16,7 +16,7 @@ import {
} from "@/components/ui/imageCard";
import { useToast } from "@/components/ui/use-toast";
import APIClient from "@/lib/api";
-import { ZBookmarkedLink } from "@/lib/types/api/links";
+import { ZBookmark } from "@/lib/types/api/bookmarks";
import { MoreHorizontal, Trash2 } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
@@ -26,7 +26,7 @@ export function LinkOptions({ linkId }: { linkId: string }) {
const router = useRouter();
const unbookmarkLink = async () => {
- let [_, error] = await APIClient.unbookmarkLink(linkId);
+ let [_, error] = await APIClient.deleteBookmark(linkId);
if (error) {
toast({
@@ -59,7 +59,8 @@ export function LinkOptions({ linkId }: { linkId: string }) {
);
}
-export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
+export default function LinkCard({ bookmark }: { bookmark: ZBookmark }) {
+ const link = bookmark.content;
const parsedUrl = new URL(link.url);
return (
@@ -67,15 +68,15 @@ export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
className={
"bg-gray-50 duration-300 ease-in border border-grey-100 hover:transition-all hover:border-blue-300"
}
- image={link.details?.imageUrl ?? undefined}
+ image={link?.imageUrl ?? undefined}
>
<ImageCardTitle>
<Link className="line-clamp-3" href={link.url}>
- {link.details?.title ?? parsedUrl.host}
+ {link?.title ?? parsedUrl.host}
</Link>
</ImageCardTitle>
<ImageCardBody className="py-2 overflow-clip">
- {link.tags.map((t) => (
+ {bookmark.tags.map((t) => (
<Badge
variant="default"
className="bg-gray-300 text-gray-500"
@@ -92,7 +93,7 @@ export default function LinkCard({ link }: { link: ZBookmarkedLink }) {
{parsedUrl.host}
</Link>
</div>
- <LinkOptions linkId={link.id} />
+ <LinkOptions linkId={bookmark.id} />
</div>
</ImageCardFooter>
</ImageCard>
diff --git a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx
deleted file mode 100644
index 66f0d766..00000000
--- a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { getServerSession } from "next-auth";
-import { redirect } from "next/navigation";
-import { authOptions } from "@/lib/auth";
-import { getLinks } from "@/lib/services/links";
-import LinkCard from "./LinkCard";
-
-export default async function LinksGrid() {
- const session = await getServerSession(authOptions);
- if (!session) {
- redirect("/");
- }
- const links = await getLinks(session.user.id);
-
- return (
- <div className="container grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
- {links.map((l) => (
- <LinkCard key={l.id} link={l} />
- ))}
- </div>
- );
-}
diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx
index b4158893..b9eabfe8 100644
--- a/packages/web/app/dashboard/bookmarks/page.tsx
+++ b/packages/web/app/dashboard/bookmarks/page.tsx
@@ -1,5 +1,5 @@
import AddLink from "./components/AddLink";
-import LinksGrid from "./components/LinksGrid";
+import BookmarksGrid from "./components/BookmarksGrid";
import type { Metadata } from "next";
export const metadata: Metadata = {
@@ -13,7 +13,7 @@ export default async function Bookmarks() {
<AddLink />
</div>
<div>
- <LinksGrid />
+ <BookmarksGrid />
</div>
</div>
);
diff --git a/packages/web/lib/api.ts b/packages/web/lib/api.ts
index 56686cde..8ee08601 100644
--- a/packages/web/lib/api.ts
+++ b/packages/web/lib/api.ts
@@ -2,9 +2,9 @@
import { ZodTypeAny, z } from "zod";
import {
- ZNewBookmarkedLinkRequest,
- zGetLinksResponseSchema,
-} from "./types/api/links";
+ ZNewBookmarkRequest,
+ zGetBookmarksResponseSchema,
+} from "./types/api/bookmarks";
import serverConfig from "./config";
@@ -57,24 +57,25 @@ async function doRequest<T>(
}
export default class APIClient {
- static async getLinks() {
- return await doRequest(`/links`, zGetLinksResponseSchema, {
+ static async getBookmarks() {
+ return await doRequest(`/bookmarks`, zGetBookmarksResponseSchema, {
next: { tags: ["links"] },
});
}
static async bookmarkLink(url: string) {
- const body: ZNewBookmarkedLinkRequest = {
+ const body: ZNewBookmarkRequest = {
+ type: "link",
url,
};
- return await doRequest(`/links`, undefined, {
+ return await doRequest(`/bookmarks`, undefined, {
method: "POST",
body: JSON.stringify(body),
});
}
- static async unbookmarkLink(linkId: string) {
- return await doRequest(`/links/${linkId}`, undefined, {
+ static async deleteBookmark(id: string) {
+ return await doRequest(`/bookmarks/${id}`, undefined, {
method: "DELETE",
});
}
diff --git a/packages/web/lib/services/bookmarks.ts b/packages/web/lib/services/bookmarks.ts
new file mode 100644
index 00000000..3c9929bc
--- /dev/null
+++ b/packages/web/lib/services/bookmarks.ts
@@ -0,0 +1,92 @@
+import { LinkCrawlerQueue } from "@remember/shared/queues";
+import prisma from "@remember/db";
+import { ZBookmark, ZBookmarkContent } from "@/lib/types/api/bookmarks";
+
+const defaultBookmarkFields = {
+ id: true,
+ favourited: true,
+ archived: true,
+ createdAt: true,
+ link: {
+ select: {
+ url: true,
+ title: true,
+ description: true,
+ imageUrl: true,
+ favicon: true,
+ },
+ },
+ tags: {
+ include: {
+ tag: true,
+ },
+ },
+};
+
+async function dummyPrismaReturnType() {
+ const x = await prisma.bookmark.findFirstOrThrow({
+ select: defaultBookmarkFields,
+ });
+ return x;
+}
+
+function toZodSchema(
+ bookmark: Awaited<ReturnType<typeof dummyPrismaReturnType>>,
+): ZBookmark {
+ const { tags, link, ...rest } = bookmark;
+
+ let content: ZBookmarkContent;
+ if (link) {
+ content = { type: "link", ...link };
+ } else {
+ throw new Error("Unknown content type");
+ }
+
+ return {
+ tags: tags.map((t) => t.tag),
+ content,
+ ...rest,
+ };
+}
+
+export async function deleteBookmark(bookmarkId: string, userId: string) {
+ await prisma.bookmark.delete({
+ where: {
+ id: bookmarkId,
+ userId,
+ },
+ });
+}
+
+export async function bookmarkLink(url: string, userId: string) {
+ const bookmark = await prisma.bookmark.create({
+ data: {
+ link: {
+ create: {
+ url,
+ },
+ },
+ userId,
+ },
+ select: defaultBookmarkFields,
+ });
+
+ // Enqueue crawling request
+ await LinkCrawlerQueue.add("crawl", {
+ bookmarkId: bookmark.id,
+ url: url,
+ });
+
+ return toZodSchema(bookmark);
+}
+
+export async function getBookmarks(userId: string) {
+ return (
+ await prisma.bookmark.findMany({
+ where: {
+ userId,
+ },
+ select: defaultBookmarkFields,
+ })
+ ).map(toZodSchema);
+}
diff --git a/packages/web/lib/services/links.ts b/packages/web/lib/services/links.ts
deleted file mode 100644
index d273b118..00000000
--- a/packages/web/lib/services/links.ts
+++ /dev/null
@@ -1,78 +0,0 @@
-import { LinkCrawlerQueue } from "@remember/shared/queues";
-import prisma from "@remember/db";
-import { ZBookmarkedLink } from "@/lib/types/api/links";
-
-const defaultLinkFields = {
- id: true,
- url: true,
- createdAt: true,
- details: {
- select: {
- title: true,
- description: true,
- imageUrl: true,
- favicon: true,
- },
- },
- tags: {
- include: {
- tag: true,
- },
- },
-};
-
-async function dummyPrismaReturnType() {
- return await prisma.bookmarkedLink.findFirstOrThrow({
- select: defaultLinkFields,
- });
-}
-
-function toZodSchema(
- link: Awaited<ReturnType<typeof dummyPrismaReturnType>>,
-): ZBookmarkedLink {
- return {
- id: link.id,
- url: link.url,
- createdAt: link.createdAt,
- details: link.details,
- tags: link.tags.map((t) => t.tag),
- };
-}
-
-export async function unbookmarkLink(linkId: string, userId: string) {
- await prisma.bookmarkedLink.delete({
- where: {
- id: linkId,
- userId,
- },
- });
-}
-
-export async function bookmarkLink(url: string, userId: string) {
- const link = await prisma.bookmarkedLink.create({
- data: {
- url,
- userId,
- },
- select: defaultLinkFields,
- });
-
- // Enqueue crawling request
- await LinkCrawlerQueue.add("crawl", {
- linkId: link.id,
- url: link.url,
- });
-
- return toZodSchema(link);
-}
-
-export async function getLinks(userId: string) {
- return (
- await prisma.bookmarkedLink.findMany({
- where: {
- userId,
- },
- select: defaultLinkFields,
- })
- ).map(toZodSchema);
-}
diff --git a/packages/web/lib/types/api/bookmarks.ts b/packages/web/lib/types/api/bookmarks.ts
new file mode 100644
index 00000000..485fbfab
--- /dev/null
+++ b/packages/web/lib/types/api/bookmarks.ts
@@ -0,0 +1,37 @@
+import { z } from "zod";
+import { zBookmarkTagSchema } from "@/lib/types/api/tags";
+
+export const zBookmarkedLinkSchema = z.object({
+ type: z.literal("link"),
+ url: z.string().url(),
+ title: z.string().nullish(),
+ description: z.string().nullish(),
+ imageUrl: z.string().url().nullish(),
+ favicon: z.string().url().nullish(),
+});
+export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
+
+export const zBookmarkContentSchema = z.discriminatedUnion("type", [
+ zBookmarkedLinkSchema,
+]);
+export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>;
+
+export const zBookmarkSchema = z.object({
+ id: z.string(),
+ createdAt: z.coerce.date(),
+ archived: z.boolean(),
+ favourited: z.boolean(),
+ tags: z.array(zBookmarkTagSchema),
+ content: zBookmarkContentSchema,
+});
+export type ZBookmark = z.infer<typeof zBookmarkSchema>;
+
+// POST /v1/bookmarks
+export const zNewBookmarkRequestSchema = zBookmarkContentSchema;
+export type ZNewBookmarkRequest = z.infer<typeof zNewBookmarkRequestSchema>;
+
+// GET /v1/bookmarks
+export const zGetBookmarksResponseSchema = z.object({
+ bookmarks: z.array(zBookmarkSchema),
+});
+export type ZGetBookmarksResponse = z.infer<typeof zGetBookmarksResponseSchema>;
diff --git a/packages/web/lib/types/api/links.ts b/packages/web/lib/types/api/links.ts
deleted file mode 100644
index f84445f6..00000000
--- a/packages/web/lib/types/api/links.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { z } from "zod";
-import { zBookmarkTagSchema } from "@/lib/types/api/tags";
-
-export const zBookmarkedLinkSchema = z.object({
- id: z.string(),
- url: z.string().url(),
- createdAt: z.coerce.date(),
-
- details: z
- .object({
- title: z.string().nullish(),
- description: z.string().nullish(),
- imageUrl: z.string().url().nullish(),
- favicon: z.string().url().nullish(),
- })
- .nullish(),
- tags: z.array(zBookmarkTagSchema),
-});
-export type ZBookmarkedLink = z.infer<typeof zBookmarkedLinkSchema>;
-
-// POST /v1/links
-export const zNewBookmarkedLinkRequestSchema = zBookmarkedLinkSchema.pick({
- url: true,
-});
-export type ZNewBookmarkedLinkRequest = z.infer<
- typeof zNewBookmarkedLinkRequestSchema
->;
-
-// GET /v1/links
-export const zGetLinksResponseSchema = z.object({
- links: z.array(zBookmarkedLinkSchema),
-});
-export type ZGetLinksResponse = z.infer<typeof zGetLinksResponseSchema>;
diff --git a/packages/web/lib/types/api/tags.ts b/packages/web/lib/types/api/tags.ts
index f2d2bc18..bcd16f5b 100644
--- a/packages/web/lib/types/api/tags.ts
+++ b/packages/web/lib/types/api/tags.ts
@@ -4,3 +4,4 @@ export const zBookmarkTagSchema = z.object({
id: z.string(),
name: z.string(),
});
+export type ZBookmarkTags = z.infer<typeof zBookmarkTagSchema>;
diff --git a/packages/web/package.json b/packages/web/package.json
index 8a315beb..5762a476 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -13,6 +13,7 @@
"@hookform/resolvers": "^3.3.4",
"@next-auth/prisma-adapter": "^1.0.7",
"@next/eslint-plugin-next": "^14.1.0",
+ "@prisma/client": "^5.9.1",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",