From 3207264fc13c275d6dcfbd2628cc6b3974ceeaed Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Mon, 7 Apr 2025 01:03:26 +0100 Subject: feat: Allow editing bookmark details --- .../dashboard/bookmarks/BookmarkOptions.tsx | 25 +- .../dashboard/bookmarks/EditBookmarkDialog.tsx | 371 +++++++++++++++++++++ .../components/dashboard/bookmarks/LinkCard.tsx | 3 +- .../web/components/dashboard/preview/ActionBar.tsx | 25 +- .../dashboard/preview/BookmarkPreview.tsx | 10 +- .../components/dashboard/preview/EditableTitle.tsx | 60 ---- apps/web/components/ui/calendar.tsx | 69 ++++ apps/web/lib/i18n/locales/en/translation.json | 14 + apps/web/package.json | 2 + 9 files changed, 504 insertions(+), 75 deletions(-) create mode 100644 apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx delete mode 100644 apps/web/components/dashboard/preview/EditableTitle.tsx create mode 100644 apps/web/components/ui/calendar.tsx (limited to 'apps/web') diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index c37c6417..039904a0 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -19,7 +19,7 @@ import { MoreHorizontal, Pencil, RotateCw, - Tags, + SquarePen, Trash2, } from "lucide-react"; @@ -38,9 +38,9 @@ import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; import DeleteBookmarkConfirmationDialog from "./DeleteBookmarkConfirmationDialog"; +import { EditBookmarkDialog } from "./EditBookmarkDialog"; import { ArchivedActionIcon, FavouritedActionIcon } from "./icons"; import { useManageListsModal } from "./ManageListsModal"; -import { useTagModel } from "./TagModal"; export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const { t } = useTranslation(); @@ -49,14 +49,13 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const demoMode = !!useClientConfig().demoMode; - const { setOpen: setTagModalIsOpen, content: tagModal } = - useTagModel(bookmark); const { setOpen: setManageListsModalOpen, content: manageListsModal } = useManageListsModal(bookmark.id); const [deleteBookmarkDialogOpen, setDeleteBookmarkDialogOpen] = useState(false); const [isTextEditorOpen, setTextEditorOpen] = useState(false); + const [isEditBookmarkDialogOpen, setEditBookmarkDialogOpen] = useState(false); const { listId } = useBookmarkGridContext() ?? {}; const withinListContext = useBookmarkListContext(); @@ -106,8 +105,12 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { return ( <> - {tagModal} {manageListsModal} + + setEditBookmarkDialogOpen(true)}> + + {t("actions.edit")} + {bookmark.content.type === BookmarkTypes.TEXT && ( setTextEditorOpen(true)}> - - Edit + + {t("actions.open_editor")} )} {t("actions.copy_link")} )} - setTagModalIsOpen(true)}> - - {t("actions.edit_tags")} - setManageListsModalOpen(true)}> diff --git a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx new file mode 100644 index 00000000..2d47102b --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx @@ -0,0 +1,371 @@ +import * as React from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslation } from "@/lib/i18n/client"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import { CalendarIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; + +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { + BookmarkTypes, + ZBookmark, + ZUpdateBookmarksRequest, + zUpdateBookmarksRequestSchema, +} from "@hoarder/shared/types/bookmarks"; + +import { BookmarkTagsEditor } from "./BookmarkTagsEditor"; + +const formSchema = zUpdateBookmarksRequestSchema; + +export function EditBookmarkDialog({ + open, + setOpen, + bookmark, + children, +}: { + bookmark: ZBookmark; + children?: React.ReactNode; + open: boolean; + setOpen: (v: boolean) => void; +}) { + const { t } = useTranslation(); + const bookmarkToDefault = (bookmark: ZBookmark) => ({ + bookmarkId: bookmark.id, + summary: bookmark.summary, + title: bookmark.title + ? bookmark.title + : bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.title + : undefined, + createdAt: bookmark.createdAt ?? new Date(), + // Link specific defaults (only if bookmark is a link) + url: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.url + : undefined, + description: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.description ?? "") + : undefined, + author: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.author ?? "") + : undefined, + publisher: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.publisher ?? "") + : undefined, + datePublished: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.datePublished + : undefined, + }); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: bookmarkToDefault(bookmark), + }); + + const { mutate: updateBookmarkMutate, isPending: isUpdatingBookmark } = + useUpdateBookmark({ + onSuccess: (updatedBookmark) => { + toast({ description: "Bookmark details updated successfully!" }); + // Close the dialog after successful detail update + setOpen(false); + // Reset form with potentially updated data + form.reset(bookmarkToDefault(updatedBookmark)); + }, + onError: (error) => { + toast({ + variant: "destructive", + title: "Failed to update bookmark", + description: error.message, + }); + }, + }); + + function onSubmit(values: ZUpdateBookmarksRequest) { + // Ensure optional fields that are empty strings are sent as null/undefined if appropriate + const payload = { + ...values, + title: values.title ?? null, + }; + updateBookmarkMutate(payload); + } + + // Reset form when bookmark data changes externally or dialog reopens + React.useEffect(() => { + if (open) { + form.reset(bookmarkToDefault(bookmark)); + } + }, [bookmark, form, open]); + + const isLink = bookmark.content.type === BookmarkTypes.LINK; + + return ( + + {children && {children}} + + + {t("bookmark_editor.title")} + {t("bookmark_editor.subtitle")} + +
+ + ( + + {t("common.title")} + + + + + + )} + /> + + {isLink && ( + ( + + {t("common.url")} + + + + + + )} + /> + )} + + {isLink && ( + ( + + {t("common.description")} + +