diff --git a/.eslintrc.json b/.eslintrc.json index 26977516..38293435 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,6 +13,7 @@ "no-redeclare": "off", "@next/next/no-html-link-for-pages": "off", "no-undef": "off", + "react/jsx-no-undef": "off", "no-unused-vars": [ "error", { diff --git a/bun.lockb b/bun.lockb index fc5566d0..f4f09e8a 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/web/app/bookmarks/components/AddLink.tsx b/web/app/bookmarks/components/AddLink.tsx index 54cf9137..fab4db8b 100644 --- a/web/app/bookmarks/components/AddLink.tsx +++ b/web/app/bookmarks/components/AddLink.tsx @@ -1,6 +1,9 @@ "use client"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import APIClient from "@/lib/api"; +import { Plus } from "lucide-react"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -11,6 +14,7 @@ export default function AddLink() { const bookmarkLink = async () => { const [_resp, error] = await APIClient.bookmarkLink(link); if (error) { + // TODO: Proper error handling alert(error.message); return; } @@ -18,8 +22,8 @@ export default function AddLink() { }; return ( -
- + - +
); } diff --git a/web/app/bookmarks/components/LinkCard.tsx b/web/app/bookmarks/components/LinkCard.tsx index 103f97ef..75973f7e 100644 --- a/web/app/bookmarks/components/LinkCard.tsx +++ b/web/app/bookmarks/components/LinkCard.tsx @@ -1,19 +1,67 @@ +"use client"; + +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 { ZBookmarkedLink } from "@/lib/types/api/links"; +import { MoreHorizontal, Trash2 } from "lucide-react"; import Link from "next/link"; -export default async function LinkCard({ link }: { link: ZBookmarkedLink }) { +export function LinkOptions() { + // TODO: Implement deletion + return ( + + + + + + + + Delete + + + + ); +} + +export default function LinkCard({ link }: { link: ZBookmarkedLink }) { + const parsedUrl = new URL(link.url); + return ( - -
-

- {link.details?.favicon && ( - // eslint-disable-next-line @next/next/no-img-element - - )} - {link.details?.title ?? link.id} -

-

{link.details?.description ?? link.url}

-
- + + + + {link.details?.title ?? parsedUrl.host} + + + + +
+
+ + {parsedUrl.host} + +
+ +
+
+
); } diff --git a/web/app/bookmarks/components/LinksGrid.tsx b/web/app/bookmarks/components/LinksGrid.tsx index 83aaca80..4b82df98 100644 --- a/web/app/bookmarks/components/LinksGrid.tsx +++ b/web/app/bookmarks/components/LinksGrid.tsx @@ -12,7 +12,7 @@ export default async function LinksGrid() { const links = await getLinks(session.user.id); return ( -
+
{links.map((l) => ( ))} diff --git a/web/components/ui/button.tsx b/web/components/ui/button.tsx new file mode 100644 index 00000000..0ba42773 --- /dev/null +++ b/web/components/ui/button.tsx @@ -0,0 +1,56 @@ +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( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring 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 border-input bg-background hover:bg-accent hover:text-accent-foreground", + 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: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/web/components/ui/card.tsx b/web/components/ui/card.tsx new file mode 100644 index 00000000..afa13ecf --- /dev/null +++ b/web/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/web/components/ui/dropdown-menu.tsx b/web/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..f69a0d64 --- /dev/null +++ b/web/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"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, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/web/components/ui/imageCard.tsx b/web/components/ui/imageCard.tsx new file mode 100644 index 00000000..1394ae08 --- /dev/null +++ b/web/components/ui/imageCard.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export function ImageCard({ + children, + image, + className, + ...props +}: React.HTMLAttributes & { image?: string }) { + return ( +
+
+
{children}
+
+ ); +} + +export function ImageCardTitle({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export function ImageCardBody({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export function ImageCardFooter({ + className, + ...props +}: React.HTMLAttributes) { + return
; +} diff --git a/web/components/ui/input.tsx b/web/components/ui/input.tsx new file mode 100644 index 00000000..677d05fd --- /dev/null +++ b/web/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/web/package.json b/web/package.json index 6a043a77..09249bab 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,8 @@ "dependencies": { "@next-auth/prisma-adapter": "^1.0.7", "@next/eslint-plugin-next": "^14.1.0", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-slot": "^1.0.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "install": "^0.13.0",