diff options
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/Sidebar.tsx | 145 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/ListItem.tsx | 189 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 46 | ||||
| -rw-r--r-- | apps/web/src/components/ui/drawer.tsx | 118 | ||||
| -rw-r--r-- | apps/web/src/components/ui/input.tsx | 18 | ||||
| -rw-r--r-- | apps/web/src/components/ui/popover.tsx | 31 | ||||
| -rw-r--r-- | apps/web/src/components/ui/textarea.tsx | 24 |
7 files changed, 417 insertions, 154 deletions
diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx deleted file mode 100644 index 0644d779..00000000 --- a/apps/web/src/components/Sidebar.tsx +++ /dev/null @@ -1,145 +0,0 @@ -"use client"; -import { StoredContent } from "@/server/db/schema"; -import { - Plus, - MoreHorizontal, - ArrowUpRight, - Edit3, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; - -import { useState, useEffect, useRef } from "react"; - -export default function Sidebar() { - const websties: StoredContent[] = [ - { - id: 1, - content: "", - title: "Visual Studio Code", - url: "https://code.visualstudio.com", - description: "", - image: "https://code.visualstudio.com/favicon.ico", - baseUrl: "https://code.visualstudio.com", - savedAt: new Date(), - }, - { - id: 1, - content: "", - title: "yxshv/vscode: An unofficial remake of vscode's landing page", - url: "https://github.com/yxshv/vscode", - description: "", - image: "https://github.com/favicon.ico", - baseUrl: "https://github.com", - savedAt: new Date(), - }, - ]; - - return ( - <aside className="bg-rgray-3 flex h-screen w-[25%] flex-col items-start justify-between py-5 pb-[50vh] font-light"> - <div className="flex items-center justify-center gap-1 px-5 text-xl font-normal"> - <img src="/brain.png" alt="logo" className="h-10 w-10" /> - SuperMemory - </div> - <div className="flex w-full flex-col items-start justify-center p-2"> - <h1 className="mb-1 flex w-full items-center justify-center px-3 font-normal"> - Websites - <button className="ml-auto "> - <Plus className="h-4 w-4 min-w-4" /> - </button> - </h1> - {websties.map((item) => ( - <ListItem key={item.id} item={item} /> - ))} - </div> - </aside> - ); -} - -export const ListItem: React.FC<{ item: StoredContent }> = ({ item }) => { - const [isEditing, setIsEditing] = useState(false); - const editInputRef = useRef<HTMLInputElement>(null); - - useEffect(() => { - if (isEditing) { - setTimeout(() => { - editInputRef.current?.focus(); - }, 500); - } - }, [isEditing]); - - return ( - <div className="hover:bg-rgray-5 focus-within:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>[data-upright-icon]]:block [&:hover>a>img]:hidden [&:hover>button]:opacity-100"> - <a - href={item.url} - target="_blank" - onClick={(e) => isEditing && e.preventDefault()} - className="flex w-[90%] items-center gap-2 focus:outline-none" - > - {isEditing ? ( - <Edit3 className="h-4 w-4" strokeWidth={1.5} /> - ) : ( - <> - <img - src={item.image ?? "/brain.png"} - alt={item.title ?? "Untitiled website"} - className="h-4 w-4" - /> - <ArrowUpRight - data-upright-icon - className="hidden h-4 w-4 min-w-4 scale-125" - strokeWidth={1.5} - /> - </> - )} - {isEditing ? ( - <input - ref={editInputRef} - autoFocus - className="text-rgray-12 w-full bg-transparent focus:outline-none" - placeholder={item.title ?? "Untitled website"} - onBlur={(e) => setIsEditing(false)} - onKeyDown={(e) => e.key === "Escape" && setIsEditing(false)} - /> - ) : ( - <span className="w-full truncate text-nowrap"> - {item.title ?? "Untitled website"} - </span> - )} - </a> - <DropdownMenu> - <DropdownMenuTrigger asChild> - <button className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus:opacity-100 focus:outline-none"> - <MoreHorizontal className="h-4 w-4 min-w-4" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent className="w-5"> - <DropdownMenuItem onClick={() => window.open(item.url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - <DropdownMenuItem - onClick={(e) => { - setIsEditing(true); - }} - > - <Edit3 className="mr-2 h-4 w-4 " strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10"> - <Trash2 className="mr-2 h-4 w-4 " strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> - </div> - ); -}; diff --git a/apps/web/src/components/Sidebar/ListItem.tsx b/apps/web/src/components/Sidebar/ListItem.tsx new file mode 100644 index 00000000..e2648e4b --- /dev/null +++ b/apps/web/src/components/Sidebar/ListItem.tsx @@ -0,0 +1,189 @@ +"use client"; +import { cleanUrl } from "@/lib/utils"; +import { StoredContent } from "@/server/db/schema"; +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "../ui/dropdown-menu"; +import { Label } from "../ui/label"; +import { + ArrowUpRight, + MoreHorizontal, + Edit3, + Trash2, + Save, + ChevronRight, + Plus, +} from "lucide-react"; +import { useState } from "react"; +import { + Drawer, + DrawerContent, + DrawerHeader, + DrawerTitle, + DrawerDescription, + DrawerFooter, + DrawerClose, +} from "../ui/drawer"; +import { Input } from "../ui/input"; +import { Textarea } from "../ui/textarea"; +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; + +export const ListItem: React.FC<{ item: StoredContent }> = ({ item }) => { + const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false); + + return ( + <div className="hover:bg-rgray-5 focus-within:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>div>[data-upright-icon]]:scale-125 [&:hover>a>div>[data-upright-icon]]:opacity-100 [&:hover>a>div>[data-upright-icon]]:delay-150 [&:hover>a>div>img]:scale-75 [&:hover>a>div>img]:opacity-0 [&:hover>a>div>img]:delay-0 [&:hover>button]:opacity-100"> + <a + href={item.url} + target="_blank" + className="flex w-[90%] items-center gap-2 focus-visible:outline-none" + > + <div className="relative h-4 min-w-4"> + <img + src={item.image ?? "/brain.png"} + alt={item.title ?? "Untitiled website"} + className="z-1 h-4 w-4 transition-[transform,opacity] delay-150 duration-150" + /> + <ArrowUpRight + data-upright-icon + className="absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150" + strokeWidth={1.5} + /> + </div> + + <span className="w-full truncate text-nowrap"> + {item.title ?? "Untitled website"} + </span> + </a> + <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> + <DropdownMenuTrigger asChild> + <button className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus-visible:opacity-100 focus-visible:outline-none"> + <MoreHorizontal className="h-4 w-4 min-w-4" /> + </button> + </DropdownMenuTrigger> + <DropdownMenuContent className="w-5"> + <DropdownMenuItem onClick={() => window.open(item.url)}> + <ArrowUpRight + className="mr-2 h-4 w-4 scale-125" + strokeWidth={1.5} + /> + Open + </DropdownMenuItem> + <DropdownMenuItem + onClick={() => { + setIsDropdownOpen(false); + setIsEditDrawerOpen(true); + }} + > + <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> + Edit + </DropdownMenuItem> + <DropdownMenuItem className="focus-visible:bg-red-100 focus-visible:text-red-400 dark:focus-visible:bg-red-100/10"> + <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> + Delete + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + <Drawer + shouldScaleBackground + open={isEditDrawerOpen} + onOpenChange={setIsEditDrawerOpen} + > + <DrawerContent className="pb-10 lg:px-[25vw]"> + <DrawerHeader className="relative mt-10 px-0"> + <DrawerTitle className=" flex w-full justify-between"> + Edit Page Details + </DrawerTitle> + <DrawerDescription>Change the page details</DrawerDescription> + <a + target="_blank" + href={item.url} + className="text-rgray-11/90 bg-rgray-3 text-md absolute right-0 top-0 flex w-min translate-y-1/2 items-center justify-center gap-1 rounded-full px-5 py-1" + > + <img src={item.image ?? "/brain.png"} className="h-4 w-4" /> + {cleanUrl(item.url)} + </a> + </DrawerHeader> + + <div className="mt-5"> + <Label>Title</Label> + <Input + className="" + required + value={item.title ?? ""} + placeholder={item.title ?? "Enter the title for the page"} + /> + </div> + <div className="mt-5"> + <Label>Additional Context</Label> + <Textarea + className="" + value={item.content ?? ""} + placeholder={"Enter additional context for this page"} + /> + </div> + <DrawerFooter className="flex flex-row-reverse items-center justify-end px-0 pt-5"> + <DrawerClose className="flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition hover:bg-blue-100 hover:text-blue-400 focus-visible:bg-blue-100 focus-visible:text-blue-400 focus-visible:outline-none focus-visible:ring-blue-200 dark:hover:bg-blue-100/10 dark:focus-visible:bg-blue-100/10 dark:focus-visible:ring-blue-200/30"> + <Save className="mr-2 h-4 w-4 " strokeWidth={1.5} /> + Save + </DrawerClose> + <DrawerClose className="hover:bg-rgray-3 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 flex items-center justify-center rounded-md px-3 py-2 ring-2 ring-transparent transition focus-visible:outline-none"> + Cancel + </DrawerClose> + <DrawerClose className="mr-auto flex items-center justify-center rounded-md bg-red-100 px-3 py-2 text-red-400 ring-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-red-200 dark:bg-red-100/10 dark:focus-visible:ring-red-200/30"> + <Trash2 className="mr-2 h-4 w-4 " strokeWidth={1.5} /> + Delete + </DrawerClose> + </DrawerFooter> + </DrawerContent> + </Drawer> + </div> + ); +}; + +export const AddNewPagePopover: React.FC<{ + addNewUrl?: (url: string) => Promise<void>; +}> = ({ addNewUrl }) => { + const [isOpen, setIsOpen] = useState(false); + const [url, setUrl] = useState(""); + + return ( + <Popover open={isOpen} onOpenChange={setIsOpen}> + <PopoverTrigger asChild> + <button className="focus-visible:ring-rgray-7 ring-offset-rgray-3 ml-auto rounded-sm ring-2 ring-transparent ring-offset-2 focus-visible:outline-none"> + <Plus className="h-4 w-4 min-w-4" /> + </button> + </PopoverTrigger> + <PopoverContent align="start" side="top"> + <h1 className="mb-2 flex items-center justify-between "> + Add a new page + <button + onClick={() => { + setIsOpen(false); + addNewUrl?.(url); + }} + className="hover:bg-rgray-3 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 ring-offset-rgray-3 rounded-sm ring-2 ring-transparent ring-offset-2 transition focus-visible:outline-none" + > + <ChevronRight className="h-4 w-4" /> + </button> + </h1> + <Input + className="w-full" + autoFocus + onChange={(e) => setUrl(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter") { + setIsOpen(false); + addNewUrl?.(url); + } + }} + placeholder="Enter the URL of the page" + /> + </PopoverContent> + </Popover> + ); +}; diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx new file mode 100644 index 00000000..0a658b77 --- /dev/null +++ b/apps/web/src/components/Sidebar/index.tsx @@ -0,0 +1,46 @@ +"use server"; +import { StoredContent } from "@/server/db/schema"; +import { AddNewPagePopover, ListItem } from "./ListItem"; + +export default async function Sidebar() { + const pages: StoredContent[] = [ + { + id: 1, + content: "", + title: "Visual Studio Code", + url: "https://code.visualstudio.com", + description: "", + image: "https://code.visualstudio.com/favicon.ico", + baseUrl: "https://code.visualstudio.com", + savedAt: new Date(), + }, + { + id: 2, + content: "", + title: "yxshv/vscode: An unofficial remake of vscode's landing page", + url: "https://github.com/yxshv/vscode", + description: "", + image: "https://github.com/favicon.ico", + baseUrl: "https://github.com", + savedAt: new Date(), + }, + ]; + + return ( + <aside className="bg-rgray-3 flex h-screen w-[25%] flex-col items-start justify-between py-5 pb-[50vh] font-light"> + <div className="flex items-center justify-center gap-1 px-5 text-xl font-normal"> + <img src="/brain.png" alt="logo" className="h-10 w-10" /> + SuperMemory + </div> + <div className="flex w-full flex-col items-start justify-center p-2"> + <h1 className="mb-1 flex w-full items-center justify-center px-3 font-normal"> + Pages + <AddNewPagePopover /> + </h1> + {pages.map((item) => ( + <ListItem key={item.id} item={item} /> + ))} + </div> + </aside> + ); +} diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx new file mode 100644 index 00000000..705ca01c --- /dev/null +++ b/apps/web/src/components/ui/drawer.tsx @@ -0,0 +1,118 @@ +"use client"; + +import * as React from "react"; +import { Drawer as DrawerPrimitive } from "vaul"; + +import { cn } from "@/lib/utils"; + +const Drawer = ({ + shouldScaleBackground = true, + ...props +}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( + <DrawerPrimitive.Root + shouldScaleBackground={shouldScaleBackground} + {...props} + /> +); +Drawer.displayName = "Drawer"; + +const DrawerTrigger = DrawerPrimitive.Trigger; + +const DrawerPortal = DrawerPrimitive.Portal; + +const DrawerClose = DrawerPrimitive.Close; + +const DrawerOverlay = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Overlay>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Overlay + ref={ref} + className={cn("fixed inset-0 z-50 bg-black/80", className)} + {...props} + /> +)); +DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; + +const DrawerContent = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> +>(({ className, children, ...props }, ref) => ( + <DrawerPortal> + <DrawerOverlay /> + <DrawerPrimitive.Content + ref={ref} + className={cn( + "border-rgray-6 bg-rgray-2 fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border", + className, + )} + {...props} + > + <div className="bg-rgray-4 mx-auto mt-4 h-2 w-[100px] rounded-full " /> + {children} + </DrawerPrimitive.Content> + </DrawerPortal> +)); +DrawerContent.displayName = "DrawerContent"; + +const DrawerHeader = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} + {...props} + /> +); +DrawerHeader.displayName = "DrawerHeader"; + +const DrawerFooter = ({ + className, + ...props +}: React.HTMLAttributes<HTMLDivElement>) => ( + <div + className={cn("mt-auto flex flex-col gap-2 p-4", className)} + {...props} + /> +); +DrawerFooter.displayName = "DrawerFooter"; + +const DrawerTitle = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Title>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Title + ref={ref} + className={cn( + "text-rgray-12 text-xl font-medium leading-none tracking-tight", + className, + )} + {...props} + /> +)); +DrawerTitle.displayName = DrawerPrimitive.Title.displayName; + +const DrawerDescription = React.forwardRef< + React.ElementRef<typeof DrawerPrimitive.Description>, + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> +>(({ className, ...props }, ref) => ( + <DrawerPrimitive.Description + ref={ref} + className={cn("text-rgray-11 text-md", className)} + {...props} + /> +)); +DrawerDescription.displayName = DrawerPrimitive.Description.displayName; + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +}; diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index aae15c80..8a7a9340 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -1,6 +1,6 @@ -import * as React from "react" +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} @@ -11,15 +11,15 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( <input type={type} className={cn( - "flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-800 dark:bg-gray-950 dark:ring-offset-gray-950 dark:placeholder:text-gray-400 dark:focus-visible:ring-gray-300", - className + "border-rgray-6 text-rgray-12 ring-offset-rgray-2 placeholder:text-rgray-11 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm transition 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" + ); + }, +); +Input.displayName = "Input"; -export { Input } +export { Input }; diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx new file mode 100644 index 00000000..0c4563a8 --- /dev/null +++ b/apps/web/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"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( + "border-rgray-6 bg-rgray-3 text-rgray-11 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/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx new file mode 100644 index 00000000..68d8e79e --- /dev/null +++ b/apps/web/src/components/ui/textarea.tsx @@ -0,0 +1,24 @@ +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-rgray-6 ring-offset-rgray-2 placeholder:text-rgray-11 focus-visible:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm transition 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 }; |