diff options
| author | MohamedBassem <me@mbassem.com> | 2024-03-10 17:59:58 +0000 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-03-10 17:59:58 +0000 |
| commit | d6dd76021226802adf5295b3243d6f2ae4fa5cc2 (patch) | |
| tree | 7a25423d46db9e0e224b5f58b73cec5768953b44 /packages/web/app/dashboard | |
| parent | 8ab868d3f94cc6609d278dc952432f1a244c3f84 (diff) | |
| download | karakeep-d6dd76021226802adf5295b3243d6f2ae4fa5cc2.tar.zst | |
refactor: Move all components to the top level directory
Diffstat (limited to 'packages/web/app/dashboard')
37 files changed, 13 insertions, 2149 deletions
diff --git a/packages/web/app/dashboard/archive/page.tsx b/packages/web/app/dashboard/archive/page.tsx index 81eea57c..69559185 100644 --- a/packages/web/app/dashboard/archive/page.tsx +++ b/packages/web/app/dashboard/archive/page.tsx @@ -1,4 +1,4 @@ -import Bookmarks from "../bookmarks/components/Bookmarks"; +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; export default async function ArchivedBookmarkPage() { return ( diff --git a/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx b/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx deleted file mode 100644 index d12fc663..00000000 --- a/packages/web/app/dashboard/bookmarks/components/AddBookmark.tsx +++ /dev/null @@ -1,100 +0,0 @@ -"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 ( - <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), - 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<z.infer<typeof formSchema>> = (errors) => { - toast({ - description: Object.values(errors) - .map((v) => v.message) - .join("\n"), - variant: "destructive", - }); - }; - - return ( - <Form {...form}> - <form - className="flex-grow" - onSubmit={form.handleSubmit( - (value) => - createBookmarkMutator.mutate({ url: value.url, type: "link" }), - onError, - )} - > - <div className="flex w-full items-center space-x-2 py-4"> - <FormField - control={form.control} - name="url" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormControl> - <Input type="text" placeholder="Link" {...field} /> - </FormControl> - </FormItem> - ); - }} - /> - <ActionButton type="submit" loading={createBookmarkMutator.isPending}> - <Plus /> - </ActionButton> - </div> - </form> - </Form> - ); -} - -export default function AddBookmark() { - return ( - <div className="container flex gap-2"> - <AddLink /> - <AddText /> - </div> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx b/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx deleted file mode 100644 index c9fd5da0..00000000 --- a/packages/web/app/dashboard/bookmarks/components/AddToListModal.tsx +++ /dev/null @@ -1,168 +0,0 @@ -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<z.infer<typeof formSchema>>({ - 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 ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogContent> - <Form {...form}> - <form - onSubmit={form.handleSubmit((value) => { - addToList({ - bookmarkId: bookmarkId, - listId: value.listId, - }); - })} - > - <DialogHeader> - <DialogTitle>Add to List</DialogTitle> - </DialogHeader> - - <div className="py-4"> - {lists ? ( - <FormField - control={form.control} - name="listId" - render={({ field }) => { - return ( - <FormItem> - <FormControl> - <Select onValueChange={field.onChange}> - <SelectTrigger className="w-full"> - <SelectValue placeholder="Select a list" /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - {lists && - lists.lists.map((l) => ( - <SelectItem key={l.id} value={l.id}> - {l.icon} {l.name} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - ) : ( - <LoadingSpinner /> - )} - </div> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton - type="submit" - loading={isAddingToListPending} - disabled={isPending} - > - Add - </ActionButton> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ); -} - -export function useAddToListModal(bookmarkId: string) { - const [open, setOpen] = useState(false); - - return { - open, - setOpen, - content: ( - <AddToListModal bookmarkId={bookmarkId} open={open} setOpen={setOpen} /> - ), - }; -} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx deleted file mode 100644 index 1f5fa433..00000000 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkCardSkeleton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { - ImageCard, - ImageCardBody, - ImageCardContent, - ImageCardFooter, - ImageCardTitle, - ImageCardBanner, -} from "@/components/ui/imageCard"; -import { Skeleton } from "@/components/ui/skeleton"; - -export default function BookmarkCardSkeleton() { - return ( - <ImageCard - className={ - "border-grey-100 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all" - } - > - <ImageCardBanner src="/blur.avif" /> - <ImageCardContent> - <ImageCardTitle></ImageCardTitle> - <ImageCardBody className="space-y-2"> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - </ImageCardBody> - <ImageCardFooter></ImageCardFooter> - </ImageCardContent> - </ImageCard> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx deleted file mode 100644 index 4f08ebee..00000000 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkOptions.tsx +++ /dev/null @@ -1,185 +0,0 @@ -"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} - <BookmarkedTextEditor - bookmark={bookmark} - open={isTextEditorOpen} - setOpen={setTextEditorOpen} - /> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button - variant="ghost" - className="px-1 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({ - bookmarkId: linkId, - favourited: !bookmark.favourited, - }) - } - > - <Star className="mr-2 size-4" /> - <span>{bookmark.favourited ? "Un-favourite" : "Favourite"}</span> - </DropdownMenuItem> - <DropdownMenuItem - onClick={() => - updateBookmarkMutator.mutate({ - bookmarkId: linkId, - archived: !bookmark.archived, - }) - } - > - <Archive className="mr-2 size-4" /> - <span>{bookmark.archived ? "Un-archive" : "Archive"}</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={() => setAddToListModalOpen(true)}> - <List className="mr-2 size-4" /> - <span>Add to List</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={() => - deleteBookmarkMutator.mutate({ bookmarkId: bookmark.id }) - } - > - <Trash2 className="mr-2 size-4" /> - <span>Delete</span> - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx deleted file mode 100644 index a5b58f1a..00000000 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextEditor.tsx +++ /dev/null @@ -1,109 +0,0 @@ -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 ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogContent> - <DialogHeader> - <DialogTitle>{isNewBookmark ? "New Note" : "Edit Note"}</DialogTitle> - <DialogDescription> - Write your note with markdown support - </DialogDescription> - </DialogHeader> - <Textarea - value={noteText} - onChange={(e) => setNoteText(e.target.value)} - className="h-52 grow" - /> - <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/BookmarkedTextViewer.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx deleted file mode 100644 index 8a620341..00000000 --- a/packages/web/app/dashboard/bookmarks/components/BookmarkedTextViewer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Dialog, DialogContent } from "@/components/ui/dialog"; -import Markdown from "react-markdown"; - -export function BookmarkedTextViewer({ - content, - open, - setOpen, -}: { - content: string; - open: boolean; - setOpen: (open: boolean) => void; -}) { - return ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogContent className="max-h-[75%] overflow-auto"> - <Markdown className="prose">{content}</Markdown> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx b/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx deleted file mode 100644 index 1ad3670c..00000000 --- a/packages/web/app/dashboard/bookmarks/components/Bookmarks.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { redirect } from "next/navigation"; -import BookmarksGrid from "./BookmarksGrid"; -import { ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks"; -import { api } from "@/server/api/client"; -import { getServerAuthSession } from "@/server/auth"; - -export default async function Bookmarks({ - favourited, - archived, - title, - showDivider, -}: ZGetBookmarksRequest & { title: string; showDivider?: boolean }) { - const session = await getServerAuthSession(); - if (!session) { - redirect("/"); - } - - const query = { - favourited, - archived, - }; - - const bookmarks = await api.bookmarks.getBookmarks(query); - - return ( - <div className="container flex flex-col gap-3"> - <div className="text-2xl">{title}</div> - {showDivider && <hr />} - <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} /> - </div> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx deleted file mode 100644 index 4d5b6b0a..00000000 --- a/packages/web/app/dashboard/bookmarks/components/BookmarksGrid.tsx +++ /dev/null @@ -1,64 +0,0 @@ -"use client"; - -import LinkCard from "./LinkCard"; -import { ZBookmark, ZGetBookmarksRequest } from "@hoarder/trpc/types/bookmarks"; -import { api } from "@/lib/trpc"; -import TextCard from "./TextCard"; -import { Slot } from "@radix-ui/react-slot"; -import Masonry from "react-masonry-css"; -import resolveConfig from "tailwindcss/resolveConfig"; -import tailwindConfig from "@/tailwind.config"; -import { useMemo } from "react"; - -function getBreakpointConfig() { - const fullConfig = resolveConfig(tailwindConfig); - - const breakpointColumnsObj: { [key: number]: number; default: number } = { - default: 3, - }; - breakpointColumnsObj[parseInt(fullConfig.theme.screens.lg)] = 2; - breakpointColumnsObj[parseInt(fullConfig.theme.screens.md)] = 1; - breakpointColumnsObj[parseInt(fullConfig.theme.screens.sm)] = 1; - return breakpointColumnsObj; -} - -function renderBookmark(bookmark: ZBookmark) { - let comp; - switch (bookmark.content.type) { - case "link": - comp = <LinkCard bookmark={bookmark} />; - break; - case "text": - comp = <TextCard bookmark={bookmark} />; - break; - } - return ( - <Slot - key={bookmark.id} - className="border-grey-100 mb-4 border bg-gray-50 duration-300 ease-in hover:border-blue-300 hover:transition-all" - > - {comp} - </Slot> - ); -} - -export default function BookmarksGrid({ - query, - bookmarks: initialBookmarks, -}: { - query: ZGetBookmarksRequest; - bookmarks: ZBookmark[]; -}) { - const { data } = api.bookmarks.getBookmarks.useQuery(query, { - initialData: { bookmarks: initialBookmarks }, - }); - const breakpointConfig = useMemo(() => getBreakpointConfig(), []); - if (data.bookmarks.length == 0) { - return <p>No bookmarks</p>; - } - return ( - <Masonry className="flex gap-4" breakpointCols={breakpointConfig}> - {data.bookmarks.map((b) => renderBookmark(b))} - </Masonry> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx deleted file mode 100644 index 50f30e47..00000000 --- a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx +++ /dev/null @@ -1,114 +0,0 @@ -"use client"; - -import { - ImageCard, - ImageCardBanner, - ImageCardBody, - ImageCardContent, - ImageCardFooter, - ImageCardTitle, -} from "@/components/ui/imageCard"; -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import Link from "next/link"; -import BookmarkOptions from "./BookmarkOptions"; -import { api } from "@/lib/trpc"; -import { Maximize2, Star } from "lucide-react"; -import TagList from "./TagList"; -import { - isBookmarkStillCrawling, - isBookmarkStillLoading, - isBookmarkStillTagging, -} from "@/lib/bookmarkUtils"; - -export default function LinkCard({ - bookmark: initialData, - className, -}: { - bookmark: ZBookmark; - className?: string; -}) { - 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 (isBookmarkStillLoading(data)) { - return 1000; - } - return false; - }, - }, - ); - const link = bookmark.content; - if (link.type != "link") { - throw new Error("Unexpected bookmark type"); - } - const parsedUrl = new URL(link.url); - - // A dummy white pixel for when there's no image. - // TODO: Better handling for cards with no images - const image = - link.imageUrl ?? - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAA1JREFUGFdj+P///38ACfsD/QVDRcoAAAAASUVORK5CYII="; - - return ( - <ImageCard className={className}> - <Link href={link.url}> - <ImageCardBanner - src={isBookmarkStillCrawling(bookmark) ? "/blur.avif" : image} - /> - </Link> - <ImageCardContent> - <ImageCardTitle> - <Link className="line-clamp-2" href={link.url} target="_blank"> - {link?.title ?? parsedUrl.host} - </Link> - </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. */} - <ImageCardBody className="flex h-full flex-wrap space-x-1 overflow-hidden"> - <TagList - bookmark={bookmark} - loading={isBookmarkStillTagging(bookmark)} - /> - </ImageCardBody> - <ImageCardFooter> - <div className="mt-1 flex justify-between text-gray-500"> - <div className="my-auto"> - <Link - className="line-clamp-1 hover:text-black" - href={link.url} - target="_blank" - > - {parsedUrl.host} - </Link> - </div> - <div className="flex"> - {bookmark.favourited && ( - <Star - className="m-1 size-8 rounded p-1" - color="#ebb434" - fill="#ebb434" - /> - )} - <Link - className="my-auto block px-2" - href={`/dashboard/preview/${bookmark.id}`} - > - <Maximize2 size="20" /> - </Link> - <BookmarkOptions bookmark={bookmark} /> - </div> - </div> - </ImageCardFooter> - </ImageCardContent> - </ImageCard> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/TagList.tsx b/packages/web/app/dashboard/bookmarks/components/TagList.tsx deleted file mode 100644 index 6c9d2d22..00000000 --- a/packages/web/app/dashboard/bookmarks/components/TagList.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { badgeVariants } from "@/components/ui/badge"; -import Link from "next/link"; -import { Skeleton } from "@/components/ui/skeleton"; -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import { cn } from "@/lib/utils"; - -export default function TagList({ - bookmark, - loading, -}: { - bookmark: ZBookmark; - loading?: boolean; -}) { - if (loading) { - return ( - <div className="flex w-full flex-col justify-end space-y-2 p-2"> - <Skeleton className="h-4 w-full" /> - <Skeleton className="h-4 w-full" /> - </div> - ); - } - return ( - <> - {bookmark.tags.map((t) => ( - <div key={t.id} className="flex h-full flex-col justify-end"> - <Link - className={cn( - badgeVariants({ variant: "outline" }), - "hover:bg-foreground hover:text-secondary text-nowrap", - )} - href={`/dashboard/tags/${t.name}`} - > - {t.name} - </Link> - </div> - ))} - </> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx b/packages/web/app/dashboard/bookmarks/components/TagModal.tsx deleted file mode 100644 index 8c09d00e..00000000 --- a/packages/web/app/dashboard/bookmarks/components/TagModal.tsx +++ /dev/null @@ -1,207 +0,0 @@ -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 { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import { ZAttachedByEnum } from "@hoarder/trpc/types/tags"; -import { cn } from "@/lib/utils"; -import { Sparkles, X } from "lucide-react"; -import { useState, KeyboardEvent, useEffect } from "react"; - -type EditableTag = { attachedBy: ZAttachedByEnum; id?: string; name: string }; - -function TagAddInput({ addTag }: { addTag: (tag: string) => void }) { - const onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter") { - addTag(e.currentTarget.value); - e.currentTarget.value = ""; - } - }; - return ( - <Input - onKeyUp={onKeyUp} - className="h-8 w-full border-none focus-visible:ring-0 focus-visible:ring-offset-0" - /> - ); -} - -function TagPill({ - tag, - deleteCB, -}: { - tag: { attachedBy: ZAttachedByEnum; id?: string; name: string }; - deleteCB: () => void; -}) { - const isAttachedByAI = tag.attachedBy == "ai"; - return ( - <div - className={cn( - "flex min-h-8 space-x-1 rounded px-2", - isAttachedByAI - ? "bg-gradient-to-tr from-purple-500 to-purple-400 text-white" - : "bg-gray-200", - )} - > - {isAttachedByAI && <Sparkles className="m-auto size-4" />} - <p className="m-auto">{tag.name}</p> - <button className="m-auto size-4" onClick={deleteCB}> - <X className="size-4" /> - </button> - </div> - ); -} - -function TagEditor({ - tags, - setTags, -}: { - tags: Map<string, EditableTag>; - setTags: ( - cb: (m: Map<string, EditableTag>) => Map<string, EditableTag>, - ) => void; -}) { - return ( - <div className="mt-4 flex flex-wrap gap-2 rounded border p-2"> - {[...tags.values()].map((t) => ( - <TagPill - key={t.name} - tag={t} - deleteCB={() => - setTags((m) => { - const newMap = new Map(m); - newMap.delete(t.name); - return newMap; - }) - } - /> - ))} - <div className="flex-1"> - <TagAddInput - addTag={(val) => { - setTags((m) => { - if (m.has(val)) { - // Tag already exists - // Do nothing - return m; - } - const newMap = new Map(m); - newMap.set(val, { attachedBy: "human", name: val }); - return newMap; - }); - }} - /> - </div> - </div> - ); -} - -export default function TagModal({ - bookmark, - open, - setOpen, -}: { - bookmark: ZBookmark; - open: boolean; - setOpen: (open: boolean) => void; -}) { - const [tags, setTags] = useState<Map<string, EditableTag>>(new Map()); - useEffect(() => { - const m = new Map<string, EditableTag>(); - for (const t of bookmark.tags) { - m.set(t.name, { attachedBy: t.attachedBy, id: t.id, name: t.name }); - } - setTags(m); - }, [bookmark.tags]); - - const bookmarkInvalidationFunction = - api.useUtils().bookmarks.getBookmark.invalidate; - - const { mutate, isPending } = api.bookmarks.updateTags.useMutation({ - onSuccess: () => { - toast({ - description: "Tags has been updated!", - }); - bookmarkInvalidationFunction({ bookmarkId: bookmark.id }); - }, - onError: () => { - toast({ - variant: "destructive", - title: "Something went wrong", - description: "There was a problem with your request.", - }); - }, - }); - - const onSaveButton = () => { - const exitingTags = new Set(bookmark.tags.map((t) => t.name)); - - const attach = []; - const detach = []; - for (const t of tags.values()) { - if (!exitingTags.has(t.name)) { - attach.push({ tag: t.name }); - } - } - for (const t of bookmark.tags) { - if (!tags.has(t.name)) { - detach.push({ tagId: t.id }); - } - } - mutate({ - bookmarkId: bookmark.id, - attach, - detach, - }); - }; - - return ( - <Dialog open={open} onOpenChange={setOpen}> - <DialogContent> - <DialogHeader> - <DialogTitle>Edit Tags</DialogTitle> - </DialogHeader> - <TagEditor tags={tags} setTags={setTags} /> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton - type="button" - loading={isPending} - onClick={onSaveButton} - > - Save - </ActionButton> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} - -export function useTagModel(bookmark: ZBookmark) { - const [open, setOpen] = useState(false); - - return { - open, - setOpen, - content: ( - <TagModal - key={bookmark.id} - bookmark={bookmark} - open={open} - setOpen={setOpen} - /> - ), - }; -} diff --git a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx b/packages/web/app/dashboard/bookmarks/components/TextCard.tsx deleted file mode 100644 index 2565e69d..00000000 --- a/packages/web/app/dashboard/bookmarks/components/TextCard.tsx +++ /dev/null @@ -1,94 +0,0 @@ -"use client"; - -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import BookmarkOptions from "./BookmarkOptions"; -import { api } from "@/lib/trpc"; -import { Maximize2, Star } from "lucide-react"; -import { cn } from "@/lib/utils"; -import TagList from "./TagList"; -import Markdown from "react-markdown"; -import { useState } from "react"; -import { BookmarkedTextViewer } from "./BookmarkedTextViewer"; -import Link from "next/link"; -import { isBookmarkStillTagging } from "@/lib/bookmarkUtils"; - -export default function TextCard({ - bookmark: initialData, - className, -}: { - bookmark: ZBookmark; - className?: string; -}) { - const { data: bookmark } = api.bookmarks.getBookmark.useQuery( - { - bookmarkId: initialData.id, - }, - { - initialData, - refetchInterval: (query) => { - const data = query.state.data; - if (!data) { - return false; - } - if (isBookmarkStillTagging(data)) { - return 1000; - } - return false; - }, - }, - ); - const [previewModalOpen, setPreviewModalOpen] = useState(false); - const bookmarkedText = bookmark.content; - if (bookmarkedText.type != "text") { - throw new Error("Unexpected bookmark type"); - } - - return ( - <> - <BookmarkedTextViewer - content={bookmarkedText.text} - open={previewModalOpen} - setOpen={setPreviewModalOpen} - /> - <div - className={cn( - className, - cn( - "flex h-min max-h-96 flex-col gap-y-1 overflow-hidden rounded-lg p-2 shadow-md", - ), - )} - > - <Markdown className="prose grow overflow-hidden"> - {bookmarkedText.text} - </Markdown> - <div className="mt-4 flex flex-none flex-wrap gap-1 overflow-hidden"> - <TagList - bookmark={bookmark} - loading={isBookmarkStillTagging(bookmark)} - /> - </div> - <div className="flex w-full justify-between"> - <div /> - <div className="flex gap-0 text-gray-500"> - <div> - {bookmark.favourited && ( - <Star - className="my-1 size-8 rounded p-1" - color="#ebb434" - fill="#ebb434" - /> - )} - </div> - <Link - className="my-auto block px-2" - href={`/dashboard/preview/${bookmark.id}`} - > - <Maximize2 size="20" /> - </Link> - <BookmarkOptions bookmark={bookmark} /> - </div> - </div> - </div> - </> - ); -} diff --git a/packages/web/app/dashboard/bookmarks/layout.tsx b/packages/web/app/dashboard/bookmarks/layout.tsx index b03a9a19..6a588823 100644 --- a/packages/web/app/dashboard/bookmarks/layout.tsx +++ b/packages/web/app/dashboard/bookmarks/layout.tsx @@ -1,5 +1,5 @@ import React from "react"; -import AddBookmark from "./components/AddBookmark"; +import AddBookmark from "@/components/dashboard/bookmarks/AddBookmark"; import type { Metadata } from "next"; export const metadata: Metadata = { diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx index 517dc184..c9391d85 100644 --- a/packages/web/app/dashboard/bookmarks/page.tsx +++ b/packages/web/app/dashboard/bookmarks/page.tsx @@ -1,4 +1,4 @@ -import Bookmarks from "./components/Bookmarks"; +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; export default async function BookmarksPage() { return <Bookmarks title="Bookmarks" archived={false} />; diff --git a/packages/web/app/dashboard/components/AllLists.tsx b/packages/web/app/dashboard/components/AllLists.tsx deleted file mode 100644 index a77252d0..00000000 --- a/packages/web/app/dashboard/components/AllLists.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client"; - -import { api } from "@/lib/trpc"; -import SidebarItem from "./SidebarItem"; -import NewListModal, { useNewListModal } from "./NewListModal"; -import { Plus } from "lucide-react"; -import Link from "next/link"; -import { ZBookmarkList } from "@hoarder/trpc/types/lists"; - -export default function AllLists({ - initialData, -}: { - initialData: { lists: ZBookmarkList[] }; -}) { - let { data: lists } = api.lists.list.useQuery(undefined, { - initialData, - }); - // TODO: This seems to be a bug in react query - lists ||= initialData; - const { setOpen } = useNewListModal(); - - return ( - <ul className="max-h-full gap-y-2 overflow-auto text-sm font-medium"> - <NewListModal /> - <li className="flex justify-between pb-2 font-bold"> - <p>Lists</p> - <Link href="#" onClick={() => setOpen(true)}> - <Plus /> - </Link> - </li> - <SidebarItem - logo={<span className="text-lg">📋</span>} - name="All Lists" - path={`/dashboard/lists`} - className="py-0.5" - /> - <SidebarItem - logo={<span className="text-lg">⭐️</span>} - name="Favourties" - path={`/dashboard/favourites`} - className="py-0.5" - /> - <SidebarItem - logo={<span className="text-lg">🗄️</span>} - name="Archive" - path={`/dashboard/archive`} - className="py-0.5" - /> - {lists.lists.map((l) => ( - <SidebarItem - key={l.id} - logo={<span className="text-lg"> {l.icon}</span>} - name={l.name} - path={`/dashboard/lists/${l.id}`} - className="py-0.5" - /> - ))} - </ul> - ); -} diff --git a/packages/web/app/dashboard/components/ModileSidebar.tsx b/packages/web/app/dashboard/components/ModileSidebar.tsx deleted file mode 100644 index 4bd6a347..00000000 --- a/packages/web/app/dashboard/components/ModileSidebar.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import MobileSidebarItem from "./ModileSidebarItem"; -import { - Tag, - PackageOpen, - Settings, - Search, - ClipboardList, -} from "lucide-react"; -import SidebarProfileOptions from "./SidebarProfileOptions"; - -export default async function MobileSidebar() { - return ( - <aside className="w-full"> - <ul className="flex justify-between space-x-2 border-b-black bg-gray-100 px-5 py-2 pt-5"> - <MobileSidebarItem logo={<PackageOpen />} path="/dashboard/bookmarks" /> - <MobileSidebarItem logo={<Search />} path="/dashboard/search" /> - <MobileSidebarItem logo={<ClipboardList />} path="/dashboard/lists" /> - <MobileSidebarItem logo={<Tag />} path="/dashboard/tags" /> - <MobileSidebarItem logo={<Settings />} path="/dashboard/settings" /> - <SidebarProfileOptions /> - </ul> - </aside> - ); -} diff --git a/packages/web/app/dashboard/components/ModileSidebarItem.tsx b/packages/web/app/dashboard/components/ModileSidebarItem.tsx deleted file mode 100644 index 9389d2e4..00000000 --- a/packages/web/app/dashboard/components/ModileSidebarItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -export default function MobileSidebarItem({ - logo, - path, -}: { - logo: React.ReactNode; - path: string; -}) { - const currentPath = usePathname(); - return ( - <li - className={cn( - "flex w-full rounded-lg hover:bg-gray-50", - path == currentPath ? "bg-gray-50" : "", - )} - > - <Link href={path} className="mx-auto px-3 py-2"> - {logo} - </Link> - </li> - ); -} diff --git a/packages/web/app/dashboard/components/NewListModal.tsx b/packages/web/app/dashboard/components/NewListModal.tsx deleted file mode 100644 index f51616ed..00000000 --- a/packages/web/app/dashboard/components/NewListModal.tsx +++ /dev/null @@ -1,170 +0,0 @@ -"use client"; - -import data from "@emoji-mart/data"; -import Picker from "@emoji-mart/react"; - -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; - -import { ActionButton } from "@/components/ui/action-button"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} 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 { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Input } from "@/components/ui/input"; - -import { create } from "zustand"; - -export const useNewListModal = create<{ - open: boolean; - setOpen: (v: boolean) => void; -}>((set) => ({ - open: false, - setOpen: (open: boolean) => set(() => ({ open })), -})); - -export default function NewListModal() { - const { open, setOpen } = useNewListModal(); - - const formSchema = z.object({ - name: z.string(), - icon: z.string(), - }); - const form = useForm<z.infer<typeof formSchema>>({ - resolver: zodResolver(formSchema), - defaultValues: { - name: "", - icon: "💡", - }, - }); - - const listsInvalidationFunction = api.useUtils().lists.list.invalidate; - - const { mutate: createList, isPending } = api.lists.create.useMutation({ - onSuccess: () => { - toast({ - description: "List has been created!", - }); - listsInvalidationFunction(); - setOpen(false); - }, - onError: (e) => { - if (e.data?.code == "BAD_REQUEST") { - toast({ - variant: "destructive", - description: e.message, - }); - } else { - toast({ - variant: "destructive", - title: "Something went wrong", - }); - } - }, - }); - - return ( - <Dialog - open={open} - onOpenChange={(s) => { - form.reset(); - setOpen(s); - }} - > - <DialogContent> - <Form {...form}> - <form - onSubmit={form.handleSubmit((value) => { - createList(value); - })} - > - <DialogHeader> - <DialogTitle>New List</DialogTitle> - </DialogHeader> - <div className="flex w-full gap-2 py-4"> - <FormField - control={form.control} - name="icon" - render={({ field }) => { - return ( - <FormItem> - <FormControl> - <Popover> - <PopoverTrigger className="border-input h-full rounded border px-2 text-2xl"> - {field.value} - </PopoverTrigger> - <PopoverContent> - <Picker - data={data} - onEmojiSelect={(e: { native: string }) => - field.onChange(e.native) - } - /> - </PopoverContent> - </Popover> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - - <FormField - control={form.control} - name="name" - render={({ field }) => { - return ( - <FormItem className="grow"> - <FormControl> - <Input - type="text" - className="w-full" - placeholder="List Name" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - </div> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton type="submit" loading={isPending}> - Create - </ActionButton> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx deleted file mode 100644 index a5c1d7a5..00000000 --- a/packages/web/app/dashboard/components/Sidebar.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Tag, Home, PackageOpen, Settings, Search, Shield } from "lucide-react"; -import { redirect } from "next/navigation"; -import SidebarItem from "./SidebarItem"; -import { getServerAuthSession } from "@/server/auth"; -import Link from "next/link"; -import SidebarProfileOptions from "./SidebarProfileOptions"; -import { Separator } from "@/components/ui/separator"; -import AllLists from "./AllLists"; -import serverConfig from "@hoarder/shared/config"; -import { api } from "@/server/api/client"; - -export default async function Sidebar() { - const session = await getServerAuthSession(); - if (!session) { - redirect("/"); - } - - const lists = await api.lists.list(); - - return ( - <aside className="flex h-screen w-60 flex-col gap-5 border-r p-4"> - <Link href={"/dashboard/bookmarks"}> - <div className="flex items-center rounded-lg px-1 text-slate-900"> - <PackageOpen /> - <span className="ml-2 text-base font-semibold">Hoarder</span> - </div> - </Link> - <hr /> - <div> - <ul className="space-y-2 text-sm font-medium"> - <SidebarItem - logo={<Home />} - name="Home" - path="/dashboard/bookmarks" - /> - {serverConfig.meilisearch && ( - <SidebarItem - logo={<Search />} - name="Search" - path="/dashboard/search" - /> - )} - <SidebarItem logo={<Tag />} name="Tags" path="/dashboard/tags" /> - <SidebarItem - logo={<Settings />} - name="Settings" - path="/dashboard/settings" - /> - {session.user.role == "admin" && ( - <SidebarItem - logo={<Shield />} - name="Admin" - path="/dashboard/admin" - /> - )} - </ul> - </div> - <Separator /> - <AllLists initialData={lists} /> - <div className="mt-auto flex justify-between justify-self-end"> - <div className="my-auto"> {session.user.name} </div> - <SidebarProfileOptions /> - </div> - </aside> - ); -} diff --git a/packages/web/app/dashboard/components/SidebarItem.tsx b/packages/web/app/dashboard/components/SidebarItem.tsx deleted file mode 100644 index 856bdffd..00000000 --- a/packages/web/app/dashboard/components/SidebarItem.tsx +++ /dev/null @@ -1,33 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import Link from "next/link"; -import { usePathname } from "next/navigation"; - -export default function SidebarItem({ - name, - logo, - path, - className, -}: { - name: string; - logo: React.ReactNode; - path: string; - className?: string; -}) { - const currentPath = usePathname(); - return ( - <li - className={cn( - "rounded-lg px-3 py-2 hover:bg-slate-100", - path == currentPath ? "bg-gray-50" : "", - className, - )} - > - <Link href={path} className="flex w-full gap-x-2"> - {logo} - <span className="my-auto"> {name} </span> - </Link> - </li> - ); -} diff --git a/packages/web/app/dashboard/components/SidebarProfileOptions.tsx b/packages/web/app/dashboard/components/SidebarProfileOptions.tsx deleted file mode 100644 index f931b63e..00000000 --- a/packages/web/app/dashboard/components/SidebarProfileOptions.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { LogOut, MoreHorizontal } from "lucide-react"; -import { signOut } from "next-auth/react"; - -export default function SidebarProfileOptions() { - return ( - <DropdownMenu> - <DropdownMenuTrigger asChild> - <Button variant="ghost"> - <MoreHorizontal /> - </Button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-fit"> - <DropdownMenuItem - onClick={() => - signOut({ - callbackUrl: "/", - }) - } - > - <LogOut className="mr-2 size-4" /> - <span>Sign Out</span> - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - ); -} diff --git a/packages/web/app/dashboard/favourites/page.tsx b/packages/web/app/dashboard/favourites/page.tsx index 2dc555d2..de17461d 100644 --- a/packages/web/app/dashboard/favourites/page.tsx +++ b/packages/web/app/dashboard/favourites/page.tsx @@ -1,4 +1,4 @@ -import Bookmarks from "../bookmarks/components/Bookmarks"; +import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; export default async function FavouritesBookmarkPage() { return ( diff --git a/packages/web/app/dashboard/layout.tsx b/packages/web/app/dashboard/layout.tsx index 59e293d2..31d592fb 100644 --- a/packages/web/app/dashboard/layout.tsx +++ b/packages/web/app/dashboard/layout.tsx @@ -1,6 +1,6 @@ import { Separator } from "@/components/ui/separator"; -import MobileSidebar from "./components/ModileSidebar"; -import Sidebar from "./components/Sidebar"; +import MobileSidebar from "@/components/dashboard/sidebar/ModileSidebar"; +import Sidebar from "@/components/dashboard/sidebar/Sidebar"; export default async function Dashboard({ children, diff --git a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx b/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx deleted file mode 100644 index 5303b217..00000000 --- a/packages/web/app/dashboard/lists/[listId]/components/DeleteListButton.tsx +++ /dev/null @@ -1,77 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { Trash } from "lucide-react"; -import { useRouter } from "next/navigation"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { ActionButton } from "@/components/ui/action-button"; -import { useState } from "react"; -import { ZBookmarkList } from "@hoarder/trpc/types/lists"; - -export default function DeleteListButton({ list }: { list: ZBookmarkList }) { - const [isDialogOpen, setDialogOpen] = useState(false); - - const router = useRouter(); - - const listsInvalidationFunction = api.useUtils().lists.list.invalidate; - const { mutate: deleteList, isPending } = api.lists.delete.useMutation({ - onSuccess: () => { - listsInvalidationFunction(); - toast({ - description: `List "${list.icon} ${list.name}" is deleted!`, - }); - router.push("/"); - }, - onError: () => { - toast({ - variant: "destructive", - description: `Something went wrong`, - }); - }, - }); - return ( - <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}> - <DialogTrigger asChild> - <Button className="mt-auto flex gap-2" variant="destructive"> - <Trash className="size-5" /> - <span className="hidden md:block">Delete List</span> - </Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle> - Delete {list.icon} {list.name}? - </DialogTitle> - </DialogHeader> - <span> - Are you sure you want to delete {list.icon} {list.name}? - </span> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton - type="button" - variant="destructive" - loading={isPending} - onClick={() => deleteList({ listId: list.id })} - > - Delete - </ActionButton> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx b/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx deleted file mode 100644 index 979b522f..00000000 --- a/packages/web/app/dashboard/lists/[listId]/components/ListView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import BookmarksGrid from "@/app/dashboard/bookmarks/components/BookmarksGrid"; -import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; -import { ZBookmarkListWithBookmarks } from "@hoarder/trpc/types/lists"; -import { api } from "@/lib/trpc"; - -export default function ListView({ - bookmarks, - list: initialData, -}: { - list: ZBookmarkListWithBookmarks; - bookmarks: ZBookmark[]; -}) { - const { data } = api.lists.get.useQuery( - { listId: initialData.id }, - { - initialData, - }, - ); - - return ( - <BookmarksGrid query={{ ids: data.bookmarks }} bookmarks={bookmarks} /> - ); -} diff --git a/packages/web/app/dashboard/lists/[listId]/page.tsx b/packages/web/app/dashboard/lists/[listId]/page.tsx index 397a0f1e..006fd3ad 100644 --- a/packages/web/app/dashboard/lists/[listId]/page.tsx +++ b/packages/web/app/dashboard/lists/[listId]/page.tsx @@ -2,8 +2,8 @@ import { api } from "@/server/api/client"; import { getServerAuthSession } from "@/server/auth"; import { TRPCError } from "@trpc/server"; import { notFound, redirect } from "next/navigation"; -import ListView from "./components/ListView"; -import DeleteListButton from "./components/DeleteListButton"; +import ListView from "@/components/dashboard/lists/ListView"; +import DeleteListButton from "@/components/dashboard/lists/DeleteListButton"; export default async function ListPage({ params, diff --git a/packages/web/app/dashboard/lists/components/AllListsView.tsx b/packages/web/app/dashboard/lists/components/AllListsView.tsx deleted file mode 100644 index 0e2f898b..00000000 --- a/packages/web/app/dashboard/lists/components/AllListsView.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { api } from "@/lib/trpc"; -import { ZBookmarkList } from "@hoarder/trpc/types/lists"; -import { keepPreviousData } from "@tanstack/react-query"; -import { Plus } from "lucide-react"; -import Link from "next/link"; -import { useNewListModal } from "../../components/NewListModal"; - -function ListItem({ - name, - icon, - path, -}: { - name: string; - icon: string; - path: string; -}) { - return ( - <Link href={path}> - <div className="bg-background rounded-md border border-gray-200 px-4 py-2 text-lg"> - <p className="text-nowrap"> - {icon} {name} - </p> - </div> - </Link> - ); -} - -export default function AllListsView({ - initialData, -}: { - initialData: ZBookmarkList[]; -}) { - const { setOpen: setIsNewListModalOpen } = useNewListModal(); - let { data: lists } = api.lists.list.useQuery(undefined, { - initialData: { lists: initialData }, - placeholderData: keepPreviousData, - }); - - // TODO: This seems to be a bug in react query - lists ||= { lists: initialData }; - - return ( - <div className="flex flex-col flex-wrap gap-2 md:flex-row"> - <Button - className="my-auto flex h-full" - onClick={() => setIsNewListModalOpen(true)} - > - <Plus /> - <span className="my-auto">New List</span> - </Button> - <ListItem name="Favourites" icon="⭐️" path={`/dashboard/favourites`} /> - <ListItem name="Archive" icon="🗄️" path={`/dashboard/archive`} /> - {lists.lists.map((l) => ( - <ListItem - key={l.id} - name={l.name} - icon={l.icon} - path={`/dashboard/lists/${l.id}`} - /> - ))} - </div> - ); -} diff --git a/packages/web/app/dashboard/lists/page.tsx b/packages/web/app/dashboard/lists/page.tsx index 62e328b0..88eeda47 100644 --- a/packages/web/app/dashboard/lists/page.tsx +++ b/packages/web/app/dashboard/lists/page.tsx @@ -1,5 +1,5 @@ import { api } from "@/server/api/client"; -import AllListsView from "./components/AllListsView"; +import AllListsView from "@/components/dashboard/lists/AllListsView"; export default async function ListsPage() { const lists = await api.lists.list(); diff --git a/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx b/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx deleted file mode 100644 index 2a8ae1b1..00000000 --- a/packages/web/app/dashboard/preview/[bookmarkId]/components/BookmarkPreview.tsx +++ /dev/null @@ -1,101 +0,0 @@ -"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" && ( - <div className="flex flex-col space-y-2"> - <p className="text-center text-3xl"> - {bookmark.content.title || bookmark.content.url} - </p> - <Link href={bookmark.content.url} className="mx-auto flex gap-2"> - <span className="my-auto">View Original</span> - <ExternalLink /> - </Link> - </div> - ); - - let content; - switch (bookmark.content.type) { - case "link": { - if (!bookmark.content.htmlContent) { - content = ( - <div className="text-red-500">Failed to fetch link content ...</div> - ); - } else { - content = ( - <div - dangerouslySetInnerHTML={{ - __html: bookmark.content.htmlContent || "", - }} - className="prose" - /> - ); - } - break; - } - case "text": { - content = <Markdown className="prose">{bookmark.content.text}</Markdown>; - break; - } - } - - return ( - <div className="bg-background m-4 min-h-screen space-y-4 rounded-md border p-4"> - <div className="flex justify-between"> - <BackButton className="ghost" variant="ghost"> - <ArrowLeftCircle /> - </BackButton> - <div className="my-auto"> - <span className="my-auto flex gap-2"> - <CalendarDays /> {bookmark.createdAt.toLocaleString()} - </span> - </div> - </div> - <hr /> - {linkHeader} - <div className="mx-auto flex h-full border-x p-2 px-4 lg:w-2/3"> - {isBookmarkStillCrawling(bookmark) ? ( - <div className="flex w-full flex-col gap-2"> - <Skeleton className="h-4" /> - <Skeleton className="h-4" /> - <Skeleton className="h-4" /> - </div> - ) : ( - content - )} - </div> - </div> - ); -} diff --git a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx b/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx index 47aeb891..707d2b69 100644 --- a/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx +++ b/packages/web/app/dashboard/preview/[bookmarkId]/page.tsx @@ -1,5 +1,5 @@ import { api } from "@/server/api/client"; -import BookmarkPreview from "./components/BookmarkPreview"; +import BookmarkPreview from "@/components/dashboard/bookmarks/BookmarkPreview"; export default async function BookmarkPreviewPage({ params, diff --git a/packages/web/app/dashboard/search/page.tsx b/packages/web/app/dashboard/search/page.tsx index 1c26608e..514c546d 100644 --- a/packages/web/app/dashboard/search/page.tsx +++ b/packages/web/app/dashboard/search/page.tsx @@ -2,7 +2,7 @@ import { api } from "@/lib/trpc"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; -import BookmarksGrid from "../bookmarks/components/BookmarksGrid"; +import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid"; import { Input } from "@/components/ui/input"; import Loading from "../bookmarks/loading"; import { keepPreviousData } from "@tanstack/react-query"; diff --git a/packages/web/app/dashboard/settings/components/AddApiKey.tsx b/packages/web/app/dashboard/settings/components/AddApiKey.tsx deleted file mode 100644 index a4fd9c25..00000000 --- a/packages/web/app/dashboard/settings/components/AddApiKey.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; - -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { z } from "zod"; -import { useRouter } from "next/navigation"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm, SubmitErrorHandler } from "react-hook-form"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { useState } from "react"; -import { Check, Copy } from "lucide-react"; -import { ActionButton } from "@/components/ui/action-button"; - -function ApiKeySuccess({ apiKey }: { apiKey: string }) { - const [isCopied, setCopied] = useState(false); - - const onCopy = () => { - navigator.clipboard.writeText(apiKey); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }; - - return ( - <div> - <div className="py-4"> - Note: please copy the key and store it somewhere safe. Once you close - the dialog, you won't be able to access it again. - </div> - <div className="flex space-x-2 pt-2"> - <Input value={apiKey} readOnly /> - <Button onClick={onCopy}> - {!isCopied ? ( - <Copy className="size-4" /> - ) : ( - <Check className="size-4" /> - )} - </Button> - </div> - </div> - ); -} - -function AddApiKeyForm({ onSuccess }: { onSuccess: (key: string) => void }) { - const formSchema = z.object({ - name: z.string(), - }); - const router = useRouter(); - const mutator = api.apiKeys.create.useMutation({ - onSuccess: (resp) => { - onSuccess(resp.key); - router.refresh(); - }, - onError: () => { - toast({ description: "Something went wrong", variant: "destructive" }); - }, - }); - - const form = useForm<z.infer<typeof formSchema>>({ - resolver: zodResolver(formSchema), - }); - - async function onSubmit(value: z.infer<typeof formSchema>) { - mutator.mutate({ name: value.name }); - } - - const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => { - toast({ - description: Object.values(errors) - .map((v) => v.message) - .join("\n"), - variant: "destructive", - }); - }; - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(onSubmit, onError)} - className="flex w-full space-x-3 space-y-8 pt-4" - > - <FormField - control={form.control} - name="name" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormLabel>Name</FormLabel> - <FormControl> - <Input type="text" placeholder="Name" {...field} /> - </FormControl> - <FormDescription> - Give your API key a unique name - </FormDescription> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton - className="h-full" - type="submit" - loading={mutator.isPending} - > - Create - </ActionButton> - </form> - </Form> - ); -} - -export default function AddApiKey() { - const [key, setKey] = useState<string | undefined>(undefined); - const [dialogOpen, setDialogOpen] = useState<boolean>(false); - return ( - <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> - <DialogTrigger asChild> - <Button>New API Key</Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle> - {key ? "Key was successfully created" : "Create API key"} - </DialogTitle> - <DialogDescription> - {key ? ( - <ApiKeySuccess apiKey={key} /> - ) : ( - <AddApiKeyForm onSuccess={setKey} /> - )} - </DialogDescription> - </DialogHeader> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button - type="button" - variant="outline" - onClick={() => setKey(undefined)} - > - Close - </Button> - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx b/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx deleted file mode 100644 index 1598f25f..00000000 --- a/packages/web/app/dashboard/settings/components/ApiKeySettings.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table"; -import { api } from "@/server/api/client"; -import DeleteApiKey from "./DeleteApiKey"; -import AddApiKey from "./AddApiKey"; - -export default async function ApiKeys() { - const keys = await api.apiKeys.list(); - return ( - <div className="pt-4"> - <span className="text-xl">API Keys</span> - <hr className="my-2" /> - <div className="flex flex-col space-y-3"> - <div className="flex flex-1 justify-end"> - <AddApiKey /> - </div> - <Table> - <TableHeader> - <TableRow> - <TableHead>Name</TableHead> - <TableHead>Key</TableHead> - <TableHead>Created At</TableHead> - <TableHead>Action</TableHead> - </TableRow> - </TableHeader> - <TableBody> - {keys.keys.map((k) => ( - <TableRow key={k.id}> - <TableCell>{k.name}</TableCell> - <TableCell>**_{k.keyId}_**</TableCell> - <TableCell>{k.createdAt.toLocaleString()}</TableCell> - <TableCell> - <DeleteApiKey name={k.name} id={k.id} /> - </TableCell> - </TableRow> - ))} - <TableRow></TableRow> - </TableBody> - </Table> - </div> - </div> - ); -} diff --git a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx b/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx deleted file mode 100644 index 566136af..00000000 --- a/packages/web/app/dashboard/settings/components/DeleteApiKey.tsx +++ /dev/null @@ -1,74 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { Trash } from "lucide-react"; - -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { useRouter } from "next/navigation"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { ActionButton } from "@/components/ui/action-button"; -import { useState } from "react"; - -export default function DeleteApiKey({ - name, - id, -}: { - name: string; - id: string; -}) { - const [isDialogOpen, setDialogOpen] = useState(false); - const router = useRouter(); - const mutator = api.apiKeys.revoke.useMutation({ - onSuccess: () => { - toast({ - description: "Key was successfully deleted", - }); - setDialogOpen(false); - router.refresh(); - }, - }); - - return ( - <Dialog open={isDialogOpen} onOpenChange={setDialogOpen}> - <DialogTrigger asChild> - <Button variant="destructive"> - <Trash className="size-5" /> - </Button> - </DialogTrigger> - <DialogContent> - <DialogHeader> - <DialogTitle>Delete API Key</DialogTitle> - <DialogDescription> - Are you sure you want to delete the API key "{name}"? Any - service using this API key will lose access. - </DialogDescription> - </DialogHeader> - <DialogFooter className="sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton - type="button" - variant="destructive" - loading={mutator.isPending} - onClick={() => mutator.mutate({ id })} - > - Delete - </ActionButton> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/app/dashboard/settings/page.tsx b/packages/web/app/dashboard/settings/page.tsx index 95637d8c..38091e6c 100644 --- a/packages/web/app/dashboard/settings/page.tsx +++ b/packages/web/app/dashboard/settings/page.tsx @@ -1,4 +1,4 @@ -import ApiKeySettings from "./components/ApiKeySettings"; +import ApiKeySettings from "@/components/dashboard/settings/ApiKeySettings"; export default async function Settings() { return ( <div className="m-4 flex flex-col space-y-2 rounded-md border bg-white p-4"> diff --git a/packages/web/app/dashboard/tags/[tagName]/page.tsx b/packages/web/app/dashboard/tags/[tagName]/page.tsx index fa3a1f8e..c978b86a 100644 --- a/packages/web/app/dashboard/tags/[tagName]/page.tsx +++ b/packages/web/app/dashboard/tags/[tagName]/page.tsx @@ -1,7 +1,7 @@ import { getServerAuthSession } from "@/server/auth"; import { db } from "@hoarder/db"; import { notFound, redirect } from "next/navigation"; -import BookmarksGrid from "../../bookmarks/components/BookmarksGrid"; +import BookmarksGrid from "@/components/dashboard/bookmarks/BookmarksGrid"; import { api } from "@/server/api/client"; import { bookmarkTags, tagsOnBookmarks } from "@hoarder/db/schema"; import { and, eq } from "drizzle-orm"; |
