diff options
Diffstat (limited to 'packages/web/app/dashboard')
6 files changed, 275 insertions, 0 deletions
diff --git a/packages/web/app/dashboard/bookmarks/components/AddLink.tsx b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx new file mode 100644 index 00000000..fb77786c --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/AddLink.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Form, FormControl, FormField, FormItem } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import APIClient from "@/lib/api"; +import { Plus } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useForm, SubmitErrorHandler } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { toast } from "@/components/ui/use-toast"; + +const formSchema = z.object({ + url: z.string().url({ message: "The link must be a valid URL" }), +}); + +export default function AddLink() { + const router = useRouter(); + + const form = useForm<z.infer<typeof formSchema>>({ + resolver: zodResolver(formSchema), + }); + + async function onSubmit(value: z.infer<typeof formSchema>) { + const [_resp, error] = await APIClient.bookmarkLink(value.url); + if (error) { + toast({ description: error.message, variant: "destructive" }); + return; + } + router.refresh(); + } + + 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)}> + <div className="py-4 container flex w-full items-center space-x-2"> + <FormField + control={form.control} + name="url" + render={({ field }) => { + return ( + <FormItem className="flex-1"> + <FormControl> + <Input type="text" placeholder="Link" {...field} /> + </FormControl> + </FormItem> + ); + }} + /> + <Button type="submit"> + <Plus /> + </Button> + </div> + </form> + </Form> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx new file mode 100644 index 00000000..da59d9da --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/LinkCard.tsx @@ -0,0 +1,96 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + ImageCard, + ImageCardBody, + ImageCardFooter, + ImageCardTitle, +} from "@/components/ui/imageCard"; +import { useToast } from "@/components/ui/use-toast"; +import APIClient from "@/lib/api"; +import { ZBookmarkedLink } from "@/lib/types/api/links"; +import { MoreHorizontal, Trash2 } from "lucide-react"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export function LinkOptions({ linkId }: { linkId: string }) { + const { toast } = useToast(); + const router = useRouter(); + + const unbookmarkLink = async () => { + let [_, error] = await APIClient.unbookmarkLink(linkId); + + if (error) { + toast({ + variant: "destructive", + title: "Something went wrong", + description: "There was a problem with your request.", + }); + } else { + toast({ + description: "The link has been deleted!", + }); + } + + router.refresh(); + }; + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost"> + <MoreHorizontal /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-fit"> + <DropdownMenuItem className="text-destructive" onClick={unbookmarkLink}> + <Trash2 className="mr-2 h-4 w-4" /> + <span>Delete</span> + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ); +} + +export default function LinkCard({ link }: { link: ZBookmarkedLink }) { + const parsedUrl = new URL(link.url); + + return ( + <ImageCard + className={ + "bg-gray-50 duration-300 ease-in border border-grey-100 hover:transition-all hover:border-blue-300" + } + image={link.details?.imageUrl ?? undefined} + > + <ImageCardTitle> + <Link className="line-clamp-3" href={link.url}> + {link.details?.title ?? parsedUrl.host} + </Link> + </ImageCardTitle> + <ImageCardBody className="py-2 overflow-clip"> + {link.tags.map((t) => ( + <Badge variant="default" className="bg-gray-300 text-gray-500" key={t.id}> + #{t.name} + </Badge> + ))} + </ImageCardBody> + <ImageCardFooter> + <div className="flex justify-between text-gray-500"> + <div className="my-auto"> + <Link className="line-clamp-1 hover:text-black" href={link.url}> + {parsedUrl.host} + </Link> + </div> + <LinkOptions linkId={link.id} /> + </div> + </ImageCardFooter> + </ImageCard> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx new file mode 100644 index 00000000..66f0d766 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/components/LinksGrid.tsx @@ -0,0 +1,21 @@ +import { getServerSession } from "next-auth"; +import { redirect } from "next/navigation"; +import { authOptions } from "@/lib/auth"; +import { getLinks } from "@/lib/services/links"; +import LinkCard from "./LinkCard"; + +export default async function LinksGrid() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/"); + } + const links = await getLinks(session.user.id); + + return ( + <div className="container grid gap-4 grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> + {links.map((l) => ( + <LinkCard key={l.id} link={l} /> + ))} + </div> + ); +} diff --git a/packages/web/app/dashboard/bookmarks/page.tsx b/packages/web/app/dashboard/bookmarks/page.tsx new file mode 100644 index 00000000..b4158893 --- /dev/null +++ b/packages/web/app/dashboard/bookmarks/page.tsx @@ -0,0 +1,20 @@ +import AddLink from "./components/AddLink"; +import LinksGrid from "./components/LinksGrid"; +import type { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Remember - Bookmarks", +}; + +export default async function Bookmarks() { + return ( + <div className="flex flex-col"> + <div> + <AddLink /> + </div> + <div> + <LinksGrid /> + </div> + </div> + ); +} diff --git a/packages/web/app/dashboard/components/Sidebar.tsx b/packages/web/app/dashboard/components/Sidebar.tsx new file mode 100644 index 00000000..0ed87daf --- /dev/null +++ b/packages/web/app/dashboard/components/Sidebar.tsx @@ -0,0 +1,56 @@ +import { Button } from "@/components/ui/button"; +import { authOptions } from "@/lib/auth"; +import { Archive, MoreHorizontal, Star, Tag, Home, Brain} from "lucide-react"; +import { getServerSession } from "next-auth"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +function SidebarItem({ + name, + logo, + path, +}: { + name: string; + logo: React.ReactNode; + path: string; +}) { + return ( + <li className="rounded-lg px-3 py-2 hover:bg-slate-100"> + <Link href={path} className="flex w-full space-x-2"> + {logo} + <span className="my-auto"> {name} </span> + </Link> + </li> + ); +} + +export default async function Sidebar() { + const session = await getServerSession(authOptions); + if (!session) { + redirect("/"); + } + + return ( + <aside className="flex flex-col h-full w-60 border-r p-4"> + <div className="flex px-1 mb-5 items-center rounded-lg text-slate-900"> + <Brain /> + <span className="ml-2 text-base font-semibold">Remember</span> + </div> + <hr /> + <div> + <ul className="space-y-2 mt-5 text-sm font-medium"> + <SidebarItem logo={<Home />} name="Home" path="#" /> + <SidebarItem logo={<Star />} name="Favourites" path="#" /> + <SidebarItem logo={<Archive />} name="Archived" path="#" /> + <SidebarItem logo={<Tag />} name="Tags" path="#" /> + </ul> + </div> + <div className="mt-auto flex justify-between"> + <div className="my-auto"> {session.user.name} </div> + <Button variant="ghost" className="h-10 w-30"> + <MoreHorizontal /> + </Button> + </div> + </aside> + ); +} diff --git a/packages/web/app/dashboard/layout.tsx b/packages/web/app/dashboard/layout.tsx new file mode 100644 index 00000000..9b21271e --- /dev/null +++ b/packages/web/app/dashboard/layout.tsx @@ -0,0 +1,15 @@ +import Bookmarks from "@/app/dashboard/bookmarks/page"; +import Sidebar from "@/app/dashboard/components/Sidebar"; + +export default async function Dashboard() { + return ( + <div className="flex w-screen h-screen"> + <div className="flex-none"> + <Sidebar /> + </div> + <div className="flex-1 bg-gray-100"> + <Bookmarks /> + </div> + </div> + ); +} |
