diff options
Diffstat (limited to 'packages/web/components')
54 files changed, 0 insertions, 4170 deletions
diff --git a/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx b/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx deleted file mode 100644 index 5973f909..00000000 --- a/packages/web/components/dashboard/bookmarks/AddLinkButton.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -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 { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { useState } from "react"; - -export function AddLinkButton({ children }: { children: React.ReactNode }) { - const [isOpen, setOpen] = useState(false); - - 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(); - setOpen(false); - }, - 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 ( - <Dialog open={isOpen} onOpenChange={setOpen}> - <DialogTrigger asChild>{children}</DialogTrigger> - <DialogContent> - <Form {...form}> - <DialogHeader> - <DialogTitle>Add Link</DialogTitle> - </DialogHeader> - <form - className="flex flex-col gap-4" - onSubmit={form.handleSubmit( - (value) => - createBookmarkMutator.mutate({ url: value.url, type: "link" }), - onError, - )} - > - <FormField - control={form.control} - name="url" - render={({ field }) => { - return ( - <FormItem className="flex-1"> - <FormControl> - <Input type="text" placeholder="Link" {...field} /> - </FormControl> - </FormItem> - ); - }} - /> - <DialogFooter className="flex-shrink gap-1 sm:justify-end"> - <DialogClose asChild> - <Button type="button" variant="secondary"> - Close - </Button> - </DialogClose> - <ActionButton - type="submit" - loading={createBookmarkMutator.isPending} - > - Add - </ActionButton> - </DialogFooter> - </form> - </Form> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/web/components/dashboard/bookmarks/AddToListModal.tsx b/packages/web/components/dashboard/bookmarks/AddToListModal.tsx deleted file mode 100644 index c9fd5da0..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx b/packages/web/components/dashboard/bookmarks/BookmarkCardSkeleton.tsx deleted file mode 100644 index 1f5fa433..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarkOptions.tsx b/packages/web/components/dashboard/bookmarks/BookmarkOptions.tsx deleted file mode 100644 index 4f08ebee..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarkPreview.tsx b/packages/web/components/dashboard/bookmarks/BookmarkPreview.tsx deleted file mode 100644 index 2a8ae1b1..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarkedTextEditor.tsx b/packages/web/components/dashboard/bookmarks/BookmarkedTextEditor.tsx deleted file mode 100644 index a5b58f1a..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarkedTextViewer.tsx b/packages/web/components/dashboard/bookmarks/BookmarkedTextViewer.tsx deleted file mode 100644 index 8a620341..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/Bookmarks.tsx b/packages/web/components/dashboard/bookmarks/Bookmarks.tsx deleted file mode 100644 index 1ad3670c..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/BookmarksGrid.tsx b/packages/web/components/dashboard/bookmarks/BookmarksGrid.tsx deleted file mode 100644 index 4d5b6b0a..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/LinkCard.tsx b/packages/web/components/dashboard/bookmarks/LinkCard.tsx deleted file mode 100644 index 50f30e47..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/TagList.tsx b/packages/web/components/dashboard/bookmarks/TagList.tsx deleted file mode 100644 index 6c9d2d22..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/TagModal.tsx b/packages/web/components/dashboard/bookmarks/TagModal.tsx deleted file mode 100644 index 8c09d00e..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/TextCard.tsx b/packages/web/components/dashboard/bookmarks/TextCard.tsx deleted file mode 100644 index 2565e69d..00000000 --- a/packages/web/components/dashboard/bookmarks/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/components/dashboard/bookmarks/TopNav.tsx b/packages/web/components/dashboard/bookmarks/TopNav.tsx deleted file mode 100644 index 6c0f18e5..00000000 --- a/packages/web/components/dashboard/bookmarks/TopNav.tsx +++ /dev/null @@ -1,43 +0,0 @@ -"use client"; - -import { Link, NotebookPen } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; -import { useState } from "react"; -import { AddLinkButton } from "./AddLinkButton"; -import { SearchInput } from "../search/SearchInput"; - -function AddText() { - const [isEditorOpen, setEditorOpen] = useState(false); - - return ( - <div className="flex"> - <BookmarkedTextEditor open={isEditorOpen} setOpen={setEditorOpen} /> - <Button className="m-auto" onClick={() => setEditorOpen(true)}> - <NotebookPen /> - </Button> - </div> - ); -} - -function AddLink() { - return ( - <div className="flex"> - <AddLinkButton> - <Button className="m-auto"> - <Link /> - </Button> - </AddLinkButton> - </div> - ); -} - -export default function TopNav() { - return ( - <div className="container flex gap-2 py-4"> - <SearchInput /> - <AddLink /> - <AddText /> - </div> - ); -} diff --git a/packages/web/components/dashboard/lists/AllListsView.tsx b/packages/web/components/dashboard/lists/AllListsView.tsx deleted file mode 100644 index 81f31cde..00000000 --- a/packages/web/components/dashboard/lists/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/dashboard/sidebar/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/components/dashboard/lists/DeleteListButton.tsx b/packages/web/components/dashboard/lists/DeleteListButton.tsx deleted file mode 100644 index 5303b217..00000000 --- a/packages/web/components/dashboard/lists/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/components/dashboard/lists/ListView.tsx b/packages/web/components/dashboard/lists/ListView.tsx deleted file mode 100644 index 2d48d9e3..00000000 --- a/packages/web/components/dashboard/lists/ListView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import BookmarksGrid from "@/components/dashboard/bookmarks/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/components/dashboard/search/SearchInput.tsx b/packages/web/components/dashboard/search/SearchInput.tsx deleted file mode 100644 index 73d14c90..00000000 --- a/packages/web/components/dashboard/search/SearchInput.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Input } from "@/components/ui/input"; -import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; -import { cn } from "@/lib/utils"; -import React from "react"; - -const SearchInput = React.forwardRef< - HTMLInputElement, - React.HTMLAttributes<HTMLInputElement> & { loading?: boolean } ->(({ className, loading = false, ...props }, ref) => { - const { debounceSearch, searchQuery } = useDoBookmarkSearch(); - - return ( - <Input - ref={ref} - placeholder="Search" - defaultValue={searchQuery} - onChange={(e) => debounceSearch(e.target.value)} - className={cn(loading ? "animate-pulse-border" : undefined, className)} - {...props} - /> - ); -}); -SearchInput.displayName = "SearchInput"; - -export { SearchInput }; diff --git a/packages/web/components/dashboard/settings/AddApiKey.tsx b/packages/web/components/dashboard/settings/AddApiKey.tsx deleted file mode 100644 index a4fd9c25..00000000 --- a/packages/web/components/dashboard/settings/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/components/dashboard/settings/ApiKeySettings.tsx b/packages/web/components/dashboard/settings/ApiKeySettings.tsx deleted file mode 100644 index 1598f25f..00000000 --- a/packages/web/components/dashboard/settings/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/components/dashboard/settings/DeleteApiKey.tsx b/packages/web/components/dashboard/settings/DeleteApiKey.tsx deleted file mode 100644 index 566136af..00000000 --- a/packages/web/components/dashboard/settings/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/components/dashboard/sidebar/AllLists.tsx b/packages/web/components/dashboard/sidebar/AllLists.tsx deleted file mode 100644 index a77252d0..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/ModileSidebar.tsx b/packages/web/components/dashboard/sidebar/ModileSidebar.tsx deleted file mode 100644 index 4bd6a347..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/ModileSidebarItem.tsx b/packages/web/components/dashboard/sidebar/ModileSidebarItem.tsx deleted file mode 100644 index 9389d2e4..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/NewListModal.tsx b/packages/web/components/dashboard/sidebar/NewListModal.tsx deleted file mode 100644 index f51616ed..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/Sidebar.tsx b/packages/web/components/dashboard/sidebar/Sidebar.tsx deleted file mode 100644 index a5c1d7a5..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/SidebarItem.tsx b/packages/web/components/dashboard/sidebar/SidebarItem.tsx deleted file mode 100644 index 856bdffd..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/dashboard/sidebar/SidebarProfileOptions.tsx b/packages/web/components/dashboard/sidebar/SidebarProfileOptions.tsx deleted file mode 100644 index f931b63e..00000000 --- a/packages/web/components/dashboard/sidebar/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/components/signin/CredentialsForm.tsx b/packages/web/components/signin/CredentialsForm.tsx deleted file mode 100644 index 5296e163..00000000 --- a/packages/web/components/signin/CredentialsForm.tsx +++ /dev/null @@ -1,222 +0,0 @@ -"use client"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { ActionButton } from "@/components/ui/action-button"; -import { zSignUpSchema } from "@hoarder/trpc/types/users"; -import { signIn } from "next-auth/react"; -import { useState } from "react"; -import { api } from "@/lib/trpc"; -import { useRouter } from "next/navigation"; -import { TRPCClientError } from "@trpc/client"; - -const signInSchema = z.object({ - email: z.string().email(), - password: z.string(), -}); - -function SignIn() { - const [signinError, setSigninError] = useState(false); - const router = useRouter(); - const form = useForm<z.infer<typeof signInSchema>>({ - resolver: zodResolver(signInSchema), - }); - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(async (value) => { - const resp = await signIn("credentials", { - redirect: false, - email: value.email, - password: value.password, - }); - if (!resp || !resp?.ok) { - setSigninError(true); - return; - } - router.replace("/"); - })} - > - <div className="flex w-full flex-col space-y-2"> - {signinError && ( - <p className="w-full text-center text-red-500"> - Incorrect username or password - </p> - )} - <FormField - control={form.control} - name="email" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Email</FormLabel> - <FormControl> - <Input type="text" placeholder="Email" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="password" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Password</FormLabel> - <FormControl> - <Input type="password" placeholder="Password" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton type="submit" loading={form.formState.isSubmitting}> - Sign In - </ActionButton> - </div> - </form> - </Form> - ); -} - -function SignUp() { - const form = useForm<z.infer<typeof zSignUpSchema>>({ - resolver: zodResolver(zSignUpSchema), - }); - const [errorMessage, setErrorMessage] = useState(""); - - const router = useRouter(); - - const createUserMutation = api.users.create.useMutation(); - - return ( - <Form {...form}> - <form - onSubmit={form.handleSubmit(async (value) => { - try { - await createUserMutation.mutateAsync(value); - } catch (e) { - if (e instanceof TRPCClientError) { - setErrorMessage(e.message); - } - return; - } - const resp = await signIn("credentials", { - redirect: false, - email: value.email, - password: value.password, - }); - if (!resp || !resp.ok) { - setErrorMessage("Hit an unexpected error while signing in"); - return; - } - router.replace("/"); - })} - > - <div className="flex w-full flex-col space-y-2"> - {errorMessage && ( - <p className="w-full text-center text-red-500">{errorMessage}</p> - )} - <FormField - control={form.control} - name="name" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Name</FormLabel> - <FormControl> - <Input type="text" placeholder="Name" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="email" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Email</FormLabel> - <FormControl> - <Input type="text" placeholder="Email" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="password" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Password</FormLabel> - <FormControl> - <Input type="password" placeholder="Password" {...field} /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <FormField - control={form.control} - name="confirmPassword" - render={({ field }) => { - return ( - <FormItem> - <FormLabel>Confirm Password</FormLabel> - <FormControl> - <Input - type="password" - placeholder="Confirm Password" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - ); - }} - /> - <ActionButton type="submit" loading={form.formState.isSubmitting}> - Sign Up - </ActionButton> - </div> - </form> - </Form> - ); -} - -export default function CredentialsForm() { - return ( - <Tabs defaultValue="signin" className="w-full"> - <TabsList className="grid w-full grid-cols-2"> - <TabsTrigger value="signin">Sign In</TabsTrigger> - <TabsTrigger value="signup">Sign Up</TabsTrigger> - </TabsList> - <TabsContent value="signin"> - <SignIn /> - </TabsContent> - <TabsContent value="signup"> - <SignUp /> - </TabsContent> - </Tabs> - ); -} diff --git a/packages/web/components/signin/SignInForm.tsx b/packages/web/components/signin/SignInForm.tsx deleted file mode 100644 index 7c8f8936..00000000 --- a/packages/web/components/signin/SignInForm.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { getProviders } from "next-auth/react"; -import SignInProviderButton from "./SignInProviderButton"; -import CredentialsForm from "./CredentialsForm"; - -export default async function SignInForm() { - const providers = await getProviders(); - let providerValues; - if (providers) { - providerValues = Object.values(providers).filter( - // Credentials are handled manually by the sign in form - (p) => p.id != "credentials", - ); - } - - return ( - <div className="flex flex-col items-center space-y-2"> - <CredentialsForm /> - - {providerValues && providerValues.length > 0 && ( - <> - <div className="flex w-full items-center"> - <div className="flex-1 grow border-t-2 border-gray-200"></div> - <span className="bg-white px-3 text-gray-500">Or</span> - <div className="flex-1 grow border-t-2 border-gray-200"></div> - </div> - <div className="space-y-2"> - {providerValues.map((provider) => ( - <div key={provider.id}> - <SignInProviderButton provider={provider} /> - </div> - ))} - </div> - </> - )} - </div> - ); -} diff --git a/packages/web/components/signin/SignInProviderButton.tsx b/packages/web/components/signin/SignInProviderButton.tsx deleted file mode 100644 index 0831236c..00000000 --- a/packages/web/components/signin/SignInProviderButton.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; -import { Button } from "@/components/ui/button"; -import { ClientSafeProvider, signIn } from "next-auth/react"; - -export default function SignInProviderButton({ - provider, -}: { - provider: ClientSafeProvider; -}) { - return ( - <Button - onClick={() => - signIn(provider.id, { - callbackUrl: "/", - }) - } - > - Sign in with {provider.name} - </Button> - ); -} diff --git a/packages/web/components/ui/action-button.tsx b/packages/web/components/ui/action-button.tsx deleted file mode 100644 index 42e16f65..00000000 --- a/packages/web/components/ui/action-button.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, ButtonProps } from "./button"; -import LoadingSpinner from "./spinner"; - -export function ActionButton({ - children, - loading, - spinner, - disabled, - ...props -}: ButtonProps & { - loading: boolean; - spinner?: React.ReactNode; -}) { - spinner ||= <LoadingSpinner />; - if (disabled !== undefined) { - disabled ||= loading; - } else if (loading) { - disabled = true; - } - return ( - <Button {...props} disabled={disabled}> - {loading ? spinner : children} - </Button> - ); -} diff --git a/packages/web/components/ui/back-button.tsx b/packages/web/components/ui/back-button.tsx deleted file mode 100644 index 685930df..00000000 --- a/packages/web/components/ui/back-button.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { Button, ButtonProps } from "./button"; - -export function BackButton({ ...props }: ButtonProps) { - const router = useRouter(); - return <Button {...props} onClick={() => router.back()} />; -} diff --git a/packages/web/components/ui/badge.tsx b/packages/web/components/ui/badge.tsx deleted file mode 100644 index c30daca1..00000000 --- a/packages/web/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "focus:ring-ring inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground hover:bg-primary/80 border-transparent", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes<HTMLDivElement>, - VariantProps<typeof badgeVariants> {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( - <div className={cn(badgeVariants({ variant }), className)} {...props} /> - ); -} - -export { Badge, badgeVariants }; diff --git a/packages/web/components/ui/button.tsx b/packages/web/components/ui/button.tsx deleted file mode 100644 index 79b45fa0..00000000 --- a/packages/web/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "ring-offset-background focus-visible:ring-ring inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border-input bg-background hover:bg-accent hover:text-accent-foreground border", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean; -} - -const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - <Comp - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - }, -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/packages/web/components/ui/card.tsx b/packages/web/components/ui/card.tsx deleted file mode 100644 index f4e57996..00000000 --- a/packages/web/components/ui/card.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - "bg-card text-card-foreground rounded-lg border shadow-sm", - className, - )} - {...props} - /> -)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1.5 p-6", className)} - {...props} - /> -)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> ->(({ className, ...props }, ref) => ( - <h3 - ref={ref} - className={cn( - "text-2xl font-semibold leading-none tracking-tight", - className, - )} - {...props} - /> -)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => ( - <p - ref={ref} - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> -)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> -)); -CardFooter.displayName = "CardFooter"; - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, -}; diff --git a/packages/web/components/ui/dialog.tsx b/packages/web/components/ui/dialog.tsx deleted file mode 100644 index 8fe3fe35..00000000 --- a/packages/web/components/ui/dialog.tsx +++ /dev/null @@ -1,122 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Dialog = DialogPrimitive.Root; - -const DialogTrigger = DialogPrimitive.Trigger; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogClose = DialogPrimitive.Close; - -const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", - className, - )} - {...props} - /> -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg", - className, - )} - {...props} - > - {children} - <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none"> - <X className="size-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col space-y-1.5 text-center sm:text-left", - className, - )} - {...props} - /> -); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", - className, - )} - {...props} - /> -); -DialogFooter.displayName = "DialogFooter"; - -const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "text-lg font-semibold leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/packages/web/components/ui/dropdown-menu.tsx b/packages/web/components/ui/dropdown-menu.tsx deleted file mode 100644 index 3a9a2ff7..00000000 --- a/packages/web/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { Check, ChevronRight, Circle } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - "focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none", - inset && "pl-8", - className, - )} - {...props} - > - {children} - <ChevronRight className="ml-auto size-4" /> - </DropdownMenuPrimitive.SubTrigger> -)); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg", - className, - )} - {...props} - /> -)); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md", - className, - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex size-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="size-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.CheckboxItem> -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - "focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - )} - {...props} - > - <span className="absolute left-2 flex size-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="size-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn("bg-muted -mx-1 my-1 h-px", className)} - {...props} - /> -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ); -}; -DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/packages/web/components/ui/form.tsx b/packages/web/components/ui/form.tsx deleted file mode 100644 index e62e10e9..00000000 --- a/packages/web/components/ui/form.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { Slot } from "@radix-ui/react-slot"; -import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, -} from "react-hook-form"; - -import { cn } from "@/lib/utils"; -import { Label } from "@/components/ui/label"; - -const Form = FormProvider; - -type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, -> = { - name: TName; -}; - -const FormFieldContext = React.createContext<FormFieldContextValue>( - {} as FormFieldContextValue, -); - -const FormField = < - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, ->({ - ...props -}: ControllerProps<TFieldValues, TName>) => { - return ( - <FormFieldContext.Provider value={{ name: props.name }}> - <Controller {...props} /> - </FormFieldContext.Provider> - ); -}; - -const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext); - const itemContext = React.useContext(FormItemContext); - const { getFieldState, formState } = useFormContext(); - - const fieldState = getFieldState(fieldContext.name, formState); - - if (!fieldContext) { - throw new Error("useFormField should be used within <FormField>"); - } - - const { id } = itemContext; - - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - }; -}; - -type FormItemContextValue = { - id: string; -}; - -const FormItemContext = React.createContext<FormItemContextValue>( - {} as FormItemContextValue, -); - -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => { - const id = React.useId(); - - return ( - <FormItemContext.Provider value={{ id }}> - <div ref={ref} className={cn("space-y-2", className)} {...props} /> - </FormItemContext.Provider> - ); -}); -FormItem.displayName = "FormItem"; - -const FormLabel = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> ->(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField(); - - return ( - <Label - ref={ref} - className={cn(error && "text-destructive", className)} - htmlFor={formItemId} - {...props} - /> - ); -}); -FormLabel.displayName = "FormLabel"; - -const FormControl = React.forwardRef< - React.ElementRef<typeof Slot>, - React.ComponentPropsWithoutRef<typeof Slot> ->(({ ...props }, ref) => { - const { error, formItemId, formDescriptionId, formMessageId } = - useFormField(); - - return ( - <Slot - ref={ref} - id={formItemId} - aria-describedby={ - !error - ? `${formDescriptionId}` - : `${formDescriptionId} ${formMessageId}` - } - aria-invalid={!!error} - {...props} - /> - ); -}); -FormControl.displayName = "FormControl"; - -const FormDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => { - const { formDescriptionId } = useFormField(); - - return ( - <p - ref={ref} - id={formDescriptionId} - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> - ); -}); -FormDescription.displayName = "FormDescription"; - -const FormMessage = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, children, ...props }, ref) => { - const { error, formMessageId } = useFormField(); - const body = error ? String(error?.message) : children; - - if (!body) { - return null; - } - - return ( - <p - ref={ref} - id={formMessageId} - className={cn("text-destructive text-sm font-medium", className)} - {...props} - > - {body} - </p> - ); -}); -FormMessage.displayName = "FormMessage"; - -export { - useFormField, - Form, - FormItem, - FormLabel, - FormControl, - FormDescription, - FormMessage, - FormField, -}; diff --git a/packages/web/components/ui/imageCard.tsx b/packages/web/components/ui/imageCard.tsx deleted file mode 100644 index f10ebdb5..00000000 --- a/packages/web/components/ui/imageCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export function ImageCard({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return ( - <div - className={cn("h-96 overflow-hidden rounded-lg shadow-md", className)} - {...props} - /> - ); -} - -export function ImageCardBanner({ - className, - ...props -}: React.ImgHTMLAttributes<HTMLImageElement>) { - return ( - // eslint-disable-next-line @next/next/no-img-element - <img - className={cn("h-56 min-h-56 w-full object-cover", className)} - alt="card banner" - {...props} - /> - ); -} - -export function ImageCardContent({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return ( - <div - className={cn( - "flex h-40 min-h-40 flex-col justify-between p-2", - className, - )} - {...props} - /> - ); -} - -export function ImageCardTitle({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return ( - <div - className={cn("order-first flex-none text-lg font-bold", className)} - {...props} - /> - ); -} - -export function ImageCardBody({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return <div className={cn("order-1", className)} {...props} />; -} - -export function ImageCardFooter({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return <div className={cn("order-last", className)} {...props} />; -} diff --git a/packages/web/components/ui/input.tsx b/packages/web/components/ui/input.tsx deleted file mode 100644 index 21aac7ad..00000000 --- a/packages/web/components/ui/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export interface InputProps - extends React.InputHTMLAttributes<HTMLInputElement> {} - -const Input = React.forwardRef<HTMLInputElement, InputProps>( - ({ className, type, ...props }, ref) => { - return ( - <input - type={type} - className={cn( - "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Input.displayName = "Input"; - -export { Input }; diff --git a/packages/web/components/ui/label.tsx b/packages/web/components/ui/label.tsx deleted file mode 100644 index 84f8b0c7..00000000 --- a/packages/web/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", -); - -const Label = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & - VariantProps<typeof labelVariants> ->(({ className, ...props }, ref) => ( - <LabelPrimitive.Root - ref={ref} - className={cn(labelVariants(), className)} - {...props} - /> -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/packages/web/components/ui/popover.tsx b/packages/web/components/ui/popover.tsx deleted file mode 100644 index a361ba7d..00000000 --- a/packages/web/components/ui/popover.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as PopoverPrimitive from "@radix-ui/react-popover"; - -import { cn } from "@/lib/utils"; - -const Popover = PopoverPrimitive.Root; - -const PopoverTrigger = PopoverPrimitive.Trigger; - -const PopoverContent = React.forwardRef< - React.ElementRef<typeof PopoverPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border p-4 shadow-md outline-none", - className, - )} - {...props} - /> - </PopoverPrimitive.Portal> -)); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/packages/web/components/ui/scroll-area.tsx b/packages/web/components/ui/scroll-area.tsx deleted file mode 100644 index 32cb6022..00000000 --- a/packages/web/components/ui/scroll-area.tsx +++ /dev/null @@ -1,48 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; - -import { cn } from "@/lib/utils"; - -const ScrollArea = React.forwardRef< - React.ElementRef<typeof ScrollAreaPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root> ->(({ className, children, ...props }, ref) => ( - <ScrollAreaPrimitive.Root - ref={ref} - className={cn("relative overflow-hidden", className)} - {...props} - > - <ScrollAreaPrimitive.Viewport className="size-full rounded-[inherit]"> - {children} - </ScrollAreaPrimitive.Viewport> - <ScrollBar /> - <ScrollAreaPrimitive.Corner /> - </ScrollAreaPrimitive.Root> -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>, - React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar> ->(({ className, orientation = "vertical", ...props }, ref) => ( - <ScrollAreaPrimitive.ScrollAreaScrollbar - ref={ref} - orientation={orientation} - className={cn( - "flex touch-none select-none transition-colors", - orientation === "vertical" && - "h-full w-2.5 border-l border-l-transparent p-[1px]", - orientation === "horizontal" && - "h-2.5 flex-col border-t border-t-transparent p-[1px]", - className, - )} - {...props} - > - <ScrollAreaPrimitive.ScrollAreaThumb className="bg-border relative flex-1 rounded-full" /> - </ScrollAreaPrimitive.ScrollAreaScrollbar> -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/packages/web/components/ui/select.tsx b/packages/web/components/ui/select.tsx deleted file mode 100644 index efd4ff1e..00000000 --- a/packages/web/components/ui/select.tsx +++ /dev/null @@ -1,160 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SelectPrimitive from "@radix-ui/react-select"; -import { Check, ChevronDown, ChevronUp } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Select = SelectPrimitive.Root; - -const SelectGroup = SelectPrimitive.Group; - -const SelectValue = SelectPrimitive.Value; - -const SelectTrigger = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> ->(({ className, children, ...props }, ref) => ( - <SelectPrimitive.Trigger - ref={ref} - className={cn( - "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", - className, - )} - {...props} - > - {children} - <SelectPrimitive.Icon asChild> - <ChevronDown className="size-4 opacity-50" /> - </SelectPrimitive.Icon> - </SelectPrimitive.Trigger> -)); -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; - -const SelectScrollUpButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollUpButton - ref={ref} - className={cn( - "flex cursor-default items-center justify-center py-1", - className, - )} - {...props} - > - <ChevronUp className="size-4" /> - </SelectPrimitive.ScrollUpButton> -)); -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; - -const SelectScrollDownButton = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.ScrollDownButton - ref={ref} - className={cn( - "flex cursor-default items-center justify-center py-1", - className, - )} - {...props} - > - <ChevronDown className="size-4" /> - </SelectPrimitive.ScrollDownButton> -)); -SelectScrollDownButton.displayName = - SelectPrimitive.ScrollDownButton.displayName; - -const SelectContent = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> ->(({ className, children, position = "popper", ...props }, ref) => ( - <SelectPrimitive.Portal> - <SelectPrimitive.Content - ref={ref} - className={cn( - "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md", - position === "popper" && - "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", - className, - )} - position={position} - {...props} - > - <SelectScrollUpButton /> - <SelectPrimitive.Viewport - className={cn( - "p-1", - position === "popper" && - "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]", - )} - > - {children} - </SelectPrimitive.Viewport> - <SelectScrollDownButton /> - </SelectPrimitive.Content> - </SelectPrimitive.Portal> -)); -SelectContent.displayName = SelectPrimitive.Content.displayName; - -const SelectLabel = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.Label - ref={ref} - className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)} - {...props} - /> -)); -SelectLabel.displayName = SelectPrimitive.Label.displayName; - -const SelectItem = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> ->(({ className, children, ...props }, ref) => ( - <SelectPrimitive.Item - ref={ref} - className={cn( - "focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", - className, - )} - {...props} - > - <span className="absolute left-2 flex size-3.5 items-center justify-center"> - <SelectPrimitive.ItemIndicator> - <Check className="size-4" /> - </SelectPrimitive.ItemIndicator> - </span> - - <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> - </SelectPrimitive.Item> -)); -SelectItem.displayName = SelectPrimitive.Item.displayName; - -const SelectSeparator = React.forwardRef< - React.ElementRef<typeof SelectPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <SelectPrimitive.Separator - ref={ref} - className={cn("bg-muted -mx-1 my-1 h-px", className)} - {...props} - /> -)); -SelectSeparator.displayName = SelectPrimitive.Separator.displayName; - -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, - SelectScrollUpButton, - SelectScrollDownButton, -}; diff --git a/packages/web/components/ui/separator.tsx b/packages/web/components/ui/separator.tsx deleted file mode 100644 index 3b9f2b84..00000000 --- a/packages/web/components/ui/separator.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as SeparatorPrimitive from "@radix-ui/react-separator"; - -import { cn } from "@/lib/utils"; - -const Separator = React.forwardRef< - React.ElementRef<typeof SeparatorPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root> ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref, - ) => ( - <SeparatorPrimitive.Root - ref={ref} - decorative={decorative} - orientation={orientation} - className={cn( - "bg-border shrink-0", - orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]", - className, - )} - {...props} - /> - ), -); -Separator.displayName = SeparatorPrimitive.Root.displayName; - -export { Separator }; diff --git a/packages/web/components/ui/skeleton.tsx b/packages/web/components/ui/skeleton.tsx deleted file mode 100644 index 5fab2023..00000000 --- a/packages/web/components/ui/skeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cn } from "@/lib/utils"; - -function Skeleton({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) { - return ( - <div - className={cn("bg-muted animate-pulse rounded-md", className)} - {...props} - /> - ); -} - -export { Skeleton }; diff --git a/packages/web/components/ui/spinner.tsx b/packages/web/components/ui/spinner.tsx deleted file mode 100644 index adcd2807..00000000 --- a/packages/web/components/ui/spinner.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { cn } from "@/lib/utils"; - -export default function LoadingSpinner({ className }: { className?: string }) { - return ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - className={cn("animate-spin", className)} - > - <path d="M21 12a9 9 0 1 1-6.219-8.56" /> - </svg> - ); -} diff --git a/packages/web/components/ui/table.tsx b/packages/web/components/ui/table.tsx deleted file mode 100644 index 0fa9288e..00000000 --- a/packages/web/components/ui/table.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes<HTMLTableElement> ->(({ className, ...props }, ref) => ( - <div className="relative w-full overflow-auto"> - <table - ref={ref} - className={cn("w-full caption-bottom text-sm", className)} - {...props} - /> - </div> -)); -Table.displayName = "Table"; - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> -)); -TableHeader.displayName = "TableHeader"; - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <tbody - ref={ref} - className={cn("[&_tr:last-child]:border-0", className)} - {...props} - /> -)); -TableBody.displayName = "TableBody"; - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes<HTMLTableSectionElement> ->(({ className, ...props }, ref) => ( - <tfoot - ref={ref} - className={cn( - "bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", - className, - )} - {...props} - /> -)); -TableFooter.displayName = "TableFooter"; - -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes<HTMLTableRowElement> ->(({ className, ...props }, ref) => ( - <tr - ref={ref} - className={cn( - "hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", - className, - )} - {...props} - /> -)); -TableRow.displayName = "TableRow"; - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes<HTMLTableCellElement> ->(({ className, ...props }, ref) => ( - <th - ref={ref} - className={cn( - "text-muted-foreground h-12 px-4 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0", - className, - )} - {...props} - /> -)); -TableHead.displayName = "TableHead"; - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes<HTMLTableCellElement> ->(({ className, ...props }, ref) => ( - <td - ref={ref} - className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)} - {...props} - /> -)); -TableCell.displayName = "TableCell"; - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes<HTMLTableCaptionElement> ->(({ className, ...props }, ref) => ( - <caption - ref={ref} - className={cn("text-muted-foreground mt-4 text-sm", className)} - {...props} - /> -)); -TableCaption.displayName = "TableCaption"; - -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -}; diff --git a/packages/web/components/ui/tabs.tsx b/packages/web/components/ui/tabs.tsx deleted file mode 100644 index 990017db..00000000 --- a/packages/web/components/ui/tabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as TabsPrimitive from "@radix-ui/react-tabs"; - -import { cn } from "@/lib/utils"; - -const Tabs = TabsPrimitive.Root; - -const TabsList = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.List>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.List - ref={ref} - className={cn( - "bg-muted text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1", - className, - )} - {...props} - /> -)); -TabsList.displayName = TabsPrimitive.List.displayName; - -const TabsTrigger = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Trigger>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.Trigger - ref={ref} - className={cn( - "ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm", - className, - )} - {...props} - /> -)); -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; - -const TabsContent = React.forwardRef< - React.ElementRef<typeof TabsPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> ->(({ className, ...props }, ref) => ( - <TabsPrimitive.Content - ref={ref} - className={cn( - "ring-offset-background focus-visible:ring-ring mt-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2", - className, - )} - {...props} - /> -)); -TabsContent.displayName = TabsPrimitive.Content.displayName; - -export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/packages/web/components/ui/textarea.tsx b/packages/web/components/ui/textarea.tsx deleted file mode 100644 index a0de3371..00000000 --- a/packages/web/components/ui/textarea.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export interface TextareaProps - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} - -const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( - ({ className, ...props }, ref) => { - return ( - <textarea - className={cn( - "border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex min-h-[80px] w-full rounded-md border px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Textarea.displayName = "Textarea"; - -export { Textarea }; diff --git a/packages/web/components/ui/toast.tsx b/packages/web/components/ui/toast.tsx deleted file mode 100644 index 0d162dca..00000000 --- a/packages/web/components/ui/toast.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as React from "react"; -import * as ToastPrimitives from "@radix-ui/react-toast"; -import { cva, type VariantProps } from "class-variance-authority"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Viewport>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Viewport - ref={ref} - className={cn( - "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]", - className, - )} - {...props} - /> -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none", - { - variants: { - variant: { - default: "bg-background text-foreground border", - destructive: - "destructive border-destructive bg-destructive text-destructive-foreground group", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -const Toast = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Root>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> & - VariantProps<typeof toastVariants> ->(({ className, variant, ...props }, ref) => { - return ( - <ToastPrimitives.Root - ref={ref} - className={cn(toastVariants({ variant }), className)} - {...props} - /> - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Action>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Action - ref={ref} - className={cn( - "ring-offset-background hover:bg-secondary focus:ring-ring group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - className, - )} - {...props} - /> -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Close>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Close - ref={ref} - className={cn( - "text-foreground/50 hover:text-foreground absolute right-2 top-2 rounded-md p-1 opacity-0 transition-opacity focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", - className, - )} - toast-close="" - {...props} - > - <X className="size-4" /> - </ToastPrimitives.Close> -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Title>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Title - ref={ref} - className={cn("text-sm font-semibold", className)} - {...props} - /> -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef<typeof ToastPrimitives.Description>, - React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description> ->(({ className, ...props }, ref) => ( - <ToastPrimitives.Description - ref={ref} - className={cn("text-sm opacity-90", className)} - {...props} - /> -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>; - -type ToastActionElement = React.ReactElement<typeof ToastAction>; - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -}; diff --git a/packages/web/components/ui/toaster.tsx b/packages/web/components/ui/toaster.tsx deleted file mode 100644 index 7d82ed55..00000000 --- a/packages/web/components/ui/toaster.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast"; -import { useToast } from "@/components/ui/use-toast"; - -export function Toaster() { - const { toasts } = useToast(); - - return ( - <ToastProvider> - {toasts.map(function ({ id, title, description, action, ...props }) { - return ( - <Toast key={id} {...props}> - <div className="grid gap-1"> - {title && <ToastTitle>{title}</ToastTitle>} - {description && ( - <ToastDescription>{description}</ToastDescription> - )} - </div> - {action} - <ToastClose /> - </Toast> - ); - })} - <ToastViewport /> - </ToastProvider> - ); -} diff --git a/packages/web/components/ui/use-toast.ts b/packages/web/components/ui/use-toast.ts deleted file mode 100644 index 5491e140..00000000 --- a/packages/web/components/ui/use-toast.ts +++ /dev/null @@ -1,189 +0,0 @@ -// Inspired by react-hot-toast library -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial<ToasterToast>; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t, - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t, - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(_state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit<ToasterToast, "id">; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState<State>(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; |
