diff options
Diffstat (limited to 'apps/web/src/components/Sidebar')
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 480 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/DeleteConfirmation.tsx | 47 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/EditNoteDialog.tsx | 155 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/ExpandedSpace.tsx | 287 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/FilterCombobox.tsx | 303 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 709 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/SettingsTab.tsx | 99 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/index.tsx | 172 |
8 files changed, 0 insertions, 2252 deletions
diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx deleted file mode 100644 index 64147b1e..00000000 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ /dev/null @@ -1,480 +0,0 @@ -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, StoredSpace } from "@/server/db/schema"; -import { cleanUrl } from "@/lib/utils"; -import { motion } from "framer-motion"; -import { getMetaData } from "@/server/helpers"; - -export function AddMemoryPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [loading, setLoading] = useState(false); - const [url, setUrl] = useState(""); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - return ( - <div className="w-[80vw] max-w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add a web page to memory</DialogTitle> - <DialogDescription> - This will fetch the content of the web page and add it to the memory - </DialogDescription> - </DialogHeader> - <Label className="mt-5 block">URL</Label> - <Input - placeholder="Enter the URL of the page" - type="url" - data-modal-autofocus - className="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="mr-auto bg-white/5 hover:bg-slate-100 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); - const data = await addMemory( - { - title: metadata.title, - description: metadata.description, - content: "", - type: "page", - url: url, - image: metadata.image, - savedAt: new Date(), - }, - selectedSpacesId, - ); - if (data) onAdd?.(data.memory); - closeDialog(); - }} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} - -export function NoteAddPage({ - closeDialog, - defaultSpaces, - onAdd, -}: { - closeDialog: () => void; - defaultSpaces?: number[]; - onAdd?: (addedData: StoredContent) => void; -}) { - const { addMemory } = useMemory(); - - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>( - defaultSpaces ?? [], - ); - - 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 className="w-[80vw] md:w-auto"> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 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 border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-full overflow-y-auto rounded-lg border bg-white md:w-[50vw] [&>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((data) => { - if (data?.memory) onAdd?.(data.memory); - closeDialog(); - }); - } - }} - disabled={loading} - className="hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md bg-[#F4F3F2] 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, - onAdd, -}: { - closeDialog: () => void; - onAdd?: (addedData: StoredSpace) => 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="w-[80vw] 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="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="mr-auto bg-white/5 hover:hover:bg-slate-100 focus-visible:hover:bg-slate-100 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((data) => { - if (data) onAdd?.(data.space); - closeDialog(); - }); - } - }} - disabled={loading} - className="bg-rgray-4 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition hover:hover:bg-slate-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:hover:bg-slate-100 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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] 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-2 px-3 text-sm [&:hover_[data-icon]]:block [&:hover_img]:hidden"> - <button - onClick={onRemove} - className="ring-rgray-7 ring-offset-rgray-3 m-0 h-5 w-5 rounded-sm p-0 ring-offset-2 focus-visible:outline-none focus-visible:ring-2 [&:focus-visible>[data-icon]]:block [&:focus-visible>img]:hidden" - > - <img - src={ - type === "note" - ? "/note.svg" - : image ?? "/icons/logo_without_bg.png" - } - className="h-5 w-5" - /> - <X data-icon className="hidden h-5 w-5 scale-90" /> - </button> - <span>{title}</span> - <span className="ml-auto block opacity-50"> - {type === "note" ? "Note" : cleanUrl(url)} - </span> - </div> - ); -} - -export function AddExistingMemoryToSpace({ - space, - closeDialog, - fromSpaces, - notInSpaces, - onAdd, -}: { - space: { title: string; id: number }; - closeDialog: () => void; - fromSpaces?: number[]; - notInSpaces?: number[]; - onAdd?: () => void; -}) { - const { addMemoriesToSpace } = useMemory(); - - const [loading, setLoading] = useState(false); - - const [selected, setSelected] = useState<StoredContent[]>([]); - - return ( - <div className="w-[80vw] md:w-[40vw]"> - <DialogHeader> - <DialogTitle>Add an existing memory to {space.title}</DialogTitle> - <DialogDescription> - Pick the memories you want to add to this space - </DialogDescription> - </DialogHeader> - {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} - fromSpaces={fromSpaces} - notInSpaces={notInSpaces} - 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={() => { - setLoading(true); - addMemoriesToSpace( - space.id, - selected.map((i) => i.id), - ).then(() => { - onAdd?.(); - 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="focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:bg-[#F4F3F2] focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx b/apps/web/src/components/Sidebar/DeleteConfirmation.tsx deleted file mode 100644 index 7955df0d..00000000 --- a/apps/web/src/components/Sidebar/DeleteConfirmation.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { - Dialog, - DialogContent, - DialogTrigger, - DialogTitle, - DialogDescription, - DialogClose, - DialogFooter, -} from "../ui/dialog"; - -export default function DeleteConfirmation({ - onDelete, - trigger = true, - children, -}: { - trigger?: boolean; - onDelete?: () => void; - children: React.ReactNode; -}) { - return ( - <Dialog> - {trigger ? ( - <DialogTrigger asChild>{children}</DialogTrigger> - ) : ( - <>{children}</> - )} - <DialogContent> - <DialogTitle className="text-xl">Are you sure?</DialogTitle> - <DialogDescription className="text-md"> - You will not be able to recover this it. - </DialogDescription> - <DialogFooter> - <DialogClose - type={undefined} - onClick={onDelete} - className="ml-auto flex items-center justify-center rounded-md bg-red-100/10 px-3 py-2 text-red-400 transition hover:bg-red-100/5 focus-visible:bg-red-100/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-100/30" - > - Delete - </DialogClose> - <DialogClose 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"> - Cancel - </DialogClose> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Sidebar/EditNoteDialog.tsx b/apps/web/src/components/Sidebar/EditNoteDialog.tsx deleted file mode 100644 index c0ad716d..00000000 --- a/apps/web/src/components/Sidebar/EditNoteDialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import { Editor } from "novel"; -import { DialogClose, DialogFooter } from "../ui/dialog"; -import { Input } from "../ui/input"; -import { Markdown } from "tiptap-markdown"; -import { useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./FilterCombobox"; -import { useMemory } from "@/contexts/MemoryContext"; -import { Loader, Plus, Trash, X } from "lucide-react"; -import { motion } from "framer-motion"; -import { StoredContent } from "@/server/db/schema"; -import { fetchContent } from "@/actions/db"; -import { isArraysEqual } from "@/lib/utils"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function NoteEdit({ - memory, - closeDialog, - onDelete, -}: { - memory: StoredContent; - closeDialog: () => any; - onDelete?: () => void; -}) { - const { updateMemory, deleteMemory } = useMemory(); - - const [initialSpaces, setInitialSpaces] = useState<number[]>([]); - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(memory.title ?? ""); - const [content, setContent] = useState(memory.content); - 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; - } - - useEffect(() => { - fetchContent(memory.id).then((data) => { - if (data?.spaces) { - setInitialSpaces(data.spaces); - setSelectedSpacesId(data.spaces); - } - }); - }, []); - - return ( - <div> - <Input - ref={inputRef} - data-error="false" - className="w-full border-none p-0 text-xl ring-0 placeholder:transition placeholder:duration-500 focus-visible:ring-0 data-[error=true]:placeholder:text-red-400" - placeholder="Title of the note" - value={name} - disabled={loading} - onChange={(e) => setName(e.target.value)} - /> - <Editor - disableLocalStorage - defaultValue={memory.content} - onUpdate={(editor) => { - if (!editor) return; - setContent(editor.storage.markdown.getMarkdown()); - }} - extensions={[Markdown]} - className="novel-editor border-rgray-7 dark mt-5 max-h-[60vh] min-h-[40vh] w-[50vw] overflow-y-auto rounded-lg border bg-white [&>div>div]:p-5" - /> - <DialogFooter> - <FilterSpaces - selectedSpaces={selectedSpacesId} - setSelectedSpaces={setSelectedSpacesId} - className="mr-auto bg-white hover:bg-slate-100" - name={"Spaces"} - /> - <DeleteConfirmation - onDelete={() => { - deleteMemory(memory.id); - onDelete?.(); - }} - > - <button - type={undefined} - disabled={loading} - className="rounded-md px-3 py-2 ring-transparent transition hover:bg-red-100 hover:text-red-400 focus-visible:bg-red-100 focus-visible:text-red-400 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <Trash className="h-5 w-5" /> - </button> - </DeleteConfirmation> - <button - onClick={() => { - if (check()) { - setLoading(true); - console.log({ - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }); - updateMemory(memory.id, { - title: name === memory.title ? undefined : name, - content: content === memory.content ? undefined : content, - spaces: isArraysEqual(initialSpaces, selectedSpacesId) - ? undefined - : selectedSpacesId, - }).then(closeDialog); - } - }} - disabled={loading} - className="focus-visible:ring-rgray-7 relative rounded-md bg-white px-4 py-2 ring-transparent transition hover:bg-slate-100 focus-visible:bg-slate-100 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="h-5 w-5 animate-spin" /> - </motion.div> - <motion.div - initial={{ y: "0%" }} - animate={loading && { opacity: 0, y: "30%" }} - > - Save - </motion.div> - </button> - <DialogClose - type={undefined} - disabled={loading} - className="focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - Cancel - </DialogClose> - </DialogFooter> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/ExpandedSpace.tsx b/apps/web/src/components/Sidebar/ExpandedSpace.tsx deleted file mode 100644 index 55d3f3f8..00000000 --- a/apps/web/src/components/Sidebar/ExpandedSpace.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import { fetchContentForSpace, getSpace } from "@/actions/db"; -import { useMemory } from "@/contexts/MemoryContext"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { - Edit3, - Loader, - Plus, - Search, - Sparkles, - StickyNote, - Text, - Undo2, -} from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { Input, InputWithIcon } from "../ui/input"; -import { useDebounce } from "@/hooks/useDebounce"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { AddMemoryModal, MemoryItem } from "./MemoriesBar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { DialogTrigger } from "../ui/dialog"; - -export function ExpandedSpace({ - spaceId, - back, -}: { - spaceId: number; - back: () => void; -}) { - const { updateMemory, updateSpace, search } = useMemory(); - - const [parent, enableAnimations] = useAutoAnimate(); - - const inputRef = useRef<HTMLInputElement>(null); - - const [contentForSpace, setContentForSpace] = useState<StoredContent[]>([]); - - const [lastUpdatedTitle, setLastUpdatedTitle] = useState<string | null>(null); - - const [title, setTitle] = useState<string>(""); - const debouncedTitle = useDebounce(title, 500); - - const [loading, setLoading] = useState(true); - - const [saveLoading, setSaveLoading] = useState(false); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<StoredContent[]>([]); - - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "existing-memory" | "space" | null - >(null); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - - useEffect(() => { - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - }, []); - - useEffect(() => { - if ( - debouncedTitle.trim().length < 1 || - debouncedTitle.trim() === lastUpdatedTitle?.trim() - ) - return; - (async () => { - setSaveLoading(true); - await updateSpace(spaceId, debouncedTitle.trim()); - setLastUpdatedTitle(debouncedTitle); - setSaveLoading(false); - })(); - }, [debouncedTitle]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults( - ( - await search(q, { - filter: { spaces: false }, - memoriesRelativeToSpace: { - fromSpaces: [spaceId], - }, - }) - ).map((i) => i.memory!), - ); - setSearchLoading(false); - })(); - }, [query]); - - if (loading) { - return ( - <div className="flex h-full w-full items-center justify-center"> - <Loader className="h-5 w-5 animate-spin" /> - </div> - ); - } - - return ( - <div className="text-rgray-11 flex w-full flex-col items-start py-8 text-left"> - <div className="flex w-full items-center justify-start gap-2 px-8"> - <button - onClick={back} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - <Undo2 className="h-5 w-5" /> - </button> - <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 space" - data-modal-autofocus - value={title} - onChange={(e) => setTitle(e.target.value)} - /> - <button - onClick={() => { - inputRef.current?.focus(); - inputRef.current?.animate( - { - opacity: [1, 0.2, 1], - }, - { - duration: 100, - }, - ); - }} - className="focus-visible:ring-offset-rgray-3 focus-visible:ring-rgray-7 rounded-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2" - > - {saveLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-70" /> - ) : ( - <Edit3 className="h-5 w-5 opacity-70" /> - )} - </button> - </div> - <div className="w-full px-8"> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="text-rgray-11 h-5 w-5 opacity-50" /> - ) - } - className="bg-rgray-4 mt-2 w-full" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div className="mt-2 w-full px-8"> - <AddMemoryModal - onAdd={(data) => { - if (!data) { - setLoading(true); - (async () => { - const title = (await getSpace(spaceId))?.name ?? ""; - setTitle(title); - setLastUpdatedTitle(title); - setContentForSpace((await fetchContentForSpace(spaceId)) ?? []); - setLoading(false); - })(); - } else if (Object.hasOwn(data, "url")) { - const _data = data as StoredContent; - setContentForSpace((prev) => [...prev, _data]); - } - }} - data={{ space: { title, id: spaceId }, notInSpaces: [spaceId] }} - defaultSpaces={[spaceId]} - 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("existing-memory"); - }} - > - <Sparkles className="mr-2 h-4 w-4" /> - Existing Memory - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("page"); - }} - > - <StickyNote className="mr-2 h-4 w-4" /> - Page - </DropdownMenuItem> - </DialogTrigger> - <DialogTrigger className="block w-full"> - <DropdownMenuItem - onClick={() => { - setAddMemoryState("note"); - }} - > - <Text className="mr-2 h-4 w-4" /> - Note - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </AddMemoryModal> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" - > - {query.trim().length > 0 ? ( - <> - {searchResults.map((memory, i) => ( - <MemoryItem - removeFromSpace={async () => { - await updateMemory(memory.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - {...memory!} - key={i} - onDelete={() => { - setContentForSpace((prev) => - prev.filter((s) => s.id !== memory.id), - ); - setSearchResults((prev) => - prev.filter((i) => i.id !== memory.id), - ); - }} - /> - ))} - </> - ) : ( - contentForSpace.map((m) => ( - <MemoryItem - key={m.id} - {...m} - onDelete={() => - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)) - } - removeFromSpace={async () => { - await updateMemory(m.id, { - removedFromSpaces: [spaceId], - }); - setContentForSpace((prev) => prev.filter((s) => s.id !== m.id)); - }} - /> - )) - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx deleted file mode 100644 index 634a09e3..00000000 --- a/apps/web/src/components/Sidebar/FilterCombobox.tsx +++ /dev/null @@ -1,303 +0,0 @@ -"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"; -import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { useDebounce } from "@/hooks/useDebounce"; -import { StoredContent } from "@/server/db/schema"; - -export interface FilterSpacesProps - extends React.ButtonHTMLAttributes<HTMLButtonElement> { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selectedSpaces: number[]; - setSelectedSpaces: ( - spaces: number[] | ((prev: number[]) => number[]), - ) => void; - name: string; -} - -export function FilterSpaces({ - className, - side = "bottom", - align = "center", - onClose, - selectedSpaces, - setSelectedSpaces, - name, - ...props -}: FilterSpacesProps) { - 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 ( - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "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 - align={align} - side={side} - className="w-[200px] p-0" - onCloseAutoFocus={(e) => e.preventDefault()} - > - <Command - filter={(val, search) => - spaces - .find((s) => s.id.toString() === val) - ?.name.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.name.length > 10 - ? space.name.slice(0, 10) + "..." - : space.name} - {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> - ); -} - -export type FilterMemoriesProps = { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; - selected: StoredContent[]; - setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>; - fromSpaces?: number[]; - notInSpaces?: number[]; -} & React.ButtonHTMLAttributes<HTMLButtonElement>; - -export function FilterMemories({ - className, - side = "bottom", - align = "center", - onClose, - selected, - setSelected, - fromSpaces, - notInSpaces, - ...props -}: FilterMemoriesProps) { - const { search } = useMemory(); - - const [open, setOpen] = React.useState(false); - const [searchQuery, setSearchQuery] = React.useState(""); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]); - const [isSearching, setIsSearching] = React.useState(false); - - const results = React.useMemo(() => { - return searchResults.map((r) => r.memory); - }, [searchResults]); - - console.log("memoized", results); - - React.useEffect(() => { - const q = query.trim(); - if (q.length > 0) { - setIsSearching(true); - (async () => { - const results = await search(q, { - filter: { - memories: true, - spaces: false, - }, - memoriesRelativeToSpace: { - fromSpaces, - notInSpaces, - }, - }); - setSearchResults(results); - setIsSearching(false); - })(); - } else { - setSearchResults([]); - } - }, [query]); - - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); - - console.log(searchResults); - 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} - > - {props.children} - </button> - </PopoverTrigger> - <PopoverContent - onCloseAutoFocus={(e) => e.preventDefault()} - align={align} - side={side} - className="w-[200px] p-0" - > - <Command shouldFilter={false}> - <CommandInput - isSearching={isSearching} - value={searchQuery} - onValueChange={setSearchQuery} - placeholder="Filter memories..." - /> - <CommandList> - <CommandGroup> - <CommandEmpty className="text-rgray-11 py-5 text-center text-sm"> - {isSearching - ? "Searching..." - : query.trim().length > 0 - ? "Nothing Found" - : "Search something"} - </CommandEmpty> - {results.map((m) => ( - <CommandItem - key={m.id} - value={m.id.toString()} - onSelect={(val) => { - setSelected((prev) => - prev.find((p) => p.id === parseInt(val)) - ? prev.filter((v) => v.id !== parseInt(val)) - : [...prev, m], - ); - }} - asChild - > - <div className="text-rgray-11"> - <img - src={ - m.type === "note" - ? "/note.svg" - : m.image ?? "/icons/logo_without_bg.png" - } - className="mr-2 h-4 w-4" - /> - {m.title && m.title?.length > 14 - ? m.title?.slice(0, 14) + "..." - : m.title} - <Check - data-state-on={ - selected.find((i) => i.id === m.id) !== undefined - } - className={cn( - "on:opacity-100 ml-auto h-4 w-4 opacity-0", - )} - /> - </div> - </CommandItem> - ))} - </CommandGroup> - </CommandList> - </Command> - </PopoverContent> - </Popover> - </LayoutGroup> - </AnimatePresence> - ); -} diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx deleted file mode 100644 index a81d00c0..00000000 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ /dev/null @@ -1,709 +0,0 @@ -import { Editor } from "novel"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import { - MemoryWithImage, - MemoryWithImages3, - MemoryWithImages2, -} from "@/assets/MemoryWithImages"; -import { Input, InputWithIcon } from "../ui/input"; -import { - ArrowUpRight, - Edit3, - Loader, - Minus, - MoreHorizontal, - Plus, - Search, - Sparkles, - Text, - Trash2, -} from "lucide-react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "../ui/dropdown-menu"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Variant, useAnimate, motion } from "framer-motion"; -import { SearchResult, useMemory } from "@/contexts/MemoryContext"; -import { SpaceIcon } from "@/assets/Memories"; -import { Dialog, DialogContent } from "../ui/dialog"; -import useViewport from "@/hooks/useViewport"; -import useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./AddMemoryDialog"; -import { ExpandedSpace } from "./ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./EditNoteDialog"; -import DeleteConfirmation from "./DeleteConfirmation"; - -export function MemoriesBar({ isOpen }: { isOpen: boolean }) { - const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace, freeMemories, search } = useMemory(); - - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [addMemoryState, setAddMemoryState] = useState< - "page" | "note" | "space" | "existing-memory" | null - >(null); - - const [expandedSpace, setExpandedSpace] = useState<number | null>(null); - - const [searchQuery, setSearcyQuery] = useState(""); - const [searchLoading, setSearchLoading] = useState(false); - const query = useDebounce(searchQuery, 500); - - const [searchResults, setSearchResults] = useState<SearchResult[]>([]); - - useEffect(() => { - const q = query.trim(); - if (q.length < 1) { - setSearchResults([]); - return; - } - - setSearchLoading(true); - - (async () => { - setSearchResults(await search(q)); - setSearchLoading(false); - })(); - }, [query]); - - useEffect(() => { - if (!isOpen) { - setExpandedSpace(null); - } - }, [isOpen]); - - if (expandedSpace) { - return ( - <ExpandedSpace - spaceId={expandedSpace} - back={() => setExpandedSpace(null)} - // close={() => setExpandedSpace(null)} - /> - ); - } - - 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={ - searchLoading ? ( - <Loader className="text-rgray-11 h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="text-rgray-11 h-5 w-5 opacity-50" /> - ) - } - className="bg-rgray-4 mt-2 w-full" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div className="mt-2 flex w-full px-8"> - <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> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5" - > - {query.trim().length > 0 ? ( - <> - {searchResults.map(({ type, space, memory }, i) => ( - <> - {type === "memory" && ( - <MemoryItem - {...memory!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.memory?.id !== memory.id), - ); - }} - /> - )} - {type === "space" && ( - <SpaceItem - {...space!} - key={i} - onDelete={() => { - setSearchResults((prev) => - prev.filter((i) => i.space?.id !== space.id), - ); - deleteSpace(space.id); - }} - /> - )} - </> - ))} - </> - ) : ( - <> - {spaces.map((space) => ( - <SpaceItem - onDelete={() => deleteSpace(space.id)} - key={space.id} - onClick={() => setExpandedSpace(space.id)} - {...space} - /> - ))} - {freeMemories.map((m) => ( - <MemoryItem {...m} key={m.id} /> - ))} - </> - )} - </div> - </div> - ); -} - -const SpaceExitVariant: Variant = { - opacity: 0, - scale: 0, - borderRadius: "50%", - background: "var(--gray-1)", - transition: { - duration: 0.2, - }, -}; - -export function MemoryItem( - props: StoredContent & { - onDelete?: () => void; - removeFromSpace?: () => Promise<void>; - }, -) { - const { id, title, image, type, url, onDelete, removeFromSpace } = props; - - const { deleteMemory } = useMemory(); - - const name = title - ? title.length > 10 - ? title.slice(0, 10) + "..." - : title - : "<no title>"; - - const [isDialogOpen, setIsDialogOpen] = useState(false); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - return ( - <Dialog - open={type === "note" ? isDialogOpen : false} - onOpenChange={setIsDialogOpen} - > - <div - {...touchEventProps} - className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex cursor-pointer select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - > - {type === "note" ? ( - <DialogTrigger asChild> - <button data-space-text className="focus-visible:outline-none"> - {name} - </button> - </DialogTrigger> - ) : ( - <button - onClick={() => window.open(url)} - data-space-text - className="focus-visible:outline-none" - > - {name} - </button> - )} - - {type === "page" ? ( - <PageMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - url={url} - /> - ) : type === "note" ? ( - <NoteMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - removeFromSpace={removeFromSpace} - onEdit={() => setIsDialogOpen(true)} - onDelete={() => { - deleteMemory(id); - onDelete?.(); - }} - /> - ) : null} - - <div className="flex h-24 w-24 items-center justify-center"> - {type === "page" ? ( - <img - onClick={() => window.open(url)} - className="h-16 w-16" - id={id.toString()} - src={image!} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - ) : type === "note" ? ( - <Text onClick={() => setIsDialogOpen(true)} className="h-16 w-16" /> - ) : ( - <></> - )} - </div> - </div> - <DialogContent className="w-max max-w-[auto]"> - <NoteEdit - onDelete={onDelete} - closeDialog={() => setIsDialogOpen(false)} - memory={props} - /> - </DialogContent> - </Dialog> - ); -} - -export function SpaceItem({ - name, - id, - onDelete, - onClick, -}: StoredSpace & { onDelete: () => void; onClick?: () => void }) { - const { cachedMemories } = useMemory(); - - const [itemRef, animateItem] = useAnimate(); - const { width } = useViewport(); - - const [moreDropdownOpen, setMoreDropdownOpen] = useState(false); - - const touchEventProps = useTouchHold({ - onHold() { - setMoreDropdownOpen(true); - }, - }); - - const spaceMemories = useMemo(() => { - return cachedMemories.filter((m) => m.space === id); - }, [cachedMemories]); - - const _name = name.length > 10 ? name.slice(0, 10) + "..." : name; - - return ( - <motion.div - ref={itemRef} - {...touchEventProps} - className="hover:bg-rgray-2 has-[[data-state='true']]:bg-rgray-2 has-[[data-space-text]:focus-visible]:bg-rgray-2 has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 relative flex select-none flex-col-reverse items-center justify-center rounded-md p-2 pb-4 text-center font-normal ring-transparent transition has-[[data-space-text]:focus-visible]:outline-none has-[[data-space-text]:focus-visible]:ring-2 md:has-[[data-state='true']]:bg-transparent [&:hover>[data-more-button]]:opacity-100" - > - <button - onClick={onClick} - data-space-text - className="focus-visible:outline-none" - > - {_name} - </button> - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={() => { - onDelete(); - return; - if (!itemRef.current || width < 768) { - onDelete(); - return; - } - // const trash = document.querySelector("#trash")! as HTMLDivElement; - // const trashBin = document.querySelector("#trash-button")!; - // const trashRect = trashBin.getBoundingClientRect(); - // const scopeRect = itemRef.current.getBoundingClientRect(); - // const el = document.createElement("div"); - // el.style.position = "fixed"; - // el.style.top = "0"; - // el.style.left = "0"; - // el.style.width = "15px"; - // el.style.height = "15px"; - // el.style.backgroundColor = "var(--gray-7)"; - // el.style.zIndex = "60"; - // el.style.borderRadius = "50%"; - // el.style.transform = "scale(5)"; - // el.style.opacity = "0"; - // trash.dataset["open"] = "true"; - // const initial = { - // x: scopeRect.left + scopeRect.width / 2, - // y: scopeRect.top + scopeRect.height / 2, - // }; - // const delta = { - // x: - // trashRect.left + - // trashRect.width / 2 - - // scopeRect.left + - // scopeRect.width / 2, - // y: - // trashRect.top + - // trashRect.height / 4 - - // scopeRect.top + - // scopeRect.height / 2, - // }; - // const end = { - // x: trashRect.left + trashRect.width / 2, - // y: trashRect.top + trashRect.height / 4, - // }; - // el.style.offsetPath = `path('M ${initial.x} ${initial.y} Q ${delta.x * 0.01} ${delta.y * 0.01} ${end.x} ${end.y}`; - // animateItem(itemRef.current, SpaceExitVariant, { - // duration: 0.2, - // }).then(() => { - // itemRef.current.style.scale = "0"; - // onDelete(); - // }); - // document.body.appendChild(el); - // el.animate( - // { - // transform: ["scale(5)", "scale(1)"], - // opacity: [0, 0.3, 1], - // }, - // { - // duration: 200, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // }, - // ); - // el.animate( - // { - // offsetDistance: ["0%", "100%"], - // }, - // { - // duration: 2000, - // easing: "cubic-bezier(0.64, 0.57, 0.67, 1.53)", - // fill: "forwards", - // delay: 200, - // }, - // ).onfinish = () => { - // el.animate( - // { transform: "scale(0)", opacity: 0 }, - // { duration: 200, fill: "forwards" }, - // ).onfinish = () => { - // el.remove(); - // }; - // }; - }} - /> - {spaceMemories.length > 2 ? ( - <MemoryWithImages3 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length > 1 ? ( - <MemoryWithImages2 - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - images={ - spaceMemories - .map((c) => (c.type === "note" ? "/note.svg" : c.image)) - .reverse() as string[] - } - /> - ) : spaceMemories.length === 1 ? ( - <MemoryWithImage - onClick={onClick} - className="h-24 w-24" - id={id.toString()} - image={ - spaceMemories[0].type === "note" - ? "/note.svg" - : spaceMemories[0].image! - } - /> - ) : ( - <div - onClick={onClick} - className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30" - ></div> - )} - </motion.div> - ); -} - -export function SpaceMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function PageMoreButton({ - onDelete, - isOpen, - setIsOpen, - url, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - url: string; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={() => window.open(url)}> - <ArrowUpRight - className="mr-2 h-4 w-4 scale-125" - strokeWidth={1.5} - /> - Open - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function NoteMoreButton({ - onDelete, - isOpen, - setIsOpen, - onEdit, - removeFromSpace, -}: { - onDelete?: () => void; - isOpen?: boolean; - onEdit?: () => void; - setIsOpen?: (open: boolean) => void; - removeFromSpace?: () => Promise<void>; -}) { - return ( - <DeleteConfirmation onDelete={onDelete} trigger={false}> - <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> - <DropdownMenuTrigger asChild> - <button - data-more-button - className="hover:bg-rgray-3 focus-visible:bg-rgray-3 focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="text-rgray-11 h-5 w-5" /> - </button> - </DropdownMenuTrigger> - <DropdownMenuContent align="start"> - <DropdownMenuItem onClick={onEdit}> - <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Edit - </DropdownMenuItem> - {removeFromSpace && ( - <DropdownMenuItem onClick={removeFromSpace}> - <Minus className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Remove from space - </DropdownMenuItem> - )} - <DialogTrigger asChild> - <DropdownMenuItem className="focus:bg-red-100 focus:text-red-400"> - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Delete - </DropdownMenuItem> - </DialogTrigger> - </DropdownMenuContent> - </DropdownMenu> - </DeleteConfirmation> - ); -} - -export function AddMemoryModal({ - type, - children, - defaultSpaces, - onAdd, - data, -}: { - type: "page" | "note" | "space" | "existing-memory" | null; - children?: React.ReactNode | React.ReactNode[]; - defaultSpaces?: number[]; - data?: { - space?: { - title: string; - id: number; - }; - fromSpaces?: number[]; - notInSpaces?: number[]; - }; - onAdd?: (data?: StoredSpace | StoredContent | StoredContent[]) => void; -}) { - const [isDialogOpen, setIsDialogOpen] = useState(false); - - return ( - <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]" - > - {type === "page" ? ( - <AddMemoryPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "note" ? ( - <NoteAddPage - onAdd={onAdd} - defaultSpaces={defaultSpaces} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "space" ? ( - <SpaceAddPage - onAdd={onAdd} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : type === "existing-memory" ? ( - <AddExistingMemoryToSpace - onAdd={onAdd} - fromSpaces={data?.fromSpaces} - notInSpaces={data?.notInSpaces} - space={data!.space!} - closeDialog={() => setIsDialogOpen(false)} - /> - ) : ( - <></> - )} - </DialogContent> - </Dialog> - ); -} diff --git a/apps/web/src/components/Sidebar/SettingsTab.tsx b/apps/web/src/components/Sidebar/SettingsTab.tsx deleted file mode 100644 index 31b8380d..00000000 --- a/apps/web/src/components/Sidebar/SettingsTab.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { Box, LogOut } from "lucide-react"; -import { signOut, useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -export function SettingsTab({ open }: { open: boolean }) { - const { data: session } = useSession(); - - const [tweetStat, setTweetStat] = useState<[number, number] | null>(); - const [memoryStat, setMemoryStat] = useState<[number, number] | null>(); - - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch("/api/getCount").then(async (resp) => { - const data = (await resp.json()) as any; - setTweetStat([data.tweetsCount, data.tweetsLimit]); - setMemoryStat([data.pageCount, data.pageLimit]); - setLoading(false); - }); - }, [open]); - - return ( - <div className="flex h-full w-full flex-col items-start py-3 text-left font-normal text-black md:py-8"> - <div className="w-full px-6"> - <h1 className="w-full text-2xl font-medium">Settings</h1> - <div className="mt-5 grid w-full grid-cols-3 gap-1"> - <img - className="rounded-full" - src={session?.user?.image ?? "/icons/white_without_bg.png"} - onError={(e) => { - (e.target as HTMLImageElement).src = - "/icons/white_without_bg.png"; - }} - /> - <div className="col-span-2 flex flex-col items-start justify-center"> - <h1 className="text-xl font-medium">{session?.user?.name}</h1> - <span>{session?.user?.email}</span> - <button - onClick={() => signOut()} - className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative mt-auto flex items-center justify-center gap-2 rounded-md px-4 py-2 text-white ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70" - > - <LogOut className="h-4 w-4" /> - Logout - </button> - </div> - </div> - </div> - <div className="border-rgray-5 mt-auto w-full px-8 pt-8"> - <h1 className="flex w-full items-center gap-2 text-xl"> - <Box className="h-6 w-6" /> - Storage - </h1> - {loading ? ( - <div className="my-5 flex w-full flex-col items-center justify-center gap-5"> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - <div className="bg-rgray-5 h-6 w-full animate-pulse rounded-md text-lg"></div> - </div> - ) : ( - <> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Memories - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {memoryStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((memoryStat?.[0] ?? 0) / (memoryStat?.[1] ?? 100)) * 100}%`, - minWidth: memoryStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="bg-rgray-5 h-full rounded-full" - /> - </div> - </div> - <div className="my-5"> - <h2 className="text-md flex w-full items-center justify-between"> - Tweets - <div className="bg-rgray-4 flex rounded-md px-2 py-2 text-xs text-white/70"> - {tweetStat?.join("/")} - </div> - </h2> - <div className="mt-2 h-5 w-full overflow-hidden rounded-full bg-stone-400"> - <div - style={{ - width: `${((tweetStat?.[0] ?? 0) / (tweetStat?.[1] ?? 100)) * 100}%`, - minWidth: tweetStat?.[0] ?? 0 > 0 ? "5%" : "0%", - }} - className="h-full rounded-full bg-white" - /> - </div> - </div> - </> - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx deleted file mode 100644 index ae757afe..00000000 --- a/apps/web/src/components/Sidebar/index.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; -import { MemoryIcon } from "../../assets/Memories"; -import React, { useEffect, useState } from "react"; -import { AnimatePresence, motion } from "framer-motion"; -import { signOut, useSession } from "next-auth/react"; -import MessagePoster from "@/app/MessagePoster"; -import Link from "next/link"; -import { SettingsTab } from "./SettingsTab"; -import { Avatar, AvatarImage } from "@radix-ui/react-avatar"; -import { AvatarFallback } from "../ui/avatar"; - -export type MenuItem = { - icon: React.ReactNode | React.ReactNode[]; - label: string; - content?: React.ReactNode; - labelDisplay?: React.ReactNode; -}; - -export default function Sidebar({ - selectChange, - jwt, -}: { - selectChange?: (selectedItem: string | null) => void; - jwt: string; -}) { - const { data: session } = useSession(); - - const [selectedItem, setSelectedItem] = useState<string | null>(null); - - const menuItemsTop: Array<MenuItem> = []; - - const menuItemsBottom: Array<MenuItem> = [ - { - label: "Settings", - content: <SettingsTab open={selectedItem !== null} />, - icon: <></>, - }, - ]; - - const menuItems = [...menuItemsTop, ...menuItemsBottom]; - - const Subbar = menuItems.find((i) => i.label === selectedItem)?.content ?? ( - <></> - ); - - useEffect(() => { - void selectChange?.(selectedItem); - }, [selectedItem]); - - return ( - <div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex"> - <div - className={`relative z-[50] flex h-full w-full flex-col items-center justify-center border-r bg-stone-100 px-2 py-5 `} - > - <Link - data-state-on={selectedItem === "Memories"} - href="/" - onClick={() => setSelectedItem(null)} - className="focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - > - <MemoryIcon className="h-12 w-12" /> - <span className="text-black">Memories</span> - </Link> - - <div className="mt-auto" /> - - <MenuItem - item={{ - label: "Settings", - icon: ( - <svg - xmlns="http://www.w3.org/2000/svg" - fill="white" - viewBox="0 0 24 24" - strokeWidth={0.5} - stroke="black" - className="h-10 w-10" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" - /> - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" - /> - </svg> - ), - content: <SettingsTab open={selectedItem !== null} />, - }} - selectedItem={selectedItem} - setSelectedItem={setSelectedItem} - /> - {/* <MessagePoster jwt={jwt} /> */} - <div className="mt-4 flex cursor-pointer flex-col items-center justify-center gap-2 rounded-b-md border-t border-stone-600 px-2 py-3 pt-4 text-black hover:bg-stone-300 hover:opacity-100"> - <Avatar> - <AvatarImage - className="h-10 w-10 rounded-full" - src={session?.user?.image!} - alt="Profile picture" - /> - <AvatarFallback> - {session?.user?.name?.split(" ").map((n) => n[0])}{" "} - </AvatarFallback> - </Avatar> - <span>{session?.user?.name?.split(" ")[0]}</span> - </div> - </div> - <AnimatePresence> - {selectedItem && <SubSidebar>{Subbar}</SubSidebar>} - </AnimatePresence> - </div> - ); -} - -const MenuItem = ({ - item: { icon, label, labelDisplay }, - selectedItem, - setSelectedItem, - ...props -}: { - item: MenuItem; - selectedItem: string | null; - setSelectedItem: React.Dispatch<React.SetStateAction<string | null>>; -}) => { - const handleClick = () => - setSelectedItem((prev) => (prev === label ? null : label)); - - return ( - <button - data-state-on={selectedItem === label} - onClick={handleClick} - className="on:opacity-100 on:bg-stone-300 focus-visible:ring-rgray-7 relative z-[100] flex w-full flex-col items-center justify-center rounded-md px-3 py-3 text-black opacity-80 ring-2 ring-transparent transition hover:bg-stone-300 hover:opacity-100 focus-visible:opacity-100 focus-visible:outline-none" - {...props} - > - {icon} - <span className="">{labelDisplay ?? label}</span> - </button> - ); -}; - -export function SubSidebar({ children }: { children?: React.ReactNode }) { - return ( - <motion.div - initial={{ opacity: 0, x: "-100%" }} - animate={{ opacity: 1, x: 0 }} - exit={{ - opacity: 0, - x: "-100%", - transition: { delay: 0.2 }, - }} - transition={{ - duration: 0.2, - }} - className="absolute left-[100%] top-0 z-[10] hidden h-screen w-[30vw] items-start justify-center overflow-x-hidden border-r bg-stone-100 font-light md:flex" - > - <motion.div - initial={{ opacity: 0 }} - animate={{ opacity: 1 }} - exit={{ opacity: 0, transition: { delay: 0 } }} - transition={{ - delay: 0.2, - }} - className="z-[10] flex h-full w-full min-w-full flex-col items-center opacity-0" - > - <AnimatePresence>{children}</AnimatePresence> - </motion.div> - </motion.div> - ); -} |