diff options
| author | Dhravya <[email protected]> | 2024-04-03 00:24:05 -0700 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-04-03 00:24:05 -0700 |
| commit | 20febf888b6ca42fba462390f723573807aef502 (patch) | |
| tree | a7e157c07d3868a4e42135a91aeb8606781251db /apps/web/src/components | |
| parent | update: catch up with main (diff) | |
| parent | responsiveness (diff) | |
| download | supermemory-20febf888b6ca42fba462390f723573807aef502.tar.xz supermemory-20febf888b6ca42fba462390f723573807aef502.zip | |
uptodate with main
Diffstat (limited to 'apps/web/src/components')
| -rw-r--r-- | apps/web/src/components/Main.tsx | 56 | ||||
| -rw-r--r-- | apps/web/src/components/MemoryDrawer.tsx | 36 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/CategoryItem.tsx | 307 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 109 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 147 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/PagesItem.tsx | 4 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 135 | ||||
| -rw-r--r-- | apps/web/src/components/ui/command.tsx | 155 | ||||
| -rw-r--r-- | apps/web/src/components/ui/dialog.tsx | 122 | ||||
| -rw-r--r-- | apps/web/src/components/ui/drawer.tsx | 14 | ||||
| -rw-r--r-- | apps/web/src/components/ui/input.tsx | 34 | ||||
| -rw-r--r-- | apps/web/src/components/ui/textarea.tsx | 35 |
12 files changed, 1104 insertions, 50 deletions
diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx new file mode 100644 index 00000000..b6ad3787 --- /dev/null +++ b/apps/web/src/components/Main.tsx @@ -0,0 +1,56 @@ +"use client"; +import { useEffect, useRef, useState } from "react"; +import { FilterCombobox } from "./Sidebar/FilterCombobox"; +import { Textarea2 } from "./ui/textarea"; +import { ArrowRight } from "lucide-react"; +import { MemoryDrawer } from "./MemoryDrawer"; +import useViewport from "@/hooks/useViewport"; + +export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { + const [value, setValue] = useState(""); + const { width } = useViewport(); + + const textArea = useRef<HTMLTextAreaElement>(null); + + useEffect(() => { + function onResize() { + if (!textArea.current || !window.visualViewport) return; + + const visualViewportHeight = window.visualViewport.height; + } + + window.visualViewport?.addEventListener("resize", onResize); + return () => window.visualViewport?.removeEventListener("resize", onResize); + }, []); + + return ( + <main + data-sidebar-open={sidebarOpen} + className="flex h-screen max-h-screen w-full items-end justify-center px-5 pb-[20vh] pt-5 md:items-center md:px-60 md:[&[data-sidebar-open='true']]:px-20" + > + <Textarea2 + ref={textArea} + className="h-max max-h-[30em] min-h-[3em] resize-y flex-row items-start justify-center overflow-auto py-5 md:h-[20vh] md:resize-none md:flex-col md:items-center md:justify-center md:p-2 md:pb-2 md:pt-2" + textAreaProps={{ + placeholder: "Ask your SuperMemory...", + className: + "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", + value, + autoFocus: true, + onChange: (e) => setValue(e.target.value), + }} + > + <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> + <FilterCombobox className="hidden md:flex" /> + <button + disabled={value.trim().length < 1} + className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" + > + <ArrowRight className="h-5 w-5" /> + </button> + </div> + </Textarea2> + {width <= 768 && <MemoryDrawer />} + </main> + ); +} diff --git a/apps/web/src/components/MemoryDrawer.tsx b/apps/web/src/components/MemoryDrawer.tsx new file mode 100644 index 00000000..f9d7d6c4 --- /dev/null +++ b/apps/web/src/components/MemoryDrawer.tsx @@ -0,0 +1,36 @@ +import { useState } from "react"; +import { Drawer, DrawerContent, DrawerOverlay } from "./ui/drawer"; +import { MemoryIcon } from "@/assets/Memories"; + +export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {} + +export function MemoryDrawer({ className, ...props }: Props) { + const [activeSnapPoint, setActiveSnapPoint] = useState< + number | null | string + >(0.1); + + return ( + <Drawer + snapPoints={[0.1, 0.9]} + activeSnapPoint={activeSnapPoint} + shouldScaleBackground={false} + setActiveSnapPoint={setActiveSnapPoint} + open={true} + dismissible={false} + modal={false} + > + <DrawerContent + overlay={false} + className="border-rgray-6 h-full w-screen border-2 pt-4 focus-visible:outline-none" + handle={false} + > + <div className="bg-rgray-4 border-rgray-6 text-rgray-11 absolute left-1/2 top-0 flex w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-2 rounded-md border-2 px-3 py-2"> + <MemoryIcon className="h-7 w-7" /> + Memories + </div> + Hello + </DrawerContent> + <DrawerOverlay className="relative bg-transparent" /> + </Drawer> + ); +} diff --git a/apps/web/src/components/Sidebar/CategoryItem.tsx b/apps/web/src/components/Sidebar/CategoryItem.tsx new file mode 100644 index 00000000..c2e72ba5 --- /dev/null +++ b/apps/web/src/components/Sidebar/CategoryItem.tsx @@ -0,0 +1,307 @@ +"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, + Tags, + ChevronDown, + Edit3, + Trash2, + Save, + ChevronRight, + Plus, + Minus, +} 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"; +import { + AnimatePresence, + motion, + Reorder, + useMotionValue, +} from "framer-motion"; + +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(), + space: "" + }, + { + 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(), + space: "" + }, + { + id: 3, + 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(), + space: "" + }, + { + id: 4, + 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(), + space: "" + }, + { + id: 5, + 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(), + space: "" + }, + { + id: 6, + 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(), + space: "" + }, + { + id: 7, + 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(), + space: "" + }, + { + id: 8, + 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(), + space: "" + }, + { + id: 9, + 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(), + space: "" + }, +]; +export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => { + const [isExpanded, setIsExpanded] = useState(false); + const [isEditDrawerOpen, setIsEditDrawerOpen] = useState(false); + + const [items, setItems] = useState<StoredContent[]>(pages); + + return ( + <> + <div className="hover:bg-rgray-5 has-[button:focus]:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>button>div>[data-down-icon]]:scale-125 [&:hover>button>div>[data-down-icon]]:opacity-100 [&:hover>button>div>[data-down-icon]]:delay-150 [&:hover>button>div>[data-tags-icon]]:scale-75 [&:hover>button>div>[data-tags-icon]]:opacity-0 [&:hover>button>div>[data-tags-icon]]:delay-0 [&:hover>button]:opacity-100"> + <button + onClick={() => setIsExpanded((prev) => !prev)} + className="flex w-full items-center gap-2 focus-visible:outline-none" + > + <div className="relative h-5 min-w-5"> + <Tags + data-tags-icon + className="z-1 h-5 w-5 transition-[transform,opacity] delay-150 duration-150" + strokeWidth={1.5} + /> + <ChevronDown + data-down-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 ${isExpanded ? "rotate-180" : "rotate-0"}`} + strokeWidth={1.5} + /> + </div> + + <span className="w-full truncate text-nowrap text-left"> + {item.title ?? "Untitled website"} + </span> + </button> + <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> + <AnimatePresence> + {isExpanded && ( + <Reorder.Group + axis="y" + values={items} + onReorder={setItems} + as="div" + initial={{ height: 0 }} + animate={{ height: "auto" }} + exit={{ + height: 0, + transition: {}, + }} + layoutScroll + className="flex max-h-32 w-full flex-col items-center overflow-y-auto pl-7" + > + <AnimatePresence> + {items.map((item, i) => ( + <CategoryPage + key={item.id} + index={i} + item={item} + onRemove={() => + setItems((prev) => prev.filter((_, index) => i !== index)) + } + /> + ))} + </AnimatePresence> + </Reorder.Group> + )} + </AnimatePresence> + </> + ); +}; + +export const CategoryPage: React.FC<{ + item: StoredContent; + index: number; + onRemove?: () => void; +}> = ({ item, onRemove, index }) => { + return ( + <Reorder.Item + value={item} + as="div" + key={index} + exit={{ opacity: 0, scale: 0.8 }} + dragListener={false} + className="hover:bg-rgray-5 has-[a:focus]:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>div>[data-icon]]:scale-125 [&:hover>a>div>[data-icon]]:opacity-100 [&:hover>a>div>[data-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-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> + <button + onClick={() => onRemove?.()} + className="ml-auto w-4 min-w-4 rounded-[0.15rem] opacity-0 focus-visible:opacity-100 focus-visible:outline-none" + > + <Minus className="h-4 w-4 min-w-4" /> + </button> + </Reorder.Item> + ); +}; diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx new file mode 100644 index 00000000..ade54711 --- /dev/null +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -0,0 +1,109 @@ +"use client"; + +import * as React from "react"; +import { Check, ChevronsUpDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { SpaceIcon } from "@/assets/Memories"; + +const spaces = [ + { + value: "1", + label: "Cool Tech", + }, + { + value: "2", + label: "Cool Courses", + }, + { + value: "3", + label: "Cool Libraries", + }, +]; + +export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {} + +export function FilterCombobox({ className, ...props }: Props) { + const [open, setOpen] = React.useState(false); + const [values, setValues] = React.useState<string[]>([]); + + return ( + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <button + data-state-on={open} + className={cn( + "text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none", + className, + )} + {...props} + > + <SpaceIcon className="mr-1 h-5 w-5" /> + Filter + <ChevronsUpDown className="h-4 w-4" /> + <div + data-state-on={values.length > 0} + className="on:flex text-rgray-11 border-rgray-6 bg-rgray-2 absolute left-0 top-0 hidden aspect-[1] h-4 w-4 -translate-x-1/3 -translate-y-1/3 items-center justify-center rounded-full border text-center text-[9px]" + > + {values.length} + </div> + </button> + </PopoverTrigger> + <PopoverContent className="w-[200px] p-0"> + <Command + filter={(val, search) => + spaces + .find((s) => s.value === val) + ?.label.toLowerCase() + .includes(search.toLowerCase().trim()) + ? 1 + : 0 + } + > + <CommandInput placeholder="Filter spaces..." /> + <CommandList> + <CommandEmpty>Nothing found</CommandEmpty> + {/* bug: doesn't work on clicking with mouse only keyboard, weird */} + <CommandGroup> + {spaces.map((space) => ( + <CommandItem + key={space.value} + value={space.value} + onSelect={(val) => { + setValues((prev) => + prev.includes(val) + ? prev.filter((v) => v !== val) + : [...prev, val], + ); + }} + > + <SpaceIcon className="mr-2 h-4 w-4" /> + {space.label} + {values.includes(space.value)} + <Check + data-state-on={values.includes(space.value)} + className={cn("on:opacity-100 ml-auto h-4 w-4 opacity-0")} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + ); +} diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx new file mode 100644 index 00000000..72ee6d42 --- /dev/null +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -0,0 +1,147 @@ +import { + MemoryWithImage, + MemoryWithImages3, + MemoryWithImages2, +} from '@/assets/MemoryWithImages'; +import { type Space } from '../../../types/memory'; +import { InputWithIcon } from '../ui/input'; +import { Search } from 'lucide-react'; + +export function MemoriesBar() { + const spaces: Space[] = [ + { + id: 1, + title: 'Cool Tech', + description: 'Really cool mind blowing tech', + content: [ + { + id: 1, + title: 'Perplexity', + description: 'A good ui', + content: '', + image: 'https://perplexity.ai/favicon.ico', + url: 'https://perplexity.ai', + savedAt: new Date(), + baseUrl: 'https://perplexity.ai', + space: 'Cool tech', + }, + { + id: 2, + title: 'Pi.ai', + description: 'A good ui', + content: '', + image: 'https://pi.ai/pi-logo-192.png?v=2', + url: 'https://pi.ai', + savedAt: new Date(), + baseUrl: 'https://pi.ai', + space: 'Cool tech', + }, + { + id: 3, + title: 'Visual Studio Code', + description: 'A good ui', + content: '', + image: 'https://code.visualstudio.com/favicon.ico', + url: 'https://code.visualstudio.com', + savedAt: new Date(), + baseUrl: 'https://code.visualstudio.com', + space: 'Cool tech', + }, + ], + }, + { + id: 2, + title: 'Cool Courses', + description: 'Amazng', + content: [ + { + id: 1, + title: 'Animation on the web', + description: 'A good ui', + content: '', + image: 'https://animations.dev/favicon.ico', + url: 'https://animations.dev', + savedAt: new Date(), + baseUrl: 'https://animations.dev', + space: 'Cool courses', + }, + { + id: 2, + title: 'Tailwind Course', + description: 'A good ui', + content: '', + image: + 'https://tailwindcss.com/_next/static/media/tailwindcss-mark.3c5441fc7a190fb1800d4a5c7f07ba4b1345a9c8.svg', + url: 'https://tailwindcss.com', + savedAt: new Date(), + baseUrl: 'https://tailwindcss.com', + space: 'Cool courses', + }, + ], + }, + { + id: 3, + title: 'Cool Libraries', + description: 'Really cool mind blowing tech', + content: [ + { + id: 1, + title: 'Perplexity', + description: 'A good ui', + content: '', + image: 'https://yashverma.me/logo.jpg', + url: 'https://perplexity.ai', + savedAt: new Date(), + baseUrl: 'https://perplexity.ai', + space: 'Cool libraries', + }, + ], + }, + ]; + + return ( + <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> + <div className="w-full px-8"> + <h1 className="w-full text-2xl">Your Memories</h1> + <InputWithIcon + placeholder="Search" + icon={<Search className="text-rgray-11 h-5 w-5 opacity-50" />} + className="bg-rgray-4 mt-2 w-full" + /> + </div> + <div className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"> + {spaces.map((space) => ( + <Space key={space.id} {...space} /> + ))} + </div> + </div> + ); +} + +export function Space({ title, description, content, id }: Space) { + console.log(title, content.map((c) => c.image).reverse()); + return ( + <button className="hover:bg-rgray-2 focus-visible:bg-rgray-2 focus-visible:ring-rgray-7 flex flex-col items-center justify-center rounded-md p-2 text-center ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> + {content.length > 2 ? ( + <MemoryWithImages3 + className="h-24 w-24" + id={id.toString()} + images={content.map((c) => c.image).reverse() as string[]} + /> + ) : content.length === 1 ? ( + <MemoryWithImage + className="h-24 w-24" + id={id.toString()} + image={content[0].image!} + /> + ) : ( + <MemoryWithImages2 + className="h-24 w-24" + id={id.toString()} + images={content.map((c) => c.image).reverse() as string[]} + /> + )} + <span>{title}</span> + </button> + ); +} diff --git a/apps/web/src/components/Sidebar/PagesItem.tsx b/apps/web/src/components/Sidebar/PagesItem.tsx index ce762ae5..fea8bf33 100644 --- a/apps/web/src/components/Sidebar/PagesItem.tsx +++ b/apps/web/src/components/Sidebar/PagesItem.tsx @@ -36,7 +36,7 @@ export const PageItem: React.FC<{ item: StoredContent }> = ({ item }) => { 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"> + <div className="hover:bg-rgray-5 has-[a:focus]:bg-rgray-5 flex w-full items-center rounded-full py-1 pl-3 pr-2 transition [&:hover>a>div>[data-icon]]:scale-125 [&:hover>a>div>[data-icon]]:opacity-100 [&:hover>a>div>[data-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" @@ -49,7 +49,7 @@ export const PageItem: React.FC<{ item: StoredContent }> = ({ item }) => { className="z-1 h-4 w-4 transition-[transform,opacity] delay-150 duration-150" /> <ArrowUpRight - data-upright-icon + data-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} /> diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 80d4beb5..1680000b 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -1,46 +1,103 @@ -"use server"; +"use client"; import { StoredContent } from "@/server/db/schema"; -import { AddNewPagePopover, PageItem } from "./PagesItem"; - -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(), - }, - ]; +import { MemoryIcon } from "../../assets/Memories"; +import { Trash2, User2 } from "lucide-react"; +import React, { useState } from "react"; +import { MemoriesBar } from "./MemoriesBar"; + +export type MenuItem = { + icon: React.ReactNode | React.ReactNode[]; + label: string; + content?: React.FC; +}; + +const menuItemsTop: Array<MenuItem> = [ + { + icon: <MemoryIcon className="h-10 w-10" />, + label: "Memories", + content: MemoriesBar, + }, +]; + +const menuItemsBottom: Array<MenuItem> = [ + { + icon: <Trash2 strokeWidth={1.3} className="h-6 w-6" />, + label: "Trash", + }, + { + icon: <User2 strokeWidth={1.3} className="h-6 w-6" />, + label: "Profile", + }, +]; + +export default function Sidebar({ + onSelectChange, +}: { + onSelectChange?: (selectedItem: string | null) => void; +}) { + const menuItems = [...menuItemsTop, ...menuItemsBottom]; + const [selectedItem, setSelectedItem] = useState<string | null>(null); + + React.useEffect(() => { + onSelectChange?.(selectedItem); + }, [selectedItem]); + + const Subbar = + menuItems.find((i) => i.label === selectedItem)?.content ?? (() => <></>); 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) => ( - <PageItem key={item.id} item={item} /> + <> + <div className="bg-rgray-2 border-r-rgray-6 hidden h-screen max-h-screen w-max flex-col items-center border-r px-2 py-5 text-sm font-light md:flex"> + {menuItemsTop.map((item, index) => ( + <MenuItem + key={index} + item={item} + selectedItem={selectedItem} + setSelectedItem={setSelectedItem} + /> + ))} + <div className="mt-auto" /> + {menuItemsBottom.map((item, index) => ( + <MenuItem + key={index} + item={item} + selectedItem={selectedItem} + setSelectedItem={setSelectedItem} + /> ))} </div> - </aside> + {selectedItem && ( + <SubSidebar> + <Subbar /> + </SubSidebar> + )} + </> + ); +} + +const MenuItem = ({ + item: { icon, label }, + selectedItem, + setSelectedItem, +}: { + item: MenuItem; + selectedItem: string | null; + setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>; +}) => ( + <button + data-state-on={selectedItem === label} + onClick={() => setSelectedItem((prev) => (prev === label ? null : label))} + className="on:opacity-100 on:bg-rgray-4 focus-visible:ring-rgray-7 flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" + > + {icon} + <span className="">{label}</span> + </button> +); + +export function SubSidebar({ children }: { children?: React.ReactNode }) { + return ( + <div className="bg-rgray-3 border-r-rgray-6 hidden h-screen w-[50vw] flex-col items-center border-r font-light md:flex"> + {children} + </div> ); } diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx new file mode 100644 index 00000000..54070776 --- /dev/null +++ b/apps/web/src/components/ui/command.tsx @@ -0,0 +1,155 @@ +"use client"; + +import * as React from "react"; +import { type DialogProps } from "@radix-ui/react-dialog"; +import { Command as CommandPrimitive } from "cmdk"; +import { Search } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +const Command = React.forwardRef< + React.ElementRef<typeof CommandPrimitive>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive> +>(({ className, ...props }, ref) => ( + <CommandPrimitive + ref={ref} + className={cn( + "bg-rgray-3 text-rgray-11 flex h-full w-full flex-col overflow-hidden rounded-md", + className, + )} + {...props} + /> +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +const CommandDialog = ({ children, ...props }: CommandDialogProps) => { + return ( + <Dialog {...props}> + <DialogContent className="overflow-hidden p-0 shadow-lg"> + <Command className="[&_[cmdk-group-heading]]:text-rgray-11 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> + {children} + </Command> + </DialogContent> + </Dialog> + ); +}; + +const CommandInput = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Input>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> +>(({ className, ...props }, ref) => ( + <div + className="border-rgray-6 flex items-center border-b px-3" + cmdk-input-wrapper="" + > + <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> + <CommandPrimitive.Input + ref={ref} + className={cn( + "placeholder:text-rgray-11/50 flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50", + className, + )} + {...props} + /> + </div> +)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.List>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.List + ref={ref} + className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} + {...props} + /> +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Empty>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> +>((props, ref) => ( + <CommandPrimitive.Empty + ref={ref} + className="py-6 text-center text-sm" + {...props} + /> +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Group>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Group + ref={ref} + className={cn( + "text-rgray-12 [&_[cmdk-group-heading]]:text-rgray-11 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", + className, + )} + {...props} + /> +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Separator>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Separator + ref={ref} + className={cn("bg-rgray-3 -mx-1 h-px", className)} + {...props} + /> +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef<typeof CommandPrimitive.Item>, + React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> +>(({ className, ...props }, ref) => ( + <CommandPrimitive.Item + ref={ref} + className={cn( + "aria-selected:bg-rgray-5 aria-selected:text-rgray-12 relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className, + )} + {...props} + /> +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn("text-gray-11 ml-auto text-xs tracking-widest", className)} + {...props} + /> + ); +}; +CommandShortcut.displayName = "CommandShortcut"; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx new file mode 100644 index 00000000..ec19b41a --- /dev/null +++ b/apps/web/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"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( + "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%] border-rgray-6 bg-rgray-3 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-rgray-2 focus:ring-rgray-7 data-[state=open]:bg-rgray-3 data-[state=open]:text-rgray-11 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="h-4 w-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-rgray-11 text-sm", className)} + {...props} + /> +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx index 705ca01c..28e8dbdf 100644 --- a/apps/web/src/components/ui/drawer.tsx +++ b/apps/web/src/components/ui/drawer.tsx @@ -28,6 +28,7 @@ const DrawerOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( <DrawerPrimitive.Overlay ref={ref} + data-drawer-overlay className={cn("fixed inset-0 z-50 bg-black/80", className)} {...props} /> @@ -36,10 +37,13 @@ DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; const DrawerContent = React.forwardRef< React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> ->(({ className, children, ...props }, ref) => ( + React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { + overlay?: boolean; + handle?: boolean; + } +>(({ className, children, overlay = true, handle = true, ...props }, ref) => ( <DrawerPortal> - <DrawerOverlay /> + {overlay && <DrawerOverlay />} <DrawerPrimitive.Content ref={ref} className={cn( @@ -48,7 +52,9 @@ const DrawerContent = React.forwardRef< )} {...props} > - <div className="bg-rgray-4 mx-auto mt-4 h-2 w-[100px] rounded-full " /> + {handle && ( + <div className="bg-rgray-4 mx-auto mt-4 h-2 w-[100px] rounded-full " /> + )} {children} </DrawerPrimitive.Content> </DrawerPortal> diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 8a7a9340..cee654a8 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( <input type={type} className={cn( - "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 ", + "border-rgray-6 text-rgray-11 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 disabled:cursor-not-allowed disabled:opacity-50 ", className, )} ref={ref} @@ -20,6 +20,34 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>( ); }, ); -Input.displayName = "Input"; -export { Input }; +export interface InputWithIconProps + extends React.InputHTMLAttributes<HTMLInputElement> { + icon: React.ReactNode; +} + +const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>( + ({ className, type, icon, ...props }, ref) => { + return ( + <div + className={cn( + "border-rgray-6 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm transition focus-within:outline-none focus-within:ring-2 ", + className, + )} + > + {icon} + <input + type={type} + className={ + "placeholder:text-rgray-11/50 w-full bg-transparent file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" + } + ref={ref} + {...props} + /> + </div> + ); + }, +); +InputWithIcon.displayName = "Input"; + +export { Input, InputWithIcon }; diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx index 68d8e79e..10a7ed75 100644 --- a/apps/web/src/components/ui/textarea.tsx +++ b/apps/web/src/components/ui/textarea.tsx @@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( 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", + "border-rgray-6 text-rgray-11 placeholder:text-rgray-11/70 focus-within:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 disabled:cursor-not-allowed disabled:opacity-50", className, )} ref={ref} @@ -21,4 +21,35 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( ); Textarea.displayName = "Textarea"; -export { Textarea }; +export interface Textarea2Props extends React.HTMLAttributes<HTMLDivElement> { + textAreaProps?: TextareaProps; +} + +const Textarea2 = React.forwardRef<HTMLTextAreaElement, Textarea2Props>( + ({ className, children, textAreaProps: _textAreaProps, ...props }, ref) => { + const { className: textAreaClassName, ...textAreaProps } = + _textAreaProps || {}; + return ( + <div + className={cn( + "border-rgray-6 text-rgray-11 focus-within:ring-rgray-7 flex h-auto min-h-[80px] w-full flex-col items-start justify-center rounded-md border bg-transparent px-3 py-2 text-sm transition focus-within:outline-none focus-within:ring-2", + className, + )} + {...props} + > + <textarea + className={cn( + "placeholder:text-rgray-11/70 h-full w-full resize-none bg-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", + textAreaClassName, + )} + ref={ref} + {...textAreaProps} + /> + {children} + </div> + ); + }, +); +Textarea2.displayName = "Textarea2"; + +export { Textarea, Textarea2 }; |