diff options
| -rw-r--r-- | apps/web/app/dashboard/lists/[listId]/page.tsx | 27 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx | 31 | ||||
| -rw-r--r-- | apps/web/lib/hooks/list-context.tsx | 21 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 2 | ||||
| -rw-r--r-- | packages/trpc/routers/lists.ts | 31 |
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({ |
