diff options
| author | yxshv <[email protected]> | 2024-04-11 16:37:46 +0530 |
|---|---|---|
| committer | yxshv <[email protected]> | 2024-04-11 16:37:46 +0530 |
| commit | 539f50367d2964579dbb6aa62876fab973b17840 (patch) | |
| tree | a071ab8c30d2448207bc68c92a57d5663dd73724 /apps/web/src/components/Sidebar | |
| parent | merge pls (diff) | |
| parent | Merge branch 'main' of https://github.com/Dhravya/supermemory (diff) | |
| download | supermemory-539f50367d2964579dbb6aa62876fab973b17840.tar.xz supermemory-539f50367d2964579dbb6aa62876fab973b17840.zip | |
Merge branch 'main' of https://github.com/dhravya/supermemory
Diffstat (limited to 'apps/web/src/components/Sidebar')
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 213 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 125 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 181 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 9 |
4 files changed, 428 insertions, 100 deletions
diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx new file mode 100644 index 00000000..886507ff --- /dev/null +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -0,0 +1,213 @@ +import { Editor } from "novel"; +import { + DialogClose, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { Markdown } from "tiptap-markdown"; +import { useEffect, useRef, useState } from "react"; +import { FilterSpaces } from "./FilterCombobox"; +import { useMemory } from "@/contexts/MemoryContext"; + +export function AddMemoryPage() { + const { addMemory } = useMemory(); + + const [url, setUrl] = useState(""); + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); + + return ( + <form className="md:w-[40vw]"> + <DialogHeader> + <DialogTitle>Add a web page to memory</DialogTitle> + <DialogDescription> + This will take you the web page you are trying to add to memory, where + the extension will save the page to memory + </DialogDescription> + </DialogHeader> + <Label className="mt-5 block">URL</Label> + <Input + placeholder="Enter the URL of the page" + type="url" + data-modal-autofocus + className="bg-rgray-4 mt-2 w-full" + value={url} + onChange={(e) => setUrl(e.target.value)} + /> + <DialogFooter> + <FilterSpaces + selectedSpaces={selectedSpacesId} + setSelectedSpaces={setSelectedSpacesId} + className="hover:bg-rgray-5 mr-auto bg-white/5" + name={"Spaces"} + /> + <button + type={"submit"} + onClick={async () => { + // @Dhravya this is adding a memory with insufficient information fix pls + await addMemory( + { + title: url, + content: "", + type: "page", + url: url, + image: "/icons/logo_without_bg.png", + savedAt: new Date(), + }, + selectedSpacesId, + ); + }} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + > + Add + </button> + <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> + Cancel + </DialogClose> + </DialogFooter> + </form> + ); +} + +export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); + + const inputRef = useRef<HTMLInputElement>(null); + const [name, setName] = useState(""); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + + function check(): boolean { + const data = { + name: name.trim(), + content, + }; + if (!data.name || data.name.length < 1) { + if (!inputRef.current) { + alert("Please enter a name for the note"); + return false; + } + inputRef.current.value = ""; + inputRef.current.placeholder = "Please enter a title for the note"; + inputRef.current.dataset["error"] = "true"; + setTimeout(() => { + inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.dataset["error"] = "false"; + }, 500); + inputRef.current.focus(); + return false; + } + return true; + } + + return ( + <div> + <Input + ref={inputRef} + data-error="false" + className="w-full border-none p-0 text-xl ring-0 placeholder:text-white/30 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" + placeholder="Title of the note" + data-modal-autofocus + value={name} + onChange={(e) => setName(e.target.value)} + /> + <Editor + disableLocalStorage + defaultValue={""} + onUpdate={(editor) => { + if (!editor) return; + setContent(editor.storage.markdown.getMarkdown()); + }} + extensions={[Markdown]} + className="novel-editor bg-rgray-4 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border [&>div>div]:p-5" + /> + <DialogFooter> + <FilterSpaces + selectedSpaces={selectedSpacesId} + setSelectedSpaces={setSelectedSpacesId} + className="hover:bg-rgray-5 mr-auto bg-white/5" + name={"Spaces"} + /> + <button + onClick={() => { + if (check()) { + closeDialog(); + } + }} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + > + Add + </button> + <DialogClose + type={undefined} + className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + > + Cancel + </DialogClose> + </DialogFooter> + </div> + ); +} + +export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { + const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); + + const inputRef = useRef<HTMLInputElement>(null); + const [name, setName] = useState(""); + const [content, setContent] = useState(""); + const [loading, setLoading] = useState(false); + + function check(): boolean { + const data = { + name: name.trim(), + content, + }; + console.log(name); + if (!data.name || data.name.length < 1) { + if (!inputRef.current) { + alert("Please enter a name for the note"); + return false; + } + inputRef.current.value = ""; + inputRef.current.placeholder = "Please enter a title for the note"; + inputRef.current.dataset["error"] = "true"; + setTimeout(() => { + inputRef.current!.placeholder = "Title of the note"; + inputRef.current!.dataset["error"] = "false"; + }, 500); + inputRef.current.focus(); + return false; + } + return true; + } + + return ( + <div className="md:w-[40vw]"> + <DialogHeader> + <DialogTitle>Add a space</DialogTitle> + </DialogHeader> + <Label className="mt-5 block">Name</Label> + <Input + placeholder="Enter the name of the space" + type="url" + data-modal-autofocus + className="bg-rgray-4 mt-2 w-full" + /> + <Label className="mt-5 block">Memories</Label> + <DialogFooter> + <DialogClose + type={undefined} + className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2" + > + Add + </DialogClose> + <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> + Cancel + </DialogClose> + </DialogFooter> + </div> + ); +} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx index a8e3a1e5..0a93ee55 100644 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx @@ -30,19 +30,137 @@ export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { setSelectedSpaces: ( spaces: number[] | ((prev: number[]) => number[]), ) => void; + name: string; } -export function FilterCombobox({ +export function FilterSpaces({ className, side = "bottom", align = "center", onClose, selectedSpaces, setSelectedSpaces, + name, ...props }: Props) { - const { spaces, addSpace } = useMemory(); + const { spaces } = useMemory(); + const [open, setOpen] = React.useState(false); + + const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => + selectedSpaces.includes(a) && !selectedSpaces.includes(b) + ? -1 + : selectedSpaces.includes(b) && !selectedSpaces.includes(a) + ? 1 + : 0, + ); + + React.useEffect(() => { + if (!open) { + onClose?.(); + } + }, [open]); + return ( + <AnimatePresence mode="popLayout"> + <LayoutGroup> + <Popover open={open} onOpenChange={setOpen}> + <PopoverTrigger asChild> + <button + type={undefined} + 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" /> + {name} + <ChevronsUpDown className="h-4 w-4" /> + <div + data-state-on={selectedSpaces.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]" + > + {selectedSpaces.length} + </div> + </button> + </PopoverTrigger> + <PopoverContent + onCloseAutoFocus={(e) => e.preventDefault()} + align={align} + side={side} + className="w-[200px] p-0" + > + <Command + filter={(val, search) => + spaces + .find((s) => s.id.toString() === val) + ?.title.toLowerCase() + .includes(search.toLowerCase().trim()) + ? 1 + : 0 + } + > + <CommandInput placeholder="Filter spaces..." /> + <CommandList asChild> + <motion.div layoutScroll> + <CommandEmpty>Nothing found</CommandEmpty> + <CommandGroup> + {sortedSpaces.map((space) => ( + <CommandItem + key={space.id} + value={space.id.toString()} + onSelect={(val) => { + setSelectedSpaces((prev: number[]) => + prev.includes(parseInt(val)) + ? prev.filter((v) => v !== parseInt(val)) + : [...prev, parseInt(val)], + ); + }} + asChild + > + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1, transition: { delay: 0.05 } }} + transition={{ duration: 0.15 }} + layout + layoutId={`space-combobox-${space.id}`} + className="text-rgray-11" + > + <SpaceIcon className="mr-2 h-4 w-4" /> + {space.title} + {selectedSpaces.includes(space.id)} + <Check + data-state-on={selectedSpaces.includes(space.id)} + className={cn( + "on:opacity-100 ml-auto h-4 w-4 opacity-0", + )} + /> + </motion.div> + </CommandItem> + ))} + </CommandGroup> + </motion.div> + </CommandList> + </Command> + </PopoverContent> + </Popover> + </LayoutGroup> + </AnimatePresence> + ); +} + +export function FilterMemories({ + className, + side = "bottom", + align = "center", + onClose, + selectedSpaces, + setSelectedSpaces, + name, + ...props +}: Props) { + const { spaces } = useMemory(); const [open, setOpen] = React.useState(false); const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => @@ -65,6 +183,7 @@ export function FilterCombobox({ <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild> <button + type={undefined} 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", @@ -73,7 +192,7 @@ export function FilterCombobox({ {...props} > <SpaceIcon className="mr-1 h-5 w-5" /> - Filter + {name} <ChevronsUpDown className="h-4 w-4" /> <div data-state-on={selectedSpaces.length > 0} diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index d7d8b5b5..66c3138b 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -1,3 +1,4 @@ +import { Editor } from "novel"; import { useAutoAnimate } from "@formkit/auto-animate/react"; import { MemoryWithImage, @@ -22,7 +23,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Variant, useAnimate, motion } from "framer-motion"; import { useMemory } from "@/contexts/MemoryContext"; import { SpaceIcon } from "@/assets/Memories"; @@ -38,6 +39,8 @@ import { import { Label } from "../ui/label"; import useViewport from "@/hooks/useViewport"; import useTouchHold from "@/hooks/useTouchHold"; +import { DialogTrigger } from "@radix-ui/react-dialog"; +import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); @@ -59,38 +62,49 @@ export function MemoriesBar() { /> </div> <div className="mt-2 flex w-full px-8"> - <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> - <DropdownMenuTrigger asChild> - <button className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> - <Plus className="mr-2 h-5 w-5" /> - Add - </button> - </DropdownMenuTrigger> - <DropdownMenuContent> - <DropdownMenuItem - onClick={() => { - setIsDropdownOpen(false); - setAddMemoryState("page"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Page to Memory - </DropdownMenuItem> - <DropdownMenuItem> - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - <DropdownMenuItem> - <SpaceIcon className="mr-2 h-4 w-4" /> - Space - </DropdownMenuItem> - </DropdownMenuContent> - </DropdownMenu> + <AddMemoryModal type={addMemoryState}> + <DropdownMenu open={isDropdownOpen} onOpenChange={setIsDropdownOpen}> + <DropdownMenuTrigger asChild> + <button className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 hover:bg-rgray-4 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition focus-visible:outline-none focus-visible:ring-2"> + <Plus className="mr-2 h-5 w-5" /> + Add + </button> + </DropdownMenuTrigger> + <DropdownMenuContent onCloseAutoFocus={(e) => e.preventDefault()}> + <DialogTrigger className="block w-full"> + <DropdownMenuItem + onClick={() => { + setAddMemoryState("page"); + }} + > + <Sparkles className="mr-2 h-4 w-4" /> + Page to Memory + </DropdownMenuItem> + </DialogTrigger> + <DialogTrigger className="block w-full"> + <DropdownMenuItem + onClick={() => { + setAddMemoryState("note"); + }} + > + <Text className="mr-2 h-4 w-4" /> + Note + </DropdownMenuItem> + </DialogTrigger> + <DialogTrigger className="block w-full"> + <DropdownMenuItem + onClick={() => { + setAddMemoryState("space"); + }} + > + <SpaceIcon className="mr-2 h-4 w-4" /> + Space + </DropdownMenuItem> + </DialogTrigger> + </DropdownMenuContent> + </DropdownMenu> + </AddMemoryModal> </div> - <AddMemoryModal - state={addMemoryState} - onStateChange={setAddMemoryState} - /> <div ref={parent} className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" @@ -295,69 +309,52 @@ export function SpaceMoreButton({ } export function AddMemoryModal({ - state, - onStateChange, + type, + children, }: { - state: "page" | "note" | "space" | null; - onStateChange: (state: "page" | "note" | "space" | null) => void; + type: "page" | "note" | "space" | null; + children?: React.ReactNode | React.ReactNode[]; }) { + const [isDialogOpen, setIsDialogOpen] = useState(false); + return ( - <> - <Dialog - open={state === "page"} - onOpenChange={(open) => onStateChange(open ? "page" : null)} + <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> + {children} + <DialogContent + onOpenAutoFocus={(e) => { + e.preventDefault(); + const novel = document.querySelector('[contenteditable="true"]') as + | HTMLDivElement + | undefined; + if (novel) { + novel.autofocus = false; + novel.onfocus = () => { + ( + document.querySelector("[data-modal-autofocus]") as + | HTMLInputElement + | undefined + )?.focus(); + novel.onfocus = null; + }; + } + ( + document.querySelector("[data-modal-autofocus]") as + | HTMLInputElement + | undefined + )?.focus(); + }} + className="w-max max-w-[auto]" > - <DialogContent> - <DialogHeader> - <DialogTitle>Add a web page to memory</DialogTitle> - <DialogDescription> - This will take you the web page you are trying to add to memory, - where the extension will save the page to memory - </DialogDescription> - </DialogHeader> - <Label className="mt-5">URL</Label> - <Input - autoFocus - placeholder="Enter the URL of the page" - type="url" - className="bg-rgray-4 mt-2 w-full" - /> - <DialogFooter> - <DialogClose className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> - Add - </DialogClose> - <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - <Dialog open={state === "note"}> - <DialogContent> - <DialogHeader> - <DialogTitle>Add a web page to memory</DialogTitle> - <DialogDescription> - This will take you the web page you are trying to add to memory, - where the extension will save the page to memory - </DialogDescription> - </DialogHeader> - <Label className="mt-5">URL</Label> - <Input - autoFocus - placeholder="Enter the URL of the page" - type="url" - className="bg-rgray-4 mt-2 w-full" - /> - <DialogFooter> - <DialogClose className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> - Add - </DialogClose> - <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - </> + {type === "page" ? ( + <AddMemoryPage /> + ) : type === "note" ? ( + <NoteAddPage closeDialog={() => setIsDialogOpen(false)} /> + ) : type === "space" ? ( + <SpaceAddPage closeDialog={() => setIsDialogOpen(false)} /> + ) : ( + <></> + )} + </DialogContent> + </Dialog> ); } diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 965455e6..1487e113 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -13,6 +13,7 @@ export type MenuItem = { icon: React.ReactNode | React.ReactNode[]; label: string; content?: React.ReactNode; + labelDisplay?: React.ReactNode; }; export default function Sidebar({ @@ -73,7 +74,7 @@ export default function Sidebar({ return ( <> <div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex"> - <div className="bg-rgray-2 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 "> + <div className="bg-rgray-3 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 "> <MenuItem item={{ label: "Memories", @@ -83,9 +84,7 @@ export default function Sidebar({ selectedItem={selectedItem} setSelectedItem={setSelectedItem} /> - <div className="mt-auto" /> - <MenuItem item={{ label: "Trash", @@ -131,7 +130,7 @@ export default function Sidebar({ } const MenuItem = ({ - item: { icon, label }, + item: { icon, label, labelDisplay }, selectedItem, setSelectedItem, ...props @@ -147,7 +146,7 @@ const MenuItem = ({ {...props} > {icon} - <span className="">{label}</span> + <span className="">{labelDisplay ?? label}</span> </button> ); |