From d6dd76021226802adf5295b3243d6f2ae4fa5cc2 Mon Sep 17 00:00:00 2001 From: MohamedBassem Date: Sun, 10 Mar 2024 17:59:58 +0000 Subject: refactor: Move all components to the top level directory --- .../components/dashboard/bookmarks/AddBookmark.tsx | 100 ++++++++++ .../dashboard/bookmarks/AddToListModal.tsx | 168 +++++++++++++++++ .../dashboard/bookmarks/BookmarkCardSkeleton.tsx | 30 +++ .../dashboard/bookmarks/BookmarkOptions.tsx | 185 ++++++++++++++++++ .../dashboard/bookmarks/BookmarkPreview.tsx | 101 ++++++++++ .../dashboard/bookmarks/BookmarkedTextEditor.tsx | 109 +++++++++++ .../dashboard/bookmarks/BookmarkedTextViewer.tsx | 20 ++ .../components/dashboard/bookmarks/Bookmarks.tsx | 32 ++++ .../dashboard/bookmarks/BookmarksGrid.tsx | 64 +++++++ .../components/dashboard/bookmarks/LinkCard.tsx | 114 ++++++++++++ .../web/components/dashboard/bookmarks/TagList.tsx | 39 ++++ .../components/dashboard/bookmarks/TagModal.tsx | 207 +++++++++++++++++++++ .../components/dashboard/bookmarks/TextCard.tsx | 94 ++++++++++ .../components/dashboard/lists/AllListsView.tsx | 66 +++++++ .../dashboard/lists/DeleteListButton.tsx | 77 ++++++++ .../web/components/dashboard/lists/ListView.tsx | 25 +++ .../components/dashboard/settings/AddApiKey.tsx | 167 +++++++++++++++++ .../dashboard/settings/ApiKeySettings.tsx | 49 +++++ .../components/dashboard/settings/DeleteApiKey.tsx | 74 ++++++++ .../web/components/dashboard/sidebar/AllLists.tsx | 60 ++++++ .../components/dashboard/sidebar/ModileSidebar.tsx | 24 +++ .../dashboard/sidebar/ModileSidebarItem.tsx | 27 +++ .../components/dashboard/sidebar/NewListModal.tsx | 170 +++++++++++++++++ .../web/components/dashboard/sidebar/Sidebar.tsx | 66 +++++++ .../components/dashboard/sidebar/SidebarItem.tsx | 33 ++++ .../dashboard/sidebar/SidebarProfileOptions.tsx | 35 ++++ 26 files changed, 2136 insertions(+) create mode 100644 packages/web/components/dashboard/bookmarks/AddBookmark.tsx create mode 100644 packages/web/components/dashboard/bookmarks/AddToListModal.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx create mode 100644 packages/web/components/dashboard/bookmarks/Bookmarks.tsx create mode 100644 packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx create mode 100644 packages/web/components/dashboard/bookmarks/LinkCard.tsx create mode 100644 packages/web/components/dashboard/bookmarks/TagList.tsx create mode 100644 packages/web/components/dashboard/bookmarks/TagModal.tsx create mode 100644 packages/web/components/dashboard/bookmarks/TextCard.tsx create mode 100644 packages/web/components/dashboard/lists/AllListsView.tsx create mode 100644 packages/web/components/dashboard/lists/DeleteListButton.tsx create mode 100644 packages/web/components/dashboard/lists/ListView.tsx create mode 100644 packages/web/components/dashboard/settings/AddApiKey.tsx create mode 100644 packages/web/components/dashboard/settings/ApiKeySettings.tsx create mode 100644 packages/web/components/dashboard/settings/DeleteApiKey.tsx create mode 100644 packages/web/components/dashboard/sidebar/AllLists.tsx create mode 100644 packages/web/components/dashboard/sidebar/ModileSidebar.tsx create mode 100644 packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx create mode 100644 packages/web/components/dashboard/sidebar/NewListModal.tsx create mode 100644 packages/web/components/dashboard/sidebar/Sidebar.tsx create mode 100644 packages/web/components/dashboard/sidebar/SidebarItem.tsx create mode 100644 packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx (limited to 'packages/web/components/dashboard') diff --git a/packages/web/components/dashboard/bookmarks/AddBookmark.tsx b/packages/web/components/dashboard/bookmarks/AddBookmark.tsx new file mode 100644 index 00000000..d12fc663 --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/AddBookmark.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +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"; + +function AddText() { + const [isEditorOpen, setEditorOpen] = useState(false); + + return ( +
+ + +
+ ); +} + +function AddLink() { + const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + url: "", + }, + }); + + const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; + const createBookmarkMutator = api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + invalidateBookmarksCache(); + form.reset(); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + + const onError: SubmitErrorHandler> = (errors) => { + toast({ + description: Object.values(errors) + .map((v) => v.message) + .join("\n"), + variant: "destructive", + }); + }; + + return ( +
+ + createBookmarkMutator.mutate({ url: value.url, type: "link" }), + onError, + )} + > +
+ { + return ( + + + + + + ); + }} + /> + + + +
+
+ + ); +} + +export default function AddBookmark() { + return ( +
+ + +
+ ); +} diff --git a/packages/web/components/dashboard/bookmarks/AddToListModal.tsx b/packages/web/components/dashboard/bookmarks/AddToListModal.tsx new file mode 100644 index 00000000..c9fd5da0 --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/AddToListModal.tsx @@ -0,0 +1,168 @@ +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/components/ui/form"; + +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { useState } from "react"; + +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import LoadingSpinner from "@/components/ui/spinner"; +import { z } from "zod"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; + +export default function AddToListModal({ + bookmarkId, + open, + setOpen, +}: { + bookmarkId: string; + open: boolean; + setOpen: (open: boolean) => void; +}) { + const formSchema = z.object({ + listId: z.string({ + required_error: "Please select a list", + }), + }); + const form = useForm>({ + resolver: zodResolver(formSchema), + }); + + const { data: lists, isPending: isFetchingListsPending } = + api.lists.list.useQuery(); + + const listInvalidationFunction = api.useUtils().lists.get.invalidate; + const bookmarksInvalidationFunction = + api.useUtils().bookmarks.getBookmarks.invalidate; + + const { mutate: addToList, isPending: isAddingToListPending } = + api.lists.addToList.useMutation({ + onSuccess: (_resp, req) => { + toast({ + description: "List has been updated!", + }); + listInvalidationFunction({ listId: req.listId }); + bookmarksInvalidationFunction(); + }, + onError: (e) => { + if (e.data?.code == "BAD_REQUEST") { + toast({ + variant: "destructive", + description: e.message, + }); + } else { + toast({ + variant: "destructive", + title: "Something went wrong", + }); + } + }, + }); + + const isPending = isFetchingListsPending || isAddingToListPending; + + return ( + + +
+ { + addToList({ + bookmarkId: bookmarkId, + listId: value.listId, + }); + })} + > + + Add to List + + +
+ {lists ? ( + { + return ( + + + + + + + ); + }} + /> + ) : ( + + )} +
+ + + + + + Add + + +
+ +
+
+ ); +} + +export function useAddToListModal(bookmarkId: string) { + const [open, setOpen] = useState(false); + + return { + open, + setOpen, + content: ( + + ), + }; +} diff --git a/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx b/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx new file mode 100644 index 00000000..1f5fa433 --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx @@ -0,0 +1,30 @@ +import { + ImageCard, + ImageCardBody, + ImageCardContent, + ImageCardFooter, + ImageCardTitle, + ImageCardBanner, +} from "@/components/ui/imageCard"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function BookmarkCardSkeleton() { + return ( + + + + + + + + + + + + + ); +} diff --git a/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx new file mode 100644 index 00000000..4f08ebee --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useToast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { ZBookmark, ZBookmarkedLink } from "@hoarder/trpc/types/bookmarks"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Archive, + Link, + List, + MoreHorizontal, + Pencil, + RotateCw, + Star, + Tags, + Trash2, +} from "lucide-react"; +import { useTagModel } from "./TagModal"; +import { useState } from "react"; +import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; +import { useAddToListModal } from "./AddToListModal"; + +export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { + const { toast } = useToast(); + const linkId = bookmark.id; + + const { setOpen: setTagModalIsOpen, content: tagModal } = + useTagModel(bookmark); + const { setOpen: setAddToListModalOpen, content: addToListModal } = + useAddToListModal(bookmark.id); + + const [isTextEditorOpen, setTextEditorOpen] = useState(false); + + const invalidateAllBookmarksCache = + api.useUtils().bookmarks.getBookmarks.invalidate; + + const invalidateBookmarkCache = + api.useUtils().bookmarks.getBookmark.invalidate; + + const onError = () => { + toast({ + variant: "destructive", + title: "Something went wrong", + description: "There was a problem with your request.", + }); + }; + const deleteBookmarkMutator = api.bookmarks.deleteBookmark.useMutation({ + onSuccess: () => { + toast({ + description: "The bookmark has been deleted!", + }); + }, + onError, + onSettled: () => { + invalidateAllBookmarksCache(); + }, + }); + + const updateBookmarkMutator = api.bookmarks.updateBookmark.useMutation({ + onSuccess: () => { + toast({ + description: "The bookmark has been updated!", + }); + }, + onError, + onSettled: () => { + invalidateBookmarkCache({ bookmarkId: bookmark.id }); + invalidateAllBookmarksCache(); + }, + }); + + const crawlBookmarkMutator = api.bookmarks.recrawlBookmark.useMutation({ + onSuccess: () => { + toast({ + description: "Re-fetch has been enqueued!", + }); + }, + onError, + onSettled: () => { + invalidateBookmarkCache({ bookmarkId: bookmark.id }); + }, + }); + + return ( + <> + {tagModal} + {addToListModal} + + + + + + + {bookmark.content.type === "text" && ( + setTextEditorOpen(true)}> + + Edit + + )} + + updateBookmarkMutator.mutate({ + bookmarkId: linkId, + favourited: !bookmark.favourited, + }) + } + > + + {bookmark.favourited ? "Un-favourite" : "Favourite"} + + + updateBookmarkMutator.mutate({ + bookmarkId: linkId, + archived: !bookmark.archived, + }) + } + > + + {bookmark.archived ? "Un-archive" : "Archive"} + + {bookmark.content.type === "link" && ( + { + navigator.clipboard.writeText( + (bookmark.content as ZBookmarkedLink).url, + ); + toast({ + description: "Link was added to your clipboard!", + }); + }} + > + + Copy Link + + )} + setTagModalIsOpen(true)}> + + Edit Tags + + + setAddToListModalOpen(true)}> + + Add to List + + + {bookmark.content.type === "link" && ( + + crawlBookmarkMutator.mutate({ bookmarkId: bookmark.id }) + } + > + + Refresh + + )} + + deleteBookmarkMutator.mutate({ bookmarkId: bookmark.id }) + } + > + + Delete + + + + + ); +} diff --git a/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx b/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx new file mode 100644 index 00000000..2a8ae1b1 --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { BackButton } from "@/components/ui/back-button"; +import { Skeleton } from "@/components/ui/skeleton"; +import { isBookmarkStillCrawling } from "@/lib/bookmarkUtils"; +import { api } from "@/lib/trpc"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { ArrowLeftCircle, CalendarDays, ExternalLink } from "lucide-react"; +import Link from "next/link"; +import Markdown from "react-markdown"; + +export default function BookmarkPreview({ + initialData, +}: { + initialData: ZBookmark; +}) { + const { data: bookmark } = api.bookmarks.getBookmark.useQuery( + { + bookmarkId: initialData.id, + }, + { + initialData, + refetchInterval: (query) => { + const data = query.state.data; + if (!data) { + return false; + } + // If the link is not crawled or not tagged + if (isBookmarkStillCrawling(data)) { + return 1000; + } + return false; + }, + }, + ); + + const linkHeader = bookmark.content.type == "link" && ( +
+

+ {bookmark.content.title || bookmark.content.url} +

+ + View Original + + +
+ ); + + let content; + switch (bookmark.content.type) { + case "link": { + if (!bookmark.content.htmlContent) { + content = ( +
Failed to fetch link content ...
+ ); + } else { + content = ( +
+ ); + } + break; + } + case "text": { + content = {bookmark.content.text}; + break; + } + } + + return ( +
+
+ + + +
+ + {bookmark.createdAt.toLocaleString()} + +
+
+
+ {linkHeader} +
+ {isBookmarkStillCrawling(bookmark) ? ( +
+ + + +
+ ) : ( + content + )} +
+
+ ); +} diff --git a/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx new file mode 100644 index 00000000..a5b58f1a --- /dev/null +++ b/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx @@ -0,0 +1,109 @@ +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + 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); + }, + 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 ( + + + + {isNewBookmark ? "New Note" : "Edit Note"} + + Write your note with markdown support + + +