diff options
| author | MohamedBassem <me@mbassem.com> | 2024-02-28 20:45:28 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-02-28 20:45:28 +0000 |
| commit | 3208dda3848ad739f54cebf44c423e2b68e85b2d (patch) | |
| tree | 25602c451354a296e8779197fdd42acab7526502 /packages/web/app/dashboard/bookmarks/components | |
| parent | 7096fb3941579e5c045796361745d597e03ff7fc (diff) | |
| download | karakeep-3208dda3848ad739f54cebf44c423e2b68e85b2d.tar.zst | |
feature: Add support for storing and previewing raw notes
Diffstat (limited to 'packages/web/app/dashboard/bookmarks/components')
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx (renamed from packages/web/app/dashboard/bookmarks/components/AddLink.tsx) | 43 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx | 68 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx | 108 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx | 20 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/LinkCard.tsx | 50 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/TagList.tsx | 40 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/TagModal.tsx | 5 | ||||
| -rw-r--r-- | packages/web/app/dashboard/bookmarks/components/TextCard.tsx | 62 |
8 files changed, 320 insertions, 76 deletions
diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx index 242a52a5..4f0de87a 100644 --- a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx +++ b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx @@ -2,25 +2,40 @@ import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Plus } from "lucide-react"; +import { Pencil, Plus } from "lucide-react"; import { useForm, SubmitErrorHandler } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { toast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; +import { useState } from "react"; -const formSchema = z.object({ - url: z.string().url({ message: "The link must be a valid URL" }), -}); +function AddText() { + const [isEditorOpen, setEditorOpen] = useState(false); -export default function AddLink() { + return ( + <div className="flex"> + <BookmarkedTextEditor open={isEditorOpen} setOpen={setEditorOpen} /> + <Button className="m-auto" onClick={() => setEditorOpen(true)}> + <Pencil /> + </Button> + </div> + ); +} + +function AddLink() { + const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), + }); const form = useForm<z.infer<typeof formSchema>>({ resolver: zodResolver(formSchema), }); const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; - const bookmarkLinkMutator = api.bookmarks.bookmarkLink.useMutation({ + const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({ onSuccess: () => { invalidateBookmarksCache(); }, @@ -41,13 +56,14 @@ export default function AddLink() { return ( <Form {...form}> <form + className="flex-grow" onSubmit={form.handleSubmit( (value) => - bookmarkLinkMutator.mutate({ url: value.url, type: "link" }), + createBookmarkMutator.mutate({ url: value.url, type: "link" }), onError, )} > - <div className="container flex w-full items-center space-x-2 py-4"> + <div className="flex w-full items-center space-x-2 py-4"> <FormField control={form.control} name="url" @@ -61,7 +77,7 @@ export default function AddLink() { ); }} /> - <ActionButton type="submit" loading={bookmarkLinkMutator.isPending}> + <ActionButton type="submit" loading={createBookmarkMutator.isPending}> <Plus /> </ActionButton> </div> @@ -69,3 +85,12 @@ export default function AddLink() { </Form> ); } + +export default function AddBookmark() { + return ( + <div className="container flex gap-2"> + <AddLink /> + <AddText /> + </div> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx index b8f6d8f2..866a4cbd 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx @@ -2,7 +2,7 @@ import { useToast } from "@/components/ui/use-toast"; import { api } from "@/lib/trpc"; -import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { ZBookmark, ZBookmarkedLink } from "@/lib/types/api/bookmarks"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -14,12 +14,15 @@ import { Archive, Link, MoreHorizontal, + Pencil, RotateCw, Star, Tags, Trash2, } from "lucide-react"; import { useTagModel } from "./TagModal"; +import { useState } from "react"; +import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const { toast } = useToast(); @@ -27,6 +30,8 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const [_, setTagModalIsOpen, tagModal] = useTagModel(bookmark); + const [isTextEditorOpen, setTextEditorOpen] = useState(false); + const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; const onError = () => { @@ -72,13 +77,27 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { return ( <> {tagModal} + <BookmarkedTextEditor + bookmark={bookmark} + open={isTextEditorOpen} + setOpen={setTextEditorOpen} + /> <DropdownMenu> <DropdownMenuTrigger asChild> - <Button variant="ghost"> + <Button + variant="ghost" + className="focus-visible:ring-0 focus-visible:ring-offset-0" + > <MoreHorizontal /> </Button> </DropdownMenuTrigger> <DropdownMenuContent className="w-fit"> + {bookmark.content.type === "text" && ( + <DropdownMenuItem onClick={() => setTextEditorOpen(true)}> + <Pencil className="mr-2 size-4" /> + <span>Edit</span> + </DropdownMenuItem> + )} <DropdownMenuItem onClick={() => updateBookmarkMutator.mutate({ @@ -101,29 +120,36 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { <Archive className="mr-2 size-4" /> <span>{bookmark.archived ? "Un-archive" : "Archive"}</span> </DropdownMenuItem> - <DropdownMenuItem - onClick={() => { - navigator.clipboard.writeText(bookmark.content.url); - toast({ - description: "Link was added to your clipboard!", - }); - }} - > - <Link className="mr-2 size-4" /> - <span>Copy Link</span> - </DropdownMenuItem> + {bookmark.content.type === "link" && ( + <DropdownMenuItem + onClick={() => { + navigator.clipboard.writeText( + (bookmark.content as ZBookmarkedLink).url, + ); + toast({ + description: "Link was added to your clipboard!", + }); + }} + > + <Link className="mr-2 size-4" /> + <span>Copy Link</span> + </DropdownMenuItem> + )} <DropdownMenuItem onClick={() => setTagModalIsOpen(true)}> <Tags className="mr-2 size-4" /> <span>Edit Tags</span> </DropdownMenuItem> - <DropdownMenuItem - onClick={() => - crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) - } - > - <RotateCw className="mr-2 size-4" /> - <span>Refresh</span> - </DropdownMenuItem> + + {bookmark.content.type === "link" && ( + <DropdownMenuItem + onClick={() => + crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) + } + > + <RotateCw className="mr-2 size-4" /> + <span>Refresh</span> + </DropdownMenuItem> + )} <DropdownMenuItem className="text-destructive" onClick={() => diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx new file mode 100644 index 00000000..e9138e03 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx @@ -0,0 +1,108 @@ +import { ZBookmark } from "@/lib/types/api/bookmarks"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { api } from "@/lib/trpc"; +import { useState } from "react"; +import { toast } from "@/components/ui/use-toast"; + +export function BookmarkedTextEditor({ + bookmark, + open, + setOpen, +}: { + bookmark?: ZBookmark; + open: boolean; + setOpen: (open: boolean) => void; +}) { + const isNewBookmark = bookmark === undefined; + const [noteText, setNoteText] = useState( + bookmark && bookmark.content.type == "text" ? bookmark.content.text : "", + ); + + const invalidateAllBookmarksCache = + api.useUtils().bookmarks.getBookmarks.invalidate; + const invalidateOneBookmarksCache = + api.useUtils().bookmarks.getBookmark.invalidate; + + const { mutate: createBookmarkMutator, isPending: isCreationPending } = + api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + invalidateAllBookmarksCache(); + toast({ + description: "Note created!", + }); + setOpen(false); + setNoteText(""); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + const { mutate: updateBookmarkMutator, isPending: isUpdatePending } = + api.bookmarks.updateBookmarkText.useMutation({ + onSuccess: () => { + invalidateOneBookmarksCache({ + bookmarkId: bookmark!.id, + }); + toast({ + description: "Note updated!", + }); + setOpen(false); + setNoteText(""); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + const isPending = isCreationPending || isUpdatePending; + + const onSave = () => { + if (isNewBookmark) { + createBookmarkMutator({ + type: "text", + text: noteText, + }); + } else { + updateBookmarkMutator({ + bookmarkId: bookmark.id, + text: noteText, + }); + } + }; + + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogContent> + <DialogHeader> + <DialogTitle className="pb-4"> + {isNewBookmark ? "New Note" : "Edit Note"} + </DialogTitle> + <Textarea + value={noteText} + onChange={(e) => setNoteText(e.target.value)} + className="h-52 grow" + /> + </DialogHeader> + <DialogFooter className="flex-shrink gap-1 sm:justify-end"> + <DialogClose asChild> + <Button type="button" variant="secondary"> + Close + </Button> + </DialogClose> + <ActionButton type="button" loading={isPending} onClick={onSave}> + Save + </ActionButton> + </DialogFooter> + </DialogContent> + </Dialog> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx index e07d48b6..c960c8b7 100644 --- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx +++ b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx @@ -3,11 +3,18 @@ import LinkCard from "./LinkCard"; import { ZBookmark, ZGetBookmarksRequest } from "@/lib/types/api/bookmarks"; import { api } from "@/lib/trpc"; +import TextCard from "./TextCard"; -function renderBookmark(bookmark: ZBookmark) { +function renderBookmark(bookmark: ZBookmark, className: string) { switch (bookmark.content.type) { case "link": - return <LinkCard key={bookmark.id} bookmark={bookmark} />; + return ( + <LinkCard key={bookmark.id} bookmark={bookmark} className={className} /> + ); + case "text": + return ( + <TextCard key={bookmark.id} bookmark={bookmark} className={className} /> + ); } } @@ -25,8 +32,13 @@ export default function BookmarksGrid({ return <p>No bookmarks</p>; } return ( - <div className="container grid grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> - {data.bookmarks.map((b) => renderBookmark(b))} + <div className="container grid grid-flow-row-dense grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3"> + {data.bookmarks.map((b) => + renderBookmark( + b, + "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all", + ), + )} </div> ); } diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx index cd0f128c..73d3f300 100644 --- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx +++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx @@ -1,6 +1,5 @@ "use client"; -import { Badge } from "@/components/ui/badge"; import { ImageCard, ImageCardBanner, @@ -13,50 +12,24 @@ import { ZBookmark } from "@/lib/types/api/bookmarks"; import Link from "next/link"; import BookmarkOptions from "./BookmarkOptions"; import { api } from "@/lib/trpc"; -import { Skeleton } from "@/components/ui/skeleton"; import { Star } from "lucide-react"; +import { cn } from "@/lib/utils"; +import TagList from "./TagList"; function isStillCrawling(bookmark: ZBookmark) { return ( + bookmark.content.type == "link" && !bookmark.content.crawledAt && Date.now().valueOf() - bookmark.createdAt.valueOf() < 30 * 1000 ); } -function TagList(bookmark: ZBookmark, stillCrawling: boolean) { - if (stillCrawling) { - return ( - <ImageCardBody className="space-y-2"> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - </ImageCardBody> - ); - } - return ( - <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden"> - {bookmark.tags.map((t) => ( - <Link - className="flex h-full flex-col justify-end" - key={t.id} - href={`/dashboard/tags/${t.name}`} - > - <Badge - variant="default" - className="text-nowrap bg-gray-300 text-gray-500 hover:text-white" - > - #{t.name} - </Badge> - </Link> - ))} - </ImageCardBody> - ); -} - export default function LinkCard({ bookmark: initialData, + className, }: { bookmark: ZBookmark; + className: string; }) { const { data: bookmark } = api.bookmarks.getBookmark.useQuery( { @@ -78,6 +51,9 @@ export default function LinkCard({ }, ); const link = bookmark.content; + if (link.type != "link") { + throw new Error("Unexpected bookmark type"); + } const isCrawling = isStillCrawling(bookmark); const parsedUrl = new URL(link.url); @@ -88,11 +64,7 @@ export default function LinkCard({ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII="; return ( - <ImageCard - className={ - "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all" - } - > + <ImageCard className={cn(className, "row-span-2")}> {bookmark.favourited && ( <Star className="absolute m-1 size-8 rounded bg-gray-100 p-1" @@ -111,7 +83,9 @@ export default function LinkCard({ </ImageCardTitle> {/* There's a hack here. Every tag has the full hight of the container itself. That why, when we enable flex-wrap, the overflowed don't show up. */} - {TagList(bookmark, isCrawling)} + <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden"> + <TagList bookmark={bookmark} loading={isCrawling} /> + </ImageCardBody> <ImageCardFooter> <div className="flex justify-between text-gray-500"> <div className="my-auto"> diff --git a/packages/web/app/dashboard/bookmarks/components/TagList.tsx b/packages/web/app/dashboard/bookmarks/components/TagList.tsx new file mode 100644 index 00000000..440452a7 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/TagList.tsx @@ -0,0 +1,40 @@ +import { Badge } from "@/components/ui/badge"; +import Link from "next/link"; +import { Skeleton } from "@/components/ui/skeleton"; +import { ZBookmark } from "@/lib/types/api/bookmarks"; + +export default function TagList({ + bookmark, + loading, +}: { + bookmark: ZBookmark; + loading?: boolean; +}) { + if (loading) { + return ( + <div className="space-y-2"> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-full" /> + <Skeleton className="h-4 w-full" /> + </div> + ); + } + return ( + <> + {bookmark.tags.map((t) => ( + <Link + className="flex h-full flex-col justify-end" + key={t.id} + href={`/dashboard/tags/${t.name}`} + > + <Badge + variant="default" + className="text-nowrap bg-gray-300 text-gray-500 hover:text-white" + > + #{t.name} + </Badge> + </Link> + ))} + </> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx index b0e391b7..3b6a0bd4 100644 --- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx +++ b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx @@ -4,7 +4,6 @@ import { Dialog, DialogClose, DialogContent, - DialogDescription, DialogFooter, DialogHeader, DialogTitle, @@ -168,10 +167,8 @@ export default function TagModal({ <DialogContent> <DialogHeader> <DialogTitle>Edit Tags</DialogTitle> - <DialogDescription> - <TagEditor tags={tags} setTags={setTags} /> - </DialogDescription> </DialogHeader> + <TagEditor tags={tags} setTags={setTags} /> <DialogFooter className="sm:justify-end"> <DialogClose asChild> <Button type="button" variant="secondary"> diff --git a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx new file mode 100644 index 00000000..7ee1a90b --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { ZBookmark } from "@/lib/types/api/bookmarks"; +import BookmarkOptions from "./BookmarkOptions"; +import { api } from "@/lib/trpc"; +import { Star } from "lucide-react"; +import { cn } from "@/lib/utils"; +import TagList from "./TagList"; + +export default function TextCard({ + bookmark: initialData, + className, +}: { + bookmark: ZBookmark; + className: string; +}) { + const { data: bookmark } = api.bookmarks.getBookmark.useQuery( + { + bookmarkId: initialData.id, + }, + { + initialData, + }, + ); + const bookmarkedText = bookmark.content; + if (bookmarkedText.type != "text") { + throw new Error("Unexpected bookmark type"); + } + + const numWords = bookmarkedText.text.split(" ").length; + + return ( + <div + className={cn( + className, + cn( + "flex flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md", + numWords > 12 ? "row-span-2 h-96" : "row-span-1 h-40", + ), + )} + > + <p className="grow overflow-hidden text-ellipsis"> + {bookmarkedText.text} + </p> + <div className="flex flex-none flex-wrap gap-1 overflow-hidden"> + <TagList bookmark={bookmark} /> + </div> + <div className="flex w-full justify-between"> + <div> + {bookmark.favourited && ( + <Star + className="my-1 size-8 rounded p-1" + color="#ebb434" + fill="#ebb434" + /> + )} + </div> + <BookmarkOptions bookmark={bookmark} /> + </div> + </div> + ); +} |
