diff options
Diffstat (limited to 'apps/web/components/dashboard/bookmarks')
3 files changed, 387 insertions, 12 deletions
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} + <EditBookmarkDialog + bookmark={bookmark} + open={isEditBookmarkDialogOpen} + setOpen={setEditBookmarkDialogOpen} + /> <DeleteBookmarkConfirmationDialog bookmark={bookmark} open={deleteBookmarkDialogOpen} @@ -128,10 +131,14 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-fit"> + <DropdownMenuItem onClick={() => setEditBookmarkDialogOpen(true)}> + <Pencil className="mr-2 size-4" /> + <span>{t("actions.edit")}</span> + </DropdownMenuItem> {bookmark.content.type === BookmarkTypes.TEXT && ( <DropdownMenuItem onClick={() => setTextEditorOpen(true)}> - <Pencil className="mr-2 size-4" /> - <span>Edit</span> + <SquarePen className="mr-2 size-4" /> + <span>{t("actions.open_editor")}</span> </DropdownMenuItem> )} <DropdownMenuItem @@ -202,10 +209,6 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { <span>{t("actions.copy_link")}</span> </DropdownMenuItem> )} - <DropdownMenuItem onClick={() => setTagModalIsOpen(true)}> - <Tags className="mr-2 size-4" /> - <span>{t("actions.edit_tags")}</span> - </DropdownMenuItem> <DropdownMenuItem onClick={() => setManageListsModalOpen(true)}> <List className="mr-2 size-4" /> 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<ZUpdateBookmarksRequest>({ + 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 ( + <Dialog open={open} onOpenChange={setOpen}> + {children && <DialogTrigger asChild>{children}</DialogTrigger>} + <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl"> + <DialogHeader> + <DialogTitle>{t("bookmark_editor.title")}</DialogTitle> + <DialogDescription>{t("bookmark_editor.subtitle")}</DialogDescription> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.title")}</FormLabel> + <FormControl> + <Input + placeholder="Bookmark title" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {isLink && ( + <FormField + control={form.control} + name="url" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.url")}</FormLabel> + <FormControl> + <Input placeholder="https://example.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.description")}</FormLabel> + <FormControl> + <Textarea + placeholder="Bookmark description" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <FormField + control={form.control} + name="summary" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.summary")}</FormLabel> + <FormControl> + <Textarea + placeholder="Bookmark summary" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + <FormField + control={form.control} + name="author" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("bookmark_editor.author")}</FormLabel> + <FormControl> + <Input + placeholder="Author name" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="publisher" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("bookmark_editor.publisher")}</FormLabel> + <FormControl> + <Input + placeholder="Publisher name" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + )} + + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + <FormField + control={form.control} + name="createdAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>{t("common.created_at")}</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={"outline"} + className={cn( + "pl-3 text-left font-normal", + !field.value && "text-muted-foreground", + )} + > + {field.value ? ( + format(field.value, "PPP") + ) : ( + <span>{t("bookmark_editor.pick_a_date")}</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => + date > new Date() || date < new Date("1900-01-01") + } + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {isLink && ( + <FormField + control={form.control} + name="datePublished" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel> + {t("bookmark_editor.date_published")} + </FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={"outline"} + className={cn( + "pl-3 text-left font-normal", + !field.value && "text-muted-foreground", + )} + > + {field.value ? ( + format(field.value, "PPP") + ) : ( + <span>{t("bookmark_editor.pick_a_date")}</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value ?? undefined} // Calendar expects Date | undefined + onSelect={(date) => field.onChange(date ?? null)} // Handle undefined -> null + disabled={(date) => + date > new Date() || date < new Date("1900-01-01") + } + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + <FormItem> + <FormLabel>{t("common.tags")}</FormLabel> + <FormControl> + <BookmarkTagsEditor bookmark={bookmark} /> + </FormControl> + <FormMessage /> + </FormItem> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isUpdatingBookmark} + > + {t("actions.cancel")} + </Button> + <ActionButton type="submit" loading={isUpdatingBookmark}> + {t("bookmark_editor.save_changes")} + </ActionButton> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx index 86eed9e7..34044305 100644 --- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import type { ZBookmarkTypeLink } from "@hoarder/shared/types/bookmarks"; import { getBookmarkLinkImageUrl, + getBookmarkTitle, getSourceUrl, isBookmarkStillCrawling, } from "@hoarder/shared-react/utils/bookmarkUtils"; @@ -18,7 +19,7 @@ function LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) { const parsedUrl = new URL(link.url); return ( <Link href={link.url} target="_blank" rel="noreferrer"> - {bookmark.title ?? link?.title ?? parsedUrl.host} + {getBookmarkTitle(bookmark) ?? parsedUrl.host} </Link> ); } |
