supermemory/apps/web/src/components/Sidebar/AddMemoryDialog.tsx
2024-04-14 14:29:23 +05:30

352 lines
12 KiB
TypeScript

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 { FilterMemories, FilterSpaces } from "./FilterCombobox";
import { useMemory } from "@/contexts/MemoryContext";
import { Loader, Plus, X } from "lucide-react";
import { StoredContent } from "@/server/db/schema";
import { cleanUrl } from "@/lib/utils";
import { motion } from "framer-motion";
import { getMetaData } from "@/server/helpers";
export function AddMemoryPage({ closeDialog }: { closeDialog: () => void }) {
const { addMemory } = useMemory();
const [loading, setLoading] = useState(false);
const [url, setUrl] = useState("");
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
return (
<div 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 disabled:cursor-not-allowed disabled:opacity-70"
value={url}
onChange={(e) => setUrl(e.target.value)}
disabled={loading}
/>
<DialogFooter>
<FilterSpaces
selectedSpaces={selectedSpacesId}
setSelectedSpaces={setSelectedSpacesId}
className="hover:bg-rgray-5 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
name={"Spaces"}
disabled={loading}
/>
<button
type={"submit"}
disabled={loading}
onClick={async () => {
setLoading(true);
const metadata = await getMetaData(url);
await addMemory(
{
title: metadata.title,
description: metadata.description,
content: "",
type: "page",
url: url,
image: metadata.image,
savedAt: new Date(),
},
selectedSpacesId,
);
closeDialog();
}}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
>
<Loader className="text-rgray-11 h-5 w-5 animate-spin" />
</motion.div>
<motion.div
initial={{ y: "0%" }}
animate={loading && { opacity: 0, y: "30%" }}
>
Add
</motion.div>
</button>
<DialogClose
disabled={loading}
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 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
</DialogFooter>
</div>
);
}
export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
const { addMemory } = useMemory();
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}
disabled={loading}
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()) {
setLoading(true);
addMemory(
{
content,
title: name,
type: "note",
url: `https://notes.supermemory.dhr.wtf/`,
image: "",
savedAt: new Date(),
},
selectedSpacesId,
).then(closeDialog);
}
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
>
<Loader className="text-rgray-11 h-5 w-5 animate-spin" />
</motion.div>
<motion.div
initial={{ y: "0%" }}
animate={loading && { opacity: 0, y: "30%" }}
>
Add
</motion.div>
</button>
<DialogClose
type={undefined}
disabled={loading}
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 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
</DialogFooter>
</div>
);
}
export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
const { addSpace } = useMemory();
const inputRef = useRef<HTMLInputElement>(null);
const [name, setName] = useState("");
const [loading, setLoading] = useState(false);
const [selected, setSelected] = useState<StoredContent[]>([]);
function check(): boolean {
const data = {
name: name.trim(),
};
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 space";
inputRef.current.dataset["error"] = "true";
setTimeout(() => {
inputRef.current!.placeholder = "Enter the name of the space";
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
ref={inputRef}
placeholder="Enter the name of the space"
type="url"
data-modal-autofocus
value={name}
disabled={loading}
onChange={(e) => setName(e.target.value)}
className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10"
/>
{selected.length > 0 && (
<>
<Label className="mt-5 block">Add Memories</Label>
<div className="flex min-h-5 flex-col items-center justify-center py-2">
{selected.map((i) => (
<MemorySelectedItem
key={i.id}
onRemove={() =>
setSelected((prev) => prev.filter((p) => p.id !== i.id))
}
{...i}
/>
))}
</div>
</>
)}
<DialogFooter>
<FilterMemories
selected={selected}
setSelected={setSelected}
disabled={loading}
className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
>
<Plus className="h-5 w-5" />
Memory
</FilterMemories>
<button
type={undefined}
onClick={() => {
if (check()) {
setLoading(true);
addSpace(
name,
selected.map((s) => s.id),
).then(() => closeDialog());
}
}}
disabled={loading}
className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
<motion.div
initial={{ x: "-50%", y: "-100%" }}
animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
>
<Loader className="text-rgray-11 h-5 w-5 animate-spin" />
</motion.div>
<motion.div
initial={{ y: "0%" }}
animate={loading && { opacity: 0, y: "30%" }}
>
Add
</motion.div>
</button>
<DialogClose
disabled={loading}
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 disabled:cursor-not-allowed disabled:opacity-70"
>
Cancel
</DialogClose>
</DialogFooter>
</div>
);
}
export function MemorySelectedItem({
id,
title,
url,
type,
image,
onRemove,
}: StoredContent & { onRemove: () => void }) {
return (
<div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden">
<img
src={
type === "note" ? "/note.svg" : image ?? "/icons/logo_without_bg.png"
}
className="h-5 w-5"
/>
<button
onClick={onRemove}
data-icon
className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none"
>
<X className="h-5 w-5 scale-90" />
</button>
<span>{title}</span>
<span className="ml-auto block opacity-50">
{type === "note" ? "Note" : cleanUrl(url)}
</span>
</div>
);
}