diff options
| author | MohamedBassem <me@mbassem.com> | 2025-04-07 01:03:26 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2025-04-08 03:48:12 -0700 |
| commit | 3207264fc13c275d6dcfbd2628cc6b3974ceeaed (patch) | |
| tree | d426ffe0fe6bc3b9e692d96af94aa8d5d2a51162 /apps | |
| parent | 817eb58832a3e715e21892417b7624f4b1cf0d46 (diff) | |
| download | karakeep-3207264fc13c275d6dcfbd2628cc6b3974ceeaed.tar.zst | |
feat: Allow editing bookmark details
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx | 25 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx | 371 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/LinkCard.tsx | 3 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/ActionBar.tsx | 25 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/BookmarkPreview.tsx | 10 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/EditableTitle.tsx | 60 | ||||
| -rw-r--r-- | apps/web/components/ui/calendar.tsx | 69 | ||||
| -rw-r--r-- | apps/web/lib/i18n/locales/en/translation.json | 14 | ||||
| -rw-r--r-- | apps/web/package.json | 2 |
9 files changed, 504 insertions, 75 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> ); } diff --git a/apps/web/components/dashboard/preview/ActionBar.tsx b/apps/web/components/dashboard/preview/ActionBar.tsx index 86c86d5a..62d9c849 100644 --- a/apps/web/components/dashboard/preview/ActionBar.tsx +++ b/apps/web/components/dashboard/preview/ActionBar.tsx @@ -8,12 +8,13 @@ import { } from "@/components/ui/tooltip"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; -import { Trash2 } from "lucide-react"; +import { Pencil, Trash2 } from "lucide-react"; import type { ZBookmark } from "@hoarder/shared/types/bookmarks"; import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; import DeleteBookmarkConfirmationDialog from "../bookmarks/DeleteBookmarkConfirmationDialog"; +import { EditBookmarkDialog } from "../bookmarks/EditBookmarkDialog"; import { ArchivedActionIcon, FavouritedActionIcon } from "../bookmarks/icons"; export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { @@ -21,6 +22,8 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { const [deleteBookmarkDialogOpen, setDeleteBookmarkDialogOpen] = useState(false); + const [isEditBookmarkDialogOpen, setEditBookmarkDialogOpen] = useState(false); + const onError = () => { toast({ variant: "destructive", @@ -49,6 +52,26 @@ export default function ActionBar({ bookmark }: { bookmark: ZBookmark }) { return ( <div className="flex items-center justify-center gap-3"> <Tooltip delayDuration={0}> + <EditBookmarkDialog + bookmark={bookmark} + open={isEditBookmarkDialogOpen} + setOpen={setEditBookmarkDialogOpen} + /> + + <TooltipTrigger asChild> + <Button + variant="none" + className="size-14 rounded-full bg-background" + onClick={() => { + setEditBookmarkDialogOpen(true); + }} + > + <Pencil /> + </Button> + </TooltipTrigger> + <TooltipContent side="bottom">{t("actions.edit")}</TooltipContent> + </Tooltip> + <Tooltip delayDuration={0}> <TooltipTrigger asChild> <ActionButton variant="none" diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index c78eab22..07ae0809 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -1,5 +1,6 @@ "use client"; +import React from "react"; import Link from "next/link"; import { BookmarkTagsEditor } from "@/components/dashboard/bookmarks/BookmarkTagsEditor"; import { FullPageSpinner } from "@/components/ui/full-page-spinner"; @@ -17,6 +18,7 @@ import { api } from "@/lib/trpc"; import { CalendarDays, ExternalLink } from "lucide-react"; import { + getBookmarkTitle, getSourceUrl, isBookmarkStillCrawling, isBookmarkStillLoading, @@ -27,7 +29,6 @@ import SummarizeBookmarkArea from "../bookmarks/SummarizeBookmarkArea"; import ActionBar from "./ActionBar"; import { AssetContentSection } from "./AssetContentSection"; import AttachmentBox from "./AttachmentBox"; -import { EditableTitle } from "./EditableTitle"; import HighlightsBox from "./HighlightsBox"; import LinkContentSection from "./LinkContentSection"; import { NoteEditor } from "./NoteEditor"; @@ -108,6 +109,7 @@ export default function BookmarkPreview({ } const sourceUrl = getSourceUrl(bookmark); + const title = getBookmarkTitle(bookmark); return ( <div className="grid h-full grid-rows-3 gap-2 overflow-hidden bg-background lg:grid-cols-3 lg:grid-rows-none"> @@ -116,7 +118,11 @@ export default function BookmarkPreview({ </div> <div className="row-span-1 flex flex-col gap-4 overflow-auto bg-accent p-4 md:col-span-2 lg:col-span-1 lg:row-auto"> <div className="flex w-full flex-col items-center justify-center gap-y-2"> - <EditableTitle bookmark={bookmark} /> + <div className="flex w-full items-center justify-center gap-2"> + <p className="line-clamp-2 text-ellipsis break-words text-lg"> + {title === undefined || title === "" ? "Untitled" : title} + </p> + </div> {sourceUrl && ( <Link href={sourceUrl} diff --git a/apps/web/components/dashboard/preview/EditableTitle.tsx b/apps/web/components/dashboard/preview/EditableTitle.tsx deleted file mode 100644 index 03b95e74..00000000 --- a/apps/web/components/dashboard/preview/EditableTitle.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { toast } from "@/components/ui/use-toast"; - -import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; -import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; - -import { EditableText } from "../EditableText"; - -export function EditableTitle({ bookmark }: { bookmark: ZBookmark }) { - const { mutate: updateBookmark, isPending } = useUpdateBookmark({ - onSuccess: () => { - toast({ - description: "Title updated!", - }); - }, - }); - - let title: string | null = null; - switch (bookmark.content.type) { - case BookmarkTypes.LINK: - title = bookmark.content.title ?? bookmark.content.url; - break; - case BookmarkTypes.TEXT: - title = null; - break; - case BookmarkTypes.ASSET: - title = bookmark.content.fileName ?? null; - break; - } - - title = bookmark.title ?? title; - if (title == "") { - title = null; - } - - return ( - <EditableText - originalText={title} - editClassName="p-2 text-lg break-all" - viewClassName="break-words line-clamp-2 text-lg text-ellipsis" - untitledClassName="text-lg italic text-gray-600" - onSave={(newTitle) => { - updateBookmark( - { - bookmarkId: bookmark.id, - title: newTitle, - }, - { - onError: () => { - toast({ - description: "Something went wrong", - variant: "destructive", - }); - }, - }, - ); - }} - isSaving={isPending} - /> - ); -} diff --git a/apps/web/components/ui/calendar.tsx b/apps/web/components/ui/calendar.tsx new file mode 100644 index 00000000..99a082f6 --- /dev/null +++ b/apps/web/components/ui/calendar.tsx @@ -0,0 +1,69 @@ +"use client"; + +import * as React from "react"; +import { buttonVariants } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { DayPicker } from "react-day-picker"; + +export type CalendarProps = React.ComponentProps<typeof DayPicker>; + +function Calendar({ + className, + classNames, + showOutsideDays = true, + ...props +}: CalendarProps) { + return ( + <DayPicker + showOutsideDays={showOutsideDays} + className={cn("p-3", className)} + classNames={{ + months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0", + month: "space-y-4", + caption: "flex justify-center pt-1 relative items-center", + caption_label: "text-sm font-medium", + nav: "space-x-1 flex items-center", + nav_button: cn( + buttonVariants({ variant: "outline" }), + "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100", + ), + nav_button_previous: "absolute left-1", + nav_button_next: "absolute right-1", + table: "w-full border-collapse space-y-1", + head_row: "flex", + head_cell: + "text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]", + row: "flex w-full mt-2", + cell: "h-9 w-9 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20", + day: cn( + buttonVariants({ variant: "ghost" }), + "h-9 w-9 p-0 font-normal aria-selected:opacity-100", + ), + day_range_end: "day-range-end", + day_selected: + "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground", + day_today: "bg-accent text-accent-foreground", + day_outside: + "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground", + day_disabled: "text-muted-foreground opacity-50", + day_range_middle: + "aria-selected:bg-accent aria-selected:text-accent-foreground", + day_hidden: "invisible", + ...classNames, + }} + components={{ + IconLeft: ({ className, ...props }) => ( + <ChevronLeft className={cn("h-4 w-4", className)} {...props} /> + ), + IconRight: ({ className, ...props }) => ( + <ChevronRight className={cn("h-4 w-4", className)} {...props} /> + ), + }} + {...props} + /> + ); +} +Calendar.displayName = "Calendar"; + +export { Calendar }; diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json index d03ddfe7..536bea57 100644 --- a/apps/web/lib/i18n/locales/en/translation.json +++ b/apps/web/lib/i18n/locales/en/translation.json @@ -7,6 +7,7 @@ "action": "Action", "actions": "Actions", "created_at": "Created At", + "updated_at": "Updated At", "key": "Key", "role": "Role", "type": "Type", @@ -27,6 +28,9 @@ "video": "Video", "archive": "Archive", "home": "Home", + "title": "Title", + "description": "Description", + "summary": "Summary", "bookmark_types": { "title": "Bookmark Type", "link": "Link", @@ -62,6 +66,7 @@ "save": "Save", "add": "Add", "edit": "Edit", + "open_editor": "Open Editor", "create": "Create", "fetch_now": "Fetch Now", "summarize_with_ai": "Summarize with AI", @@ -360,5 +365,14 @@ "title": "Duplicate Tags", "merge_all_suggestions": "Merge all suggestions?" } + }, + "bookmark_editor": { + "title": "Edit Bookmark", + "subtitle": "Make changes to the bookmark details. Click save when you're done.", + "author": "Author", + "publisher": "Publisher", + "date_published": "Date Published", + "pick_a_date": "Pick a date", + "save_changes": "Save changes" } } diff --git a/apps/web/package.json b/apps/web/package.json index 01906545..7a54fa2f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -57,6 +57,7 @@ "clsx": "^2.1.0", "cmdk": "^1.0.0", "csv-parse": "^5.5.6", + "date-fns": "^4.1.0", "dayjs": "^1.11.10", "drizzle-orm": "^0.38.3", "fastest-levenshtein": "^1.0.16", @@ -71,6 +72,7 @@ "next-themes": "^0.3.0", "prettier": "^3.4.2", "react": "^18.3.1", + "react-day-picker": "8.10.1", "react-dom": "^18.3.1", "react-draggable": "^4.4.6", "react-dropzone": "^14.2.3", |
