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

UI: Move some bookmark actions to be on bookmark hover #743

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ data

# Idea
.idea
*.iml
*.iml
.aider*
7 changes: 0 additions & 7 deletions apps/web/components/dashboard/BulkBookmarksAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,13 +257,6 @@ export default function BulkBookmarksAction() {
alwaysEnable: true,
hidden: !isBulkEditEnabled,
},
{
name: t("actions.bulk_edit"),
icon: <Pencil size={18} />,
action: () => setIsBulkEditEnabled(true),
alwaysEnable: true,
hidden: isBulkEditEnabled,
},
];

return (
Expand Down
3 changes: 0 additions & 3 deletions apps/web/components/dashboard/bookmarks/BookmarkActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,6 @@ export default function BookmarkActionBar({
}) {
return (
<div className="flex text-gray-500">
{bookmark.favourited && (
<FavouritedActionIcon className="m-1 size-8 rounded p-1" favourited />
)}
<Link
href={`/dashboard/preview/${bookmark.id}`}
className={cn(buttonVariants({ variant: "ghost" }), "px-2")}
Expand Down
77 changes: 77 additions & 0 deletions apps/web/components/dashboard/bookmarks/BookmarkHoverActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { cn } from "@/lib/utils";
import { Check } from "lucide-react";
import { useTheme } from "next-themes";

import type { ZBookmark } from "@hoarder/shared/types/bookmarks";

import ArchiveBookmarkButton from "./action-buttons/ArchiveBookmarkButton";
import FavouriteBookmarkButton from "./action-buttons/FavouriteBookmarkButton";
import { ArchivedActionIcon, FavouritedActionIcon } from "./icons";

interface BookmarkHoverActionsProps {
bookmark: ZBookmark;
isSelected: boolean;
onSelectClick: (e: React.MouseEvent) => void;
}

function SelectCheckbox({ isSelected }: { isSelected: boolean }) {
return (
<div className="z-50 opacity-100">
<div
className={cn(
"flex size-4 items-center justify-center rounded-full border border-foreground",
isSelected ? "bg-foreground" : undefined,
)}
>
<Check size={12} className={isSelected ? "text-background" : undefined} />
</div>
</div>
);
}

export function BookmarkHoverActions({
bookmark,
isSelected,
onSelectClick,
}: BookmarkHoverActionsProps) {
const { theme } = useTheme();

return (
<div
className={cn(
"absolute right-2 top-2 z-50 flex items-center gap-2 rounded-lg px-2 py-1 group-hover:visible",
isSelected ? "visible" : "invisible",
theme === "dark"
? "bg-black/50 backdrop-blur-sm"
: "bg-white/50 backdrop-blur-sm",
)}
>
<FavouriteBookmarkButton
variant="ghost"
bookmarkId={bookmark.id}
className="h-8 w-8 p-0"
>
<FavouritedActionIcon
className="size-4"
favourited={bookmark.favourited}
/>
</FavouriteBookmarkButton>
<ArchiveBookmarkButton
variant="ghost"
bookmarkId={bookmark.id}
className="h-8 w-8 p-0"
>
<ArchivedActionIcon className="size-4" archived={bookmark.archived} />
</ArchiveBookmarkButton>
<button
className={cn(
"flex h-8 w-8 items-center justify-center rounded-lg hover:bg-accent",
isSelected && "bg-accent",
)}
onClick={onSelectClick}
>
<SelectCheckbox isSelected={isSelected} />
</button>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import {
bookmarkLayoutSwitch,
useBookmarkLayout,
} from "@/lib/userLocalSettings/bookmarksLayout";
import { ArchivedActionIcon, FavouritedActionIcon } from "./icons";
import { cn } from "@/lib/utils";
import dayjs from "dayjs";
import { Check, Image as ImageIcon, NotebookPen } from "lucide-react";
import { Image as ImageIcon, NotebookPen } from "lucide-react";
import { BookmarkHoverActions } from "./BookmarkHoverActions";
import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks";
import { useToast } from "@/components/ui/use-toast";
import { useTheme } from "next-themes";

import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
Expand Down Expand Up @@ -54,53 +58,41 @@ function BottomRow({
}

function MultiBookmarkSelector({ bookmark }: { bookmark: ZBookmark }) {
const { selectedBookmarks, isBulkEditEnabled } = useBulkActionsStore();
const { isBulkEditEnabled } = useBulkActionsStore();
const toggleBookmark = useBulkActionsStore((state) => state.toggleBookmark);
const [isSelected, setIsSelected] = useState(false);
const { selectedBookmarks } = useBulkActionsStore();
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";
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
toggleBookmark(bookmark);
};

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)}
>
<div className="absolute right-2 top-2 z-50 opacity-100">
<div
<>
{isBulkEditEnabled && (
<button
className={cn(
"flex h-4 w-4 items-center justify-center rounded-full border border-gray-600",
getIconBackgroundColor(),
"absolute left-0 top-0 z-40 h-full w-full bg-opacity-0",
{
"bg-opacity-10": isSelected,
},
theme === "dark" ? "bg-white" : "bg-black",
)}
>
<Check size={12} color={getIconColor()} />
</div>
</div>
</button>
onClick={handleClick}
/>
)}
<BookmarkHoverActions
bookmark={bookmark}
isSelected={isSelected}
onSelectClick={handleClick}
/>
</>
);
}

Expand All @@ -115,7 +107,7 @@ function ListView({
return (
<div
className={cn(
"relative flex max-h-96 gap-4 overflow-hidden rounded-lg p-2 shadow-md",
"group relative flex max-h-96 gap-4 overflow-hidden rounded-lg p-2 shadow-md",
className,
)}
>
Expand Down Expand Up @@ -160,7 +152,7 @@ function GridView({
return (
<div
className={cn(
"relative flex flex-col overflow-hidden rounded-lg shadow-md",
"group relative flex flex-col overflow-hidden rounded-lg shadow-md",
className,
fitHeight && layout != "grid" ? "max-h-96" : "h-96",
)}
Expand Down Expand Up @@ -193,7 +185,7 @@ function CompactView({ bookmark, title, footer, className }: Props) {
return (
<div
className={cn(
"relative flex flex-col overflow-hidden rounded-lg shadow-md",
"group relative flex flex-col overflow-hidden rounded-lg shadow-md",
className,
"max-h-96",
)}
Expand Down
38 changes: 0 additions & 38 deletions apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,44 +133,6 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) {
<span>Edit</span>
</DropdownMenuItem>
)}
<DropdownMenuItem
disabled={demoMode}
onClick={() =>
updateBookmarkMutator.mutate({
bookmarkId: linkId,
favourited: !bookmark.favourited,
})
}
>
<FavouritedActionIcon
className="mr-2 size-4"
favourited={bookmark.favourited}
/>
<span>
{bookmark.favourited
? t("actions.unfavorite")
: t("actions.favorite")}
</span>
</DropdownMenuItem>
<DropdownMenuItem
disabled={demoMode}
onClick={() =>
updateBookmarkMutator.mutate({
bookmarkId: linkId,
archived: !bookmark.archived,
})
}
>
<ArchivedActionIcon
className="mr-2 size-4"
archived={bookmark.archived}
/>
<span>
{bookmark.archived
? t("actions.unarchive")
: t("actions.archive")}
</span>
</DropdownMenuItem>

{bookmark.content.type === BookmarkTypes.LINK && (
<DropdownMenuItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ const ArchiveBookmarkButton = React.forwardRef<
<ActionButton
ref={ref}
loading={isArchivingBookmark}
disabled={data.archived}
onClick={() =>
updateBookmark({
bookmarkId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React from "react";
import { ActionButton, ActionButtonProps } from "@/components/ui/action-button";
import { toast } from "@/components/ui/use-toast";
import { api } from "@/lib/trpc";
import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks";

interface FavouriteBookmarkButtonProps
extends Omit<ActionButtonProps, "loading" | "disabled"> {
bookmarkId: string;
onDone?: () => void;
}

const FavouriteBookmarkButton = React.forwardRef<
HTMLButtonElement,
FavouriteBookmarkButtonProps
>(({ bookmarkId, onDone, ...props }, ref) => {
const { data } = api.bookmarks.getBookmark.useQuery({ bookmarkId });

const { mutate: updateBookmark, isPending: isFavouritingBookmark } =
useUpdateBookmark({
onSuccess: () => {
toast({
description: "Bookmark has been updated!",
});
onDone?.();
},
onError: (e) => {
if (e.data?.code == "BAD_REQUEST") {
toast({
variant: "destructive",
description: e.message,
});
} else {
toast({
variant: "destructive",
title: "Something went wrong",
});
}
},
});

if (!data) {
return <span />;
}

return (
<ActionButton
ref={ref}
loading={isFavouritingBookmark}
onClick={() =>
updateBookmark({
bookmarkId,
favourited: !data.favourited,
})
}
{...props}
/>
);
});

FavouriteBookmarkButton.displayName = "FavouriteBookmarkButton";
export default FavouriteBookmarkButton;
16 changes: 12 additions & 4 deletions apps/web/lib/bulkActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,22 @@ const useBulkActionsStore = create<BookmarkState>((set, get) => ({
const isBookmarkAlreadySelected = selectedBookmarks.some(
(b) => b.id === bookmark.id,
);

if (isBookmarkAlreadySelected) {
const newSelectedBookmarks = selectedBookmarks.filter(
(b) => b.id !== bookmark.id,
);
// If this was the last selected bookmark, disable bulk edit mode
set({
selectedBookmarks: selectedBookmarks.filter(
(b) => b.id !== bookmark.id,
),
selectedBookmarks: newSelectedBookmarks,
isBulkEditEnabled: newSelectedBookmarks.length > 0
});
} else {
set({ selectedBookmarks: [...selectedBookmarks, bookmark] });
// If this is the first selected bookmark, enable bulk edit mode
set({
selectedBookmarks: [...selectedBookmarks, bookmark],
isBulkEditEnabled: true
});
}
},

Expand Down
Loading