aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/dashboard')
-rw-r--r--apps/web/components/dashboard/BulkBookmarksAction.tsx171
-rw-r--r--apps/web/components/dashboard/ChangeLayout.tsx12
-rw-r--r--apps/web/components/dashboard/GlobalActions.tsx13
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx2
-rw-r--r--apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx62
-rw-r--r--apps/web/components/dashboard/bookmarks/icons.tsx12
-rw-r--r--apps/web/components/dashboard/lists/ListHeader.tsx6
7 files changed, 263 insertions, 15 deletions
diff --git a/apps/web/components/dashboard/BulkBookmarksAction.tsx b/apps/web/components/dashboard/BulkBookmarksAction.tsx
new file mode 100644
index 00000000..b78071ee
--- /dev/null
+++ b/apps/web/components/dashboard/BulkBookmarksAction.tsx
@@ -0,0 +1,171 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import {
+ ActionButton,
+ ActionButtonWithTooltip,
+} from "@/components/ui/action-button";
+import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog";
+import { useToast } from "@/components/ui/use-toast";
+import useBulkActionsStore from "@/lib/bulkActions";
+import { Pencil, Trash2, X } from "lucide-react";
+
+import {
+ useDeleteBookmark,
+ useUpdateBookmark,
+} from "@hoarder/shared-react/hooks/bookmarks";
+
+import { ArchivedActionIcon, FavouritedActionIcon } from "./bookmarks/icons";
+
+export default function BulkBookmarksAction() {
+ const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore();
+ const setIsBulkEditEnabled = useBulkActionsStore(
+ (state) => state.setIsBulkEditEnabled,
+ );
+ const { toast } = useToast();
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+
+ useEffect(() => {
+ setIsBulkEditEnabled(false); // turn off toggle + clear selected bookmarks on mount
+ }, []);
+
+ const onError = () => {
+ toast({
+ variant: "destructive",
+ title: "Something went wrong",
+ description: "There was a problem with your request.",
+ });
+ };
+
+ const deleteBookmarkMutator = useDeleteBookmark({
+ onSuccess: () => {
+ setIsBulkEditEnabled(false);
+ },
+ onError,
+ });
+
+ const updateBookmarkMutator = useUpdateBookmark({
+ onSuccess: () => {
+ setIsBulkEditEnabled(false);
+ },
+ onError,
+ });
+
+ interface UpdateBookmarkProps {
+ favourited?: boolean;
+ archived?: boolean;
+ }
+
+ const updateBookmarks = async ({
+ favourited,
+ archived,
+ }: UpdateBookmarkProps) => {
+ await Promise.all(
+ selectedBookmarks.map((item) =>
+ updateBookmarkMutator.mutateAsync({
+ bookmarkId: item.id,
+ favourited,
+ archived,
+ }),
+ ),
+ );
+ toast({
+ description: `${selectedBookmarks.length} bookmarks have been updated!`,
+ });
+ };
+
+ const deleteBookmarks = async () => {
+ await Promise.all(
+ selectedBookmarks.map((item) =>
+ deleteBookmarkMutator.mutateAsync({ bookmarkId: item.id }),
+ ),
+ );
+ toast({
+ description: `${selectedBookmarks.length} bookmarks have been deleted!`,
+ });
+ };
+
+ const alreadyFavourited =
+ selectedBookmarks.length &&
+ selectedBookmarks.every((item) => item.favourited === true);
+
+ const alreadyArchived =
+ selectedBookmarks.length &&
+ selectedBookmarks.every((item) => item.archived === true);
+
+ const actionList = [
+ {
+ name: alreadyFavourited ? "Unfavourite" : "Favourite",
+ icon: <FavouritedActionIcon favourited={!!alreadyFavourited} size={18} />,
+ action: () => updateBookmarks({ favourited: !alreadyFavourited }),
+ isPending: updateBookmarkMutator.isPending,
+ hidden: !isBulkEditEnabled,
+ },
+ {
+ name: alreadyArchived ? "Un-arhcive" : "Archive",
+ icon: <ArchivedActionIcon size={18} archived={!!alreadyArchived} />,
+ action: () => updateBookmarks({ archived: !alreadyArchived }),
+ isPending: updateBookmarkMutator.isPending,
+ hidden: !isBulkEditEnabled,
+ },
+ {
+ name: "Delete",
+ icon: <Trash2 size={18} color="red" />,
+ action: () => setIsDeleteDialogOpen(true),
+ hidden: !isBulkEditEnabled,
+ },
+ {
+ name: "Close bulk edit",
+ icon: <X size={18} />,
+ action: () => setIsBulkEditEnabled(false),
+ alwaysEnable: true,
+ hidden: !isBulkEditEnabled,
+ },
+ {
+ name: "Bulk Edit",
+ icon: <Pencil size={18} />,
+ action: () => setIsBulkEditEnabled(true),
+ alwaysEnable: true,
+ hidden: isBulkEditEnabled,
+ },
+ ];
+
+ return (
+ <div>
+ <ActionConfirmingDialog
+ open={isDeleteDialogOpen}
+ setOpen={setIsDeleteDialogOpen}
+ title={"Delete Bookmarks"}
+ description={<p>Are you sure you want to delete these bookmarks?</p>}
+ actionButton={() => (
+ <ActionButton
+ type="button"
+ variant="destructive"
+ loading={deleteBookmarkMutator.isPending}
+ onClick={() => deleteBookmarks()}
+ >
+ Delete
+ </ActionButton>
+ )}
+ />
+ <div className="flex">
+ {actionList.map(
+ ({ name, icon: Icon, action, isPending, hidden, alwaysEnable }) => (
+ <ActionButtonWithTooltip
+ className={hidden ? "hidden" : "block"}
+ tooltip={name}
+ disabled={!selectedBookmarks.length && !alwaysEnable}
+ delayDuration={100}
+ loading={!!isPending}
+ variant="ghost"
+ key={name}
+ onClick={action}
+ >
+ {Icon}
+ </ActionButtonWithTooltip>
+ ),
+ )}
+ </div>
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/ChangeLayout.tsx b/apps/web/components/dashboard/ChangeLayout.tsx
index 59acb6bd..7449bd2d 100644
--- a/apps/web/components/dashboard/ChangeLayout.tsx
+++ b/apps/web/components/dashboard/ChangeLayout.tsx
@@ -1,7 +1,7 @@
"use client";
import React from "react";
-import { Button } from "@/components/ui/button";
+import { ButtonWithTooltip } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
@@ -20,15 +20,19 @@ const iconMap = {
list: LayoutList,
};
-export default function SidebarProfileOptions() {
+export default function ChangeLayout() {
const layout = useBookmarkLayout();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
- <Button variant="outline">
+ <ButtonWithTooltip
+ tooltip="Change layout"
+ delayDuration={100}
+ variant="ghost"
+ >
{React.createElement(iconMap[layout], { size: 18 })}
- </Button>
+ </ButtonWithTooltip>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-fit">
{Object.keys(iconMap).map((key) => (
diff --git a/apps/web/components/dashboard/GlobalActions.tsx b/apps/web/components/dashboard/GlobalActions.tsx
new file mode 100644
index 00000000..e09f92a2
--- /dev/null
+++ b/apps/web/components/dashboard/GlobalActions.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import BulkBookmarksAction from "@/components/dashboard/BulkBookmarksAction";
+import ChangeLayout from "@/components/dashboard/ChangeLayout";
+
+export default function GlobalActions() {
+ return (
+ <div className="flex min-w-max flex-wrap overflow-hidden rounded-md border bg-background">
+ <ChangeLayout />
+ <BulkBookmarksAction />
+ </div>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
index 6cc8e44e..299f47eb 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
@@ -22,7 +22,7 @@ export default function BookmarkActionBar({
href={`/dashboard/preview/${bookmark.id}`}
className={cn(buttonVariants({ variant: "ghost" }), "px-2")}
>
- <Maximize2 size="20" />
+ <Maximize2 size={16} />
</Link>
<BookmarkOptions bookmark={bookmark} />
</div>
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
index e1cc1f7c..33b65108 100644
--- a/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
+++ b/apps/web/components/dashboard/bookmarks/BookmarkLayoutAdaptingCard.tsx
@@ -1,12 +1,15 @@
import type { BookmarksLayoutTypes } from "@/lib/userLocalSettings/types";
-import React from "react";
+import React, { useEffect, useState } from "react";
import Link from "next/link";
+import useBulkActionsStore from "@/lib/bulkActions";
import {
bookmarkLayoutSwitch,
useBookmarkLayout,
} from "@/lib/userLocalSettings/bookmarksLayout";
import { cn } from "@/lib/utils";
import dayjs from "dayjs";
+import { Check } from "lucide-react";
+import { useTheme } from "next-themes";
import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
import { isBookmarkStillTagging } from "@hoarder/shared-react/utils/bookmarkUtils";
@@ -45,6 +48,57 @@ function BottomRow({
);
}
+function MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) {
+ const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore();
+ const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark);
+ const [isSelected, setIsSelected] = useState(false);
+ const { theme } = useTheme();
+
+ useEffect(() => {
+ setIsSelected(selectedBookmarks.some((item) => item.id === bookmark.id));
+ }, [selectedBookmarks]);
+
+ if (!isBulkEditEnabled) return null;
+
+ const getIconColor = () => {
+ if (theme === "dark") {
+ return isSelected ? "black" : "white";
+ }
+ return isSelected ? "white" : "black";
+ };
+
+ const getIconBackgroundColor = () => {
+ if (theme === "dark") {
+ return isSelected ? "bg-white" : "bg-white bg-opacity-10";
+ }
+ return isSelected ? "bg-black" : "bg-white bg-opacity-40";
+ };
+
+ return (
+ <button
+ className={cn(
+ "absolute left-0 top-0 z-50 h-full w-full bg-opacity-0",
+ {
+ "bg-opacity-10": isSelected,
+ },
+ theme === "dark" ? "bg-white" : "bg-black",
+ )}
+ onClick={() => toggleBookmark(bookmark)}
+ >
+ <button className="absolute right-2 top-2 z-50 opacity-100">
+ <div
+ className={cn(
+ "flex h-4 w-4 items-center justify-center rounded-full border border-gray-600",
+ getIconBackgroundColor(),
+ )}
+ >
+ <Check size={12} color={getIconColor()} />
+ </div>
+ </button>
+ </button>
+ );
+}
+
function ListView({
bookmark,
image,
@@ -56,10 +110,11 @@ function ListView({
return (
<div
className={cn(
- "flex max-h-96 gap-4 overflow-hidden rounded-lg p-2 shadow-md",
+ "relative flex max-h-96 gap-4 overflow-hidden rounded-lg p-2 shadow-md",
className,
)}
>
+ <MultiBookmarkSelector bookmark={bookmark} />
<div className="flex size-32 items-center justify-center overflow-hidden">
{image("list", "object-cover rounded-lg size-32")}
</div>
@@ -100,11 +155,12 @@ function GridView({
return (
<div
className={cn(
- "flex flex-col overflow-hidden rounded-lg shadow-md",
+ "relative flex flex-col overflow-hidden rounded-lg shadow-md",
className,
fitHeight && layout != "grid" ? "max-h-96" : "h-96",
)}
>
+ <MultiBookmarkSelector bookmark={bookmark} />
{img && <div className="h-56 w-full shrink-0 overflow-hidden">{img}</div>}
<div className="flex h-full flex-col justify-between gap-2 overflow-hidden p-2">
<div className="grow-1 flex flex-col gap-2 overflow-hidden">
diff --git a/apps/web/components/dashboard/bookmarks/icons.tsx b/apps/web/components/dashboard/bookmarks/icons.tsx
index d899f19d..04e3ff32 100644
--- a/apps/web/components/dashboard/bookmarks/icons.tsx
+++ b/apps/web/components/dashboard/bookmarks/icons.tsx
@@ -3,27 +3,31 @@ import { Archive, ArchiveRestore, Star } from "lucide-react";
export function FavouritedActionIcon({
favourited,
className,
+ size,
}: {
favourited: boolean;
className?: string;
+ size?: number;
}) {
return favourited ? (
- <Star className={className} color="#ebb434" fill="#ebb434" />
+ <Star size={size} className={className} color="#ebb434" fill="#ebb434" />
) : (
- <Star className={className} />
+ <Star size={size} className={className} />
);
}
export function ArchivedActionIcon({
archived,
className,
+ size,
}: {
archived: boolean;
className?: string;
+ size?: number;
}) {
return archived ? (
- <ArchiveRestore className={className} />
+ <ArchiveRestore size={size} className={className} />
) : (
- <Archive className={className} />
+ <Archive size={size} className={className} />
);
}
diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx
index 2f69203e..1655a80b 100644
--- a/apps/web/components/dashboard/lists/ListHeader.tsx
+++ b/apps/web/components/dashboard/lists/ListHeader.tsx
@@ -1,7 +1,7 @@
"use client";
import { useRouter } from "next/navigation";
-import ChangeLayout from "@/components/dashboard/ChangeLayout";
+import GlobalActions from "@/components/dashboard/GlobalActions";
import { Button } from "@/components/ui/button";
import { MoreHorizontal } from "lucide-react";
@@ -37,13 +37,13 @@ export default function ListHeader({
<span className="text-2xl">
{list.icon} {list.name}
</span>
- <div>
+ <div className="flex">
<ListOptions list={list}>
<Button variant="ghost">
<MoreHorizontal />
</Button>
</ListOptions>
- <ChangeLayout />
+ <GlobalActions />
</div>
</div>
);