aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components/Sidebar
diff options
context:
space:
mode:
authoryxshv <[email protected]>2024-04-11 16:37:46 +0530
committeryxshv <[email protected]>2024-04-11 16:37:46 +0530
commit539f50367d2964579dbb6aa62876fab973b17840 (patch)
treea071ab8c30d2448207bc68c92a57d5663dd73724 /apps/web/src/components/Sidebar
parentmerge pls (diff)
parentMerge branch 'main' of https://github.com/Dhravya/supermemory (diff)
downloadsupermemory-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.tsx213
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx125
-rw-r--r--apps/web/src/components/Sidebar/MemoriesBar.tsx181
-rw-r--r--apps/web/src/components/Sidebar/index.tsx9
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>
);