Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] Add bulk edit option for bookmarks #259

Merged
merged 6 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/app/dashboard/archive/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import ChangeLayout from "@/components/dashboard/ChangeLayout";
import GlobalActions from "@/components/dashboard/GlobalActions";
import InfoTooltip from "@/components/ui/info-tooltip";

function header() {
Expand All @@ -12,7 +12,7 @@ function header() {
</InfoTooltip>
</div>
<div>
<ChangeLayout />
<GlobalActions />
</div>
</div>
);
Expand Down
6 changes: 3 additions & 3 deletions apps/web/app/dashboard/bookmarks/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import React from "react";
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import ChangeLayout from "@/components/dashboard/ChangeLayout";
import GlobalActions from "@/components/dashboard/GlobalActions";
import { SearchInput } from "@/components/dashboard/search/SearchInput";

export default async function BookmarksPage() {
return (
<div>
<div className="flex gap-2">
<SearchInput />
<ChangeLayout />
<GlobalActions />
</div>
<div className="my-4 flex-1">
<div className="my-4">
<Bookmarks query={{ archived: false }} showEditorCard={true} />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/dashboard/favourites/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks";
import ChangeLayout from "@/components/dashboard/ChangeLayout";
import GlobalActions from "@/components/dashboard/GlobalActions";

export default async function FavouritesBookmarkPage() {
return (
<Bookmarks
header={
<div className="flex items-center justify-between">
<p className="text-2xl">⭐️ Favourites</p>
<ChangeLayout />
<GlobalActions />
</div>
}
query={{ favourited: true }}
Expand Down
4 changes: 2 additions & 2 deletions apps/web/app/dashboard/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Suspense, useRef } from "react";
import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid";
import ChangeLayout from "@/components/dashboard/ChangeLayout";
import GlobalActions from "@/components/dashboard/GlobalActions";
import { SearchInput } from "@/components/dashboard/search/SearchInput";
import { FullPageSpinner } from "@/components/ui/full-page-spinner";
import { useBookmarkSearch } from "@/lib/hooks/bookmark-search";
Expand All @@ -17,7 +17,7 @@ function SearchComp() {
<div className="flex flex-col gap-3">
<div className="flex gap-2">
<SearchInput ref={inputRef} autoFocus={true} />
<ChangeLayout />
<GlobalActions />
</div>
{data ? (
<BookmarksGrid bookmarks={data.bookmarks} />
Expand Down
171 changes: 171 additions & 0 deletions apps/web/components/dashboard/BulkBookmarksAction.tsx
Original file line number Diff line number Diff line change
@@ -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",
mdsaban marked this conversation as resolved.
Show resolved Hide resolved
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>
);
}
12 changes: 8 additions & 4 deletions apps/web/components/dashboard/ChangeLayout.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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) => (
Expand Down
13 changes: 13 additions & 0 deletions apps/web/components/dashboard/GlobalActions.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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>
Expand Down Expand Up @@ -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">
Expand Down
Loading
Loading