aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components/Sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/Sidebar')
-rw-r--r--apps/web/src/components/Sidebar/AddMemoryDialog.tsx480
-rw-r--r--apps/web/src/components/Sidebar/DeleteConfirmation.tsx47
-rw-r--r--apps/web/src/components/Sidebar/EditNoteDialog.tsx155
-rw-r--r--apps/web/src/components/Sidebar/ExpandedSpace.tsx287
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx303
-rw-r--r--apps/web/src/components/Sidebar/MemoriesBar.tsx709
-rw-r--r--apps/web/src/components/Sidebar/SettingsTab.tsx99
-rw-r--r--apps/web/src/components/Sidebar/index.tsx172
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>
- );
-}