diff options
7 files changed, 123 insertions, 150 deletions
diff --git a/apps/web/app/dashboard/bookmarks/page.tsx b/apps/web/app/dashboard/bookmarks/page.tsx index c9391d85..e310df01 100644 --- a/apps/web/app/dashboard/bookmarks/page.tsx +++ b/apps/web/app/dashboard/bookmarks/page.tsx @@ -1,5 +1,5 @@ import Bookmarks from "@/components/dashboard/bookmarks/Bookmarks"; export default async function BookmarksPage() { - return <Bookmarks title="Bookmarks" archived={false} />; + return <Bookmarks title="Bookmarks" archived={false} showEditorCard={true} />; } diff --git a/apps/web/components/dashboard/bookmarks/AddLinkButton.tsx b/apps/web/components/dashboard/bookmarks/AddLinkButton.tsx deleted file mode 100644 index 45a67020..00000000 --- a/apps/web/components/dashboard/bookmarks/AddLinkButton.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type { SubmitErrorHandler } from "react-hook-form"; -import { useState } from "react"; -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 } from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { toast } from "@/components/ui/use-toast"; -import { api } from "@/lib/trpc"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; - -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/apps/web/components/dashboard/bookmarks/Bookmarks.tsx b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx index 9f001551..601b1627 100644 --- a/apps/web/components/dashboard/bookmarks/Bookmarks.tsx +++ b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx @@ -11,7 +11,12 @@ export default async function Bookmarks({ archived, title, showDivider, -}: ZGetBookmarksRequest & { title: string; showDivider?: boolean }) { + showEditorCard = false, +}: ZGetBookmarksRequest & { + title: string; + showDivider?: boolean; + showEditorCard?: boolean; +}) { const session = await getServerAuthSession(); if (!session) { redirect("/"); @@ -28,7 +33,11 @@ export default async function Bookmarks({ <div className="container flex flex-col gap-3"> <div className="text-2xl">{title}</div> {showDivider && <hr />} - <BookmarksGrid query={query} bookmarks={bookmarks.bookmarks} /> + <BookmarksGrid + query={query} + bookmarks={bookmarks.bookmarks} + showEditorCard={showEditorCard} + /> </div> ); } diff --git a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx index 185e318e..644991bb 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarksGrid.tsx @@ -12,9 +12,18 @@ import type { ZGetBookmarksRequest, } from "@hoarder/trpc/types/bookmarks"; +import EditorCard from "./EditorCard"; import LinkCard from "./LinkCard"; import TextCard from "./TextCard"; +function BookmarkCard({ children }: { children: React.ReactNode }) { + return ( + <Slot className="border-grey-100 mb-4 border bg-gray-50 duration-300 ease-in hover:shadow-lg hover:transition-all"> + {children} + </Slot> + ); +} + function getBreakpointConfig() { const fullConfig = resolveConfig(tailwindConfig); @@ -37,22 +46,17 @@ function renderBookmark(bookmark: ZBookmark) { 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:shadow-lg hover:transition-all" - > - {comp} - </Slot> - ); + return <BookmarkCard key={bookmark.id}>{comp}</BookmarkCard>; } export default function BookmarksGrid({ query, bookmarks: initialBookmarks, + showEditorCard = false, }: { query: ZGetBookmarksRequest; bookmarks: ZBookmark[]; + showEditorCard?: boolean; }) { const { data } = api.bookmarks.getBookmarks.useQuery(query, { initialData: { bookmarks: initialBookmarks }, @@ -63,6 +67,11 @@ export default function BookmarksGrid({ } return ( <Masonry className="flex gap-4" breakpointCols={breakpointConfig}> + {showEditorCard && ( + <BookmarkCard> + <EditorCard /> + </BookmarkCard> + )} {data.bookmarks.map((b) => renderBookmark(b))} </Masonry> ); diff --git a/apps/web/components/dashboard/bookmarks/EditorCard.tsx b/apps/web/components/dashboard/bookmarks/EditorCard.tsx new file mode 100644 index 00000000..28e8f41f --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/EditorCard.tsx @@ -0,0 +1,91 @@ +import type { SubmitErrorHandler, SubmitHandler } from "react-hook-form"; +import { ActionButton } from "@/components/ui/action-button"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/components/ui/use-toast"; +import { api } from "@/lib/trpc"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +export default function EditorCard({ className }: { className?: string }) { + const formSchema = z.object({ + text: z.string(), + }); + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + defaultValues: { + text: "", + }, + }); + + const invalidateBookmarksCache = api.useUtils().bookmarks.invalidate; + const { mutate, isPending } = api.bookmarks.createBookmark.useMutation({ + onSuccess: () => { + invalidateBookmarksCache(); + form.reset(); + }, + onError: () => { + toast({ description: "Something went wrong", variant: "destructive" }); + }, + }); + + const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = (data) => { + const text = data.text.trim(); + try { + new URL(text); + mutate({ type: "link", url: text }); + } catch (e) { + // Not a URL + mutate({ type: "text", text }); + } + }; + const onError: SubmitErrorHandler<z.infer<typeof formSchema>> = (errors) => { + toast({ + description: Object.values(errors) + .map((v) => v.message) + .join("\n"), + variant: "destructive", + }); + }; + + return ( + <Form {...form}> + <form + className={cn( + className, + "flex h-96 flex-col gap-2 rounded-xl bg-white p-4", + )} + onSubmit={form.handleSubmit(onSubmit, onError)} + > + <FormField + control={form.control} + name="text" + render={({ field }) => { + return ( + <FormItem className="flex-1"> + <FormControl> + <Textarea + disabled={isPending} + className="h-full w-full resize-none border-none text-lg focus-visible:ring-0" + placeholder={"Paste a link or write a note ..."} + onKeyDown={(e) => { + if (e.key === "Enter" && e.metaKey) { + form.handleSubmit(onSubmit, onError)(); + } + }} + {...field} + /> + </FormControl> + </FormItem> + ); + }} + /> + <ActionButton loading={isPending} type="submit" variant="ghost"> + Save + </ActionButton> + </form> + </Form> + ); +} diff --git a/apps/web/components/dashboard/bookmarks/TopNav.tsx b/apps/web/components/dashboard/bookmarks/TopNav.tsx index 4274762c..568af15d 100644 --- a/apps/web/components/dashboard/bookmarks/TopNav.tsx +++ b/apps/web/components/dashboard/bookmarks/TopNav.tsx @@ -1,44 +1,9 @@ -"use client"; - -import { useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Link, NotebookPen } from "lucide-react"; - import { SearchInput } from "../search/SearchInput"; -import { AddLinkButton } from "./AddLinkButton"; -import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; - -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"> + <div className="container py-4"> <SearchInput /> - <AddLink /> - <AddText /> </div> ); } diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index abb19f5e..1ca6a0ae 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -1,3 +1,5 @@ +"use client"; + import React from "react"; import { Input } from "@/components/ui/input"; import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; |
