aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--apps/web/app/dashboard/lists/[listId]/page.tsx27
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx31
-rw-r--r--apps/web/lib/hooks/list-context.tsx21
-rw-r--r--packages/trpc/routers/bookmarks.ts2
-rw-r--r--packages/trpc/routers/lists.ts31
5 files changed, 96 insertions, 16 deletions
diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx
index f28d94b1..bac2b5c7 100644
--- a/apps/web/app/dashboard/lists/[listId]/page.tsx
+++ b/apps/web/app/dashboard/lists/[listId]/page.tsx
@@ -1,6 +1,7 @@
import { notFound, redirect } from "next/navigation";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import DeleteListButton from "@/components/dashboard/lists/DeleteListButton";
+import { BookmarkListContextProvider } from "@/lib/hooks/list-context";
import { api } from "@/server/api/client";
import { getServerAuthSession } from "@/server/auth";
import { TRPCError } from "@trpc/server";
@@ -28,17 +29,19 @@ export default async function ListPage({
}
return (
- <Bookmarks
- query={{ listId: list.id, archived: false }}
- showDivider={true}
- header={
- <div className="flex justify-between">
- <span className="pt-4 text-2xl">
- {list.icon} {list.name}
- </span>
- <DeleteListButton list={list} />
- </div>
- }
- />
+ <BookmarkListContextProvider listId={list.id}>
+ <Bookmarks
+ query={{ listId: list.id, archived: false }}
+ showDivider={true}
+ header={
+ <div className="flex justify-between">
+ <span className="pt-4 text-2xl">
+ {list.icon} {list.name}
+ </span>
+ <DeleteListButton list={list} />
+ </div>
+ }
+ />
+ </BookmarkListContextProvider>
);
}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
index 692d7d78..249946b4 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
@@ -1,6 +1,6 @@
"use client";
-import { useState } from "react";
+import { useContext, useState } from "react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
@@ -10,11 +10,13 @@ import {
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/components/ui/use-toast";
import { useClientConfig } from "@/lib/clientConfig";
+import { BookmarkListContext } from "@/lib/hooks/list-context";
import { api } from "@/lib/trpc";
import {
Archive,
Link,
List,
+ ListX,
MoreHorizontal,
Pencil,
RotateCw,
@@ -42,6 +44,8 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
const [isTextEditorOpen, setTextEditorOpen] = useState(false);
+ const { listId } = useContext(BookmarkListContext);
+
const invalidateAllBookmarksCache =
api.useUtils().bookmarks.getBookmarks.invalidate;
@@ -92,6 +96,16 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
},
});
+ const removeFromListMutator = api.lists.removeFromList.useMutation({
+ onSuccess: (_resp, req) => {
+ invalidateAllBookmarksCache({ listId: req.listId });
+ toast({
+ description: "The bookmark has been deleted from the list",
+ });
+ },
+ onError,
+ });
+
return (
<>
{tagModal}
@@ -166,6 +180,21 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
<span>Add to List</span>
</DropdownMenuItem>
+ {listId && (
+ <DropdownMenuItem
+ disabled={demoMode}
+ onClick={() =>
+ removeFromListMutator.mutate({
+ listId,
+ bookmarkId: bookmark.id,
+ })
+ }
+ >
+ <ListX className="mr-2 size-4" />
+ <span>Remove from List</span>
+ </DropdownMenuItem>
+ )}
+
{bookmark.content.type === "link" && (
<DropdownMenuItem
disabled={demoMode}
diff --git a/apps/web/lib/hooks/list-context.tsx b/apps/web/lib/hooks/list-context.tsx
new file mode 100644
index 00000000..cb8a20b2
--- /dev/null
+++ b/apps/web/lib/hooks/list-context.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { createContext } from "react";
+
+export const BookmarkListContext = createContext<{
+ listId: string | undefined;
+}>({ listId: undefined });
+
+export function BookmarkListContextProvider({
+ listId,
+ children,
+}: {
+ listId: string;
+ children: React.ReactNode;
+}) {
+ return (
+ <BookmarkListContext.Provider value={{ listId }}>
+ {children}
+ </BookmarkListContext.Provider>
+ );
+}
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index cd3ab17c..3a49c7fa 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -35,7 +35,7 @@ import {
} from "../types/bookmarks";
import { ZBookmarkTags } from "../types/tags";
-const ensureBookmarkOwnership = experimental_trpcMiddleware<{
+export const ensureBookmarkOwnership = experimental_trpcMiddleware<{
ctx: Context;
input: { bookmarkId: string };
}>().create(async (opts) => {
diff --git a/packages/trpc/routers/lists.ts b/packages/trpc/routers/lists.ts
index cbce3970..db5bb38e 100644
--- a/packages/trpc/routers/lists.ts
+++ b/packages/trpc/routers/lists.ts
@@ -7,8 +7,9 @@ import { bookmarkLists, bookmarksInLists } from "@hoarder/db/schema";
import { authedProcedure, Context, router } from "../index";
import { zBookmarkListSchema } from "../types/lists";
+import { ensureBookmarkOwnership } from "./bookmarks";
-const ensureListOwnership = experimental_trpcMiddleware<{
+export const ensureListOwnership = experimental_trpcMiddleware<{
ctx: Context;
input: { listId: string };
}>().create(async (opts) => {
@@ -106,6 +107,7 @@ export const listsAppRouter = router({
}),
)
.use(ensureListOwnership)
+ .use(ensureBookmarkOwnership)
.mutation(async ({ input, ctx }) => {
try {
await ctx.db.insert(bookmarksInLists).values({
@@ -117,7 +119,7 @@ export const listsAppRouter = router({
if (e.code == "SQLITE_CONSTRAINT_PRIMARYKEY") {
throw new TRPCError({
code: "BAD_REQUEST",
- message: "Bookmark already in the list",
+ message: `Bookmark ${input.bookmarkId} is already in the list ${input.listId}`,
});
}
}
@@ -127,6 +129,31 @@ export const listsAppRouter = router({
});
}
}),
+ removeFromList: authedProcedure
+ .input(
+ z.object({
+ listId: z.string(),
+ bookmarkId: z.string(),
+ }),
+ )
+ .use(ensureListOwnership)
+ .use(ensureBookmarkOwnership)
+ .mutation(async ({ input, ctx }) => {
+ const deleted = await ctx.db
+ .delete(bookmarksInLists)
+ .where(
+ and(
+ eq(bookmarksInLists.listId, input.listId),
+ eq(bookmarksInLists.bookmarkId, input.bookmarkId),
+ ),
+ );
+ if (deleted.changes == 0) {
+ throw new TRPCError({
+ code: "BAD_REQUEST",
+ message: `Bookmark ${input.bookmarkId} is already not in list ${input.listId}`,
+ });
+ }
+ }),
get: authedProcedure
.input(
z.object({