diff options
Diffstat (limited to 'apps/web/src/components')
29 files changed, 0 insertions, 4801 deletions
diff --git a/apps/web/src/components/ChatMessage.tsx b/apps/web/src/components/ChatMessage.tsx deleted file mode 100644 index 58ef9870..00000000 --- a/apps/web/src/components/ChatMessage.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useEffect } from "react"; -import { motion } from "framer-motion"; -import { ArrowUpRight, Globe, Text } from "lucide-react"; -import { convertRemToPixels } from "@/lib/utils"; -import { SpaceIcon } from "@/assets/Memories"; -import Markdown from "react-markdown"; -import { ChatHistory } from "../../types/memory"; - -export function ChatAnswer({ - children: message, - sources, - loading = false, -}: { - children: string; - sources?: ChatHistory["answer"]["sources"]; - loading?: boolean; -}) { - return ( - <div className="flex h-max w-full flex-col items-start gap-5"> - {loading ? ( - <MessageSkeleton /> - ) : ( - <div className="chat-answer h-full w-full text-lg text-white/60"> - <Markdown>{message}</Markdown> - </div> - )} - {!loading && sources && sources?.length > 0 && ( - <> - <h1 className="animate-fade-in text-rgray-12 text-md flex items-center justify-center gap-2 opacity-0 [animation-duration:1s]"> - <SpaceIcon className="h-6 w-6 -translate-y-[2px]" /> - Related Memories - </h1> - <div className="animate-fade-in -mt-3 flex items-center justify-start gap-1 opacity-0 [animation-duration:1s]"> - {sources?.map((source) => - source.isNote ? ( - <button className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm"> - <Text className="h-4 w-4" /> - {source.source} - </button> - ) : ( - <a - className="bg-rgray-3 flex items-center justify-center gap-2 rounded-full py-1 pl-2 pr-3 text-sm" - key={source.source} - href={source.source} - target="_blank" - > - <Globe className="h-4 w-4" /> - {cleanUrl(source.source)} - </a> - ), - )} - </div> - </> - )} - </div> - ); -} - -export function ChatQuestion({ children }: { children: string }) { - return ( - <div - className={`text-rgray-12 h-max w-full text-left ${children.length > 200 ? "text-xl" : "text-2xl"}`} - > - {children} - </div> - ); -} - -export function ChatMessage({ - children, - isLast = false, - index, -}: { - children: React.ReactNode | React.ReactNode[]; - isLast?: boolean; - index: number; -}) { - const messageRef = React.useRef<HTMLDivElement>(null); - - useEffect(() => { - if (!isLast) return; - messageRef.current?.parentElement?.scrollTo({ - top: messageRef.current?.offsetTop, - behavior: "smooth", - }); - }, []); - - return ( - <motion.div - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ - type: "tween", - duration: 0.5, - }} - ref={messageRef} - className={`${index === 0 ? "pt-16" : "pt-28"} flex h-max w-full resize-y flex-col items-start justify-start gap-5 transition-[height] ${isLast ? "min-h-screen pb-[40vh]" : "h-max"}`} - > - {children} - </motion.div> - ); -} - -function MessageSkeleton() { - return ( - <div className="animate-fade-in flex w-full flex-col items-start gap-3 opacity-0 [animation-delay:0.5s] [animation-duration:1s]"> - <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 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 className="bg-rgray-5 h-6 w-[70%] animate-pulse rounded-md text-lg"></div> - </div> - ); -} - -function cleanUrl(url: string) { - if (url.startsWith("https://")) { - url = url.slice(8); - } else if (url.startsWith("http://")) { - url = url.slice(7); - } - - if (url.endsWith("/")) { - url = url.slice(0, -1); - } - - return url; -} diff --git a/apps/web/src/components/Main-2.tsx b/apps/web/src/components/Main-2.tsx deleted file mode 100644 index 1b602712..00000000 --- a/apps/web/src/components/Main-2.tsx +++ /dev/null @@ -1,709 +0,0 @@ -"use client"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence } from "framer-motion"; -import { cn } from "@/lib/utils"; - -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 useTouchHold from "@/hooks/useTouchHold"; -import { DialogTrigger } from "@radix-ui/react-dialog"; -import { - AddExistingMemoryToSpace, - AddMemoryPage, - NoteAddPage, - SpaceAddPage, -} from "./Sidebar/AddMemoryDialog"; -import { ExpandedSpace } from "./Sidebar/ExpandedSpace"; -import { StoredContent, StoredSpace } from "@/server/db/schema"; -import { useDebounce } from "@/hooks/useDebounce"; -import { NoteEdit } from "./Sidebar/EditNoteDialog"; -import DeleteConfirmation from "./Sidebar/DeleteConfirmation"; - -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -function pseudoRandomizeColorWithName(name: string) { - const colorsAvailable = [ - "99e9f2", - "a5d8ff", - "d0bfff", - "eebefa", - "fcc2d7", - "b2f2bb", - "96f2d7", - "ffec99", - "ffd8a8", - "ffc9c9", - ]; - - const colorIndex = - name - .split("") - .map((char) => char.charCodeAt(0)) - .reduce((acc, charCode) => acc + charCode, 0) % colorsAvailable.length; - - return colorsAvailable[colorIndex]; -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const { width } = useViewport(); - - 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 ( - <> - <AnimatePresence mode="wait"> - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 overflow-auto bg-[#FFF] px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="mt-16 w-full"> - <div className="flex justify-between gap-4"> - <h1 className="w-full text-3xl font-medium tracking-tight"> - Your Memories - </h1> - <div className="flex w-full"> - <AddMemoryModal type={addMemoryState}> - <DropdownMenu - open={isDropdownOpen} - onOpenChange={setIsDropdownOpen} - > - <DropdownMenuTrigger asChild> - <button className="focus-visible:ring-rgray-7 ml-auto flex items-center justify-center rounded-md px-3 py-2 transition hover:bg-stone-200 focus-visible:bg-white 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> - <InputWithIcon - placeholder="Search" - icon={ - searchLoading ? ( - <Loader className="h-5 w-5 animate-spin opacity-50" /> - ) : ( - <Search className="h-5 w-5 opacity-50" /> - ) - } - className="mt-4 w-full text-black" - value={searchQuery} - onChange={(e) => setSearcyQuery(e.target.value)} - /> - </div> - <div - ref={parent} - className="grid w-full grid-flow-row grid-cols-3 gap-4 px-2 py-5" - > - {typeof window !== "undefined" ? ( - 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} /> - ))} - </> - ) - ) : ( - <> - {Array.from({ - length: spaces.length + freeMemories.length, - }).map((_, i) => ( - <div className="h-32 w-full animate-pulse rounded-2xl bg-stone-300/50"></div> - ))} - </> - )} - </div> - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - </main> - {width <= 768 && <MemoryDrawer />} - </AnimatePresence> - </> - ); -} - -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 > 20 - ? title.slice(0, 20) + "..." - : title - : "Untitled Memory"; - - 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} - > - <DialogTrigger asChild> - <button - onClick={() => (type === "page" ? window.open(url) : null)} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <div - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 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" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - {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 - 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> - {name} - </button> - </DialogTrigger> - <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 > 20 ? name.slice(0, 20) + "..." : name; - - return ( - <button - onClick={onClick} - data-space-text - className="relative flex h-min select-none flex-col items-center justify-center gap-2 text-center font-normal focus-visible:outline-none" - > - <motion.div - ref={itemRef} - {...touchEventProps} - className="has-[[data-space-text]:focus-visible]:ring-rgray-7 [&:has-[[data-space-text]:focus-visible]>[data-more-button]]:opacity-100 flex h-32 w-full items-center justify-center rounded-2xl border-2 border-black/20 p-2 pb-4 shadow-sm ring-transparent transition duration-150 ease-out hover:scale-105 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" - style={{ - backgroundColor: `#${pseudoRandomizeColorWithName(name)}`, - }} - > - <SpaceMoreButton - isOpen={moreDropdownOpen} - setIsOpen={setMoreDropdownOpen} - onEdit={onClick} - onDelete={onDelete} - /> - {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="flex items-center justify-center gap-2" - > - <svg - xmlns="http://www.w3.org/2000/svg" - fill="none" - viewBox="0 0 24 24" - strokeWidth="1.5" - stroke="currentColor" - className="h-8 w-8" - > - <path - strokeLinecap="round" - strokeLinejoin="round" - d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" - /> - </svg> - <span className="text-stone-800/80">Empty Space</span> - </div> - )} - </motion.div> - - {_name} - </button> - ); -} - -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="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </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="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </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="focus-visible:ring-rgray-7 absolute right-2 top-2 scale-0 rounded-md p-1 opacity-0 ring-transparent transition hover:bg-white focus-visible:bg-white focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 md:block md:scale-100 md:bg-transparent" - > - <MoreHorizontal className="h-5 w-5 text-black" /> - </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/Main.tsx b/apps/web/src/components/Main.tsx deleted file mode 100644 index 8ac52569..00000000 --- a/apps/web/src/components/Main.tsx +++ /dev/null @@ -1,533 +0,0 @@ -"use client"; -import { useCallback, useEffect, useRef, useState } from "react"; -import { FilterSpaces } from "./Sidebar/FilterCombobox"; -import { Textarea2 } from "./ui/textarea"; -import { ArrowRight, ArrowUp } from "lucide-react"; -import { MemoryDrawer } from "./MemoryDrawer"; -import useViewport from "@/hooks/useViewport"; -import { AnimatePresence, motion } from "framer-motion"; -import { cn, countLines, getIdsFromSource } from "@/lib/utils"; -import { ChatHistory } from "../../types/memory"; -import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useMemory } from "@/contexts/MemoryContext"; - -import Image from "next/image"; -import { getMemoriesFromUrl } from "@/actions/db"; -import { ProfileDrawer } from "./ProfileDrawer"; - -function supportsDVH() { - try { - return CSS.supports("height: 100dvh"); - } catch { - return false; - } -} - -export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) { - const searchParams = useSearchParams(); - - const [hide, setHide] = useState(false); - const [layout, setLayout] = useState<"chat" | "initial">("initial"); - const [value, setValue] = useState(""); - const { width } = useViewport(); - const [isAiLoading, setIsAiLoading] = useState(false); - - const { spaces } = useMemory(); - - // Variable to keep track of the chat history in this session - const [chatHistory, setChatHistory] = useState<ChatHistory[]>([]); - - const [toBeParsed, setToBeParsed] = useState(""); - - const textArea = useRef<HTMLDivElement>(null); - const main = useRef<HTMLDivElement>(null); - - const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); - - const [isStreaming, setIsStreaming] = useState(false); - - useEffect(() => { - const search = searchParams.get("q"); - if (search && search.trim().length > 0) { - setValue(search); - onSend(); - //router.push("/"); - } - }, []); - - useEffect(() => { - // function onResize() { - // if (!main.current || !window.visualViewport) return; - // if ( - // window.visualViewport.height < window.innerHeight + 20 && - // window.visualViewport.height > window.innerHeight - 20 - // ) { - // setHide(false); - // window.scrollTo(0, 0); - // } else { - // setHide(true); - // window.scrollTo(0, document.body.scrollHeight); - // } - // } - // window.visualViewport?.addEventListener("resize", onResize); - // return () => { - // window.visualViewport?.removeEventListener("resize", onResize); - // }; - }, []); - - useEffect(() => { - // Define a function to try parsing the accumulated data - const tryParseAccumulatedData = () => { - // Attempt to parse the "toBeParsed" state as JSON - try { - // Split the accumulated data by the known delimiter "\n\n" - const parts = toBeParsed.split("\n\n"); - let remainingData = ""; - - // Process each part to extract JSON objects - parts.forEach((part, index) => { - try { - const parsedPart = JSON.parse(part.replace("data: ", "")); // Try to parse the part as JSON - - // If the part is the last one and couldn't be parsed, keep it to accumulate more data - if (index === parts.length - 1 && !parsedPart) { - remainingData = part; - } else if (parsedPart && parsedPart.response) { - // Append to chat history in this way: - // If the last message was from the model, append to that message - // Otherwise, Start a new message from the model and append to that - if (chatHistory.length > 0) { - setChatHistory((prev: ChatHistory[]) => { - const lastMessage = prev[prev.length - 1]; - const newParts = [ - ...lastMessage.answer.parts, - { text: parsedPart.response }, - ]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: newParts, - sources: lastMessage.answer.sources, - }, - }, - ]; - }); - } else { - } - } - } catch (error) { - // If parsing fails and it's not the last part, it's a malformed JSON - if (index !== parts.length - 1) { - console.error("Malformed JSON part: ", part); - } else { - // If it's the last part, it may be incomplete, so keep it - remainingData = part; - } - } - }); - - // Update the toBeParsed state to only contain the unparsed remainder - if (remainingData !== toBeParsed) { - setToBeParsed(remainingData); - } - } catch (error) { - console.error("Error parsing accumulated data: ", error); - } - }; - - // Call the parsing function if there's data to be parsed - if (toBeParsed) { - tryParseAccumulatedData(); - } - }, [toBeParsed]); - - const modifyChatHistory = useCallback((old: ChatHistory[]) => { - const final: { role: "user" | "model"; parts: { text: string }[] }[] = []; - old.forEach((chat) => { - final.push({ - role: "user", - parts: [{ text: chat.question }], - }); - final.push({ - role: "model", - parts: chat.answer.parts.map((part) => ({ text: part.text })), - }); - }); - - return final; - }, []); - - const getSearchResults = async () => { - setIsAiLoading(true); - - const _value = value.trim(); - setValue(""); - - setChatHistory((prev) => [ - ...prev, - { - question: _value, - answer: { - parts: [], - sources: [], - }, - }, - ]); - - const sourcesResponse = await fetch( - `/api/chat?sourcesOnly=true&q=${_value}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - console.log("sources", sourcesResponse); - - const sourcesInJson = - getIdsFromSource( - ( - (await sourcesResponse.json()) as { - ids: string[]; - } - ).ids, - ) ?? []; - - const notesInSources = sourcesInJson.filter((urls) => - urls.startsWith("https://notes.supermemory.dhr.wtf/"), - ); - const nonNotes = sourcesInJson.filter((i) => !notesInSources.includes(i)); - - const fetchedTitles = await getMemoriesFromUrl(notesInSources); - - const sources = [ - ...nonNotes.map((n) => ({ isNote: false, source: n ?? "<unnamed>" })), - ...fetchedTitles.map((n) => ({ - isNote: true, - source: n.title ?? "<unnamed>", - })), - ]; - - setIsAiLoading(false); - setChatHistory((prev) => { - const lastMessage = prev[prev.length - 1]; - return [ - ...prev.slice(0, prev.length - 1), - { - ...lastMessage, - answer: { - parts: lastMessage.answer.parts, - sources, - }, - }, - ]; - }); - - const actualSelectedSpaces = selectedSpaces.map( - (space) => spaces.find((s) => s.id === space)?.name ?? "", - ); - - const response = await fetch( - `/api/chat?q=${_value}&spaces=${actualSelectedSpaces.join(",")}`, - { - method: "POST", - body: JSON.stringify({ - chatHistory: modifyChatHistory(chatHistory), - }), - }, - ); - - if (response.status !== 200) { - setIsAiLoading(false); - return; - } - - setIsStreaming(true); - - if (response.body) { - let reader = response.body?.getReader(); - let decoder = new TextDecoder("utf-8"); - let result = ""; - - // @ts-ignore - reader.read().then(function processText({ done, value }) { - if (done) { - setIsAiLoading(false); - setToBeParsed(""); - - return; - } - setToBeParsed((prev) => prev + decoder.decode(value)); - - return reader?.read().then(processText); - }); - } - }; - - const onSend = () => { - if (value.trim().length < 1) return; - setLayout("chat"); - getSearchResults(); - }; - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - return ( - <> - <AnimatePresence mode="wait"> - {layout === "chat" ? ( - <Chat - key="chat" - isLoading={isAiLoading} - chatHistory={chatHistory} - sidebarOpen={sidebarOpen} - askQuestion={onSend} - setValue={setValue} - value={value} - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - ) : ( - <main - key="intial" - data-sidebar-open={sidebarOpen} - ref={main} - className={cn( - "sidebar relative flex w-full flex-col items-end justify-center gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - hide ? "" : "main-hidden", - )} - > - <Image - className="absolute right-10 top-10 hidden rounded-md md:block" - src="/icons/logo_bw_without_bg.png" - alt="Smort logo" - width={50} - height={50} - /> - <div className="absolute right-10 top-10 block md:hidden"> - {width <= 768 && <ProfileDrawer hide={hide} />} - </div> - <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl font-bold tracking-tight md:mt-0"> - Ask your second brain - </h1> - - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525] md:hidden" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:hidden md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2 md:hidden"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - - <Textarea2 - ref={textArea} - exit={{ - opacity: 0, - y: 50, - }} - transition={{ - type: "tween", - duration: 0.2, - }} - textAreaProps={{ - placeholder: "Ask your second brain...", - className: - "h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]", - value, - autoFocus: true, - onChange: (e) => setValue(e.target.value), - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - onSend(); - } - }, - }} - className="hidden md:flex" - > - <div className="text-rgray-11/70 flex h-full w-fit items-center justify-center pl-0 md:w-full md:p-2"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - className="hidden md:flex" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <button - onClick={onSend} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:ml-auto md:mt-0" - > - <ArrowRight className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </main> - )} - {width <= 768 && <MemoryDrawer hide={hide} />} - </AnimatePresence> - </> - ); -} - -export function Chat({ - sidebarOpen, - chatHistory, - isLoading = false, - askQuestion, - setValue, - value, - selectedSpaces, - setSelectedSpaces, -}: { - sidebarOpen: boolean; - isLoading?: boolean; - chatHistory: ChatHistory[]; - askQuestion: () => void; - setValue: (value: string) => void; - value: string; - selectedSpaces: number[]; - setSelectedSpaces: React.Dispatch<React.SetStateAction<number[]>>; -}) { - const textArea = useRef<HTMLDivElement>(null); - - function onValueChange(e: React.ChangeEvent<HTMLTextAreaElement>) { - const value = e.target.value; - setValue(value); - const lines = countLines(e.target); - e.target.rows = Math.min(5, lines); - } - - const { width } = useViewport(); - - return ( - <main - data-sidebar-open={sidebarOpen} - className={cn( - "sidebar relative flex w-full flex-col items-end gap-5 px-5 pt-5 transition-[padding-left,padding-top,padding-right] delay-200 duration-200 md:items-center md:gap-10 md:px-72 [&[data-sidebar-open='true']]:pr-10 [&[data-sidebar-open='true']]:delay-0 md:[&[data-sidebar-open='true']]:pl-[calc(2.5rem+30vw)]", - )} - > - <div className="absolute right-10 top-10 z-[100] block md:hidden"> - {width <= 768 && <ProfileDrawer />} - </div> - <div className="scrollbar-none h-[70vh] w-full overflow-y-auto px-2 md:h-screen md:px-5"> - {chatHistory.map((msg, i) => ( - <ChatMessage index={i} key={i} isLast={i === chatHistory.length - 1}> - <ChatQuestion>{msg.question}</ChatQuestion> - <ChatAnswer - loading={i === chatHistory.length - 1 ? isLoading : false} - sources={msg.answer.sources} - > - {msg.answer.parts - .map((part) => part.text) - .join("") - .replace("</s>", "")} - </ChatAnswer> - </ChatMessage> - ))} - </div> - <div className="from-rgray-2 via-rgray-2 to-rgray-2/0 absolute bottom-0 left-0 w-full bg-gradient-to-t" /> - <div - data-sidebar-open={sidebarOpen} - className="absolute flex w-full items-center justify-center" - > - <div className="animate-from-top bottom-padding fixed left-1/2 mt-auto flex w-[90%] -translate-x-1/2 flex-col items-center justify-center gap-2 md:bottom-10 md:left-[auto] md:w-[50%] md:translate-x-0"> - <FilterSpaces - name={"Filter"} - onClose={() => { - textArea.current?.querySelector("textarea")?.focus(); - }} - side="top" - align="start" - className="mr-auto bg-[#252525]" - selectedSpaces={selectedSpaces} - setSelectedSpaces={setSelectedSpaces} - /> - <Textarea2 - ref={textArea} - className="bg-rgray-2 h-auto w-full flex-row items-start justify-center overflow-auto px-3 md:items-center md:justify-center" - textAreaProps={{ - placeholder: "Ask your SuperMemory...", - className: - "overflow-auto h-auto p-3 md:resize-none text-lg w-auto resize-y text-rgray-11 w-full", - value, - rows: 1, - autoFocus: true, - onChange: onValueChange, - onKeyDown: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault(); - askQuestion(); - } - }, - }} - > - <div className="text-rgray-11/70 ml-auto mt-auto flex h-full w-min items-center justify-center pb-3 pr-2"> - <button - onClick={askQuestion} - disabled={value.trim().length < 1} - className="text-rgray-11/70 bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-4 mt-auto flex items-center justify-center rounded-full p-2 ring-2 ring-transparent transition-[filter] focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - > - <ArrowUp className="h-5 w-5" /> - </button> - </div> - </Textarea2> - </div> - </div> - </main> - ); -} diff --git a/apps/web/src/components/MemoryDrawer.tsx b/apps/web/src/components/MemoryDrawer.tsx deleted file mode 100644 index 14283281..00000000 --- a/apps/web/src/components/MemoryDrawer.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { useRef, useState } from "react"; -import { Drawer, DrawerContent, DrawerOverlay } from "./ui/drawer"; -import { MemoryIcon } from "@/assets/Memories"; -import { cn } from "@/lib/utils"; -import { MemoriesBar } from "./Sidebar/MemoriesBar"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function MemoryDrawer({ className, hide = false, ...props }: Props) { - const [activeSnapPoint, setActiveSnapPoint] = useState< - number | null | string - >(0.1); - - return ( - <Drawer - snapPoints={[0.1, 0.9]} - activeSnapPoint={activeSnapPoint} - shouldScaleBackground={false} - setActiveSnapPoint={setActiveSnapPoint} - open={true} - dismissible={false} - modal={false} - > - <DrawerContent - overlay={false} - data-expanded={activeSnapPoint === 0.9} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 h-full w-screen border transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - handle={false} - > - <button - onClick={() => - setActiveSnapPoint((prev) => (prev === 0.9 ? 0.1 : 0.9)) - } - className="bg-rgray-4 border-rgray-6 text-rgray-11 absolute left-1/2 top-0 flex w-fit -translate-x-1/2 -translate-y-1/2 items-center justify-center gap-2 rounded-md border px-3 py-2" - > - <MemoryIcon className="h-7 w-7" /> - Memories - </button> - <div className="h-full w-full overflow-y-auto"> - <MemoriesBar isOpen={true} /> - </div> - </DrawerContent> - <DrawerOverlay className="relative bg-transparent" /> - </Drawer> - ); -} diff --git a/apps/web/src/components/ProfileDrawer.tsx b/apps/web/src/components/ProfileDrawer.tsx deleted file mode 100644 index bdb32e03..00000000 --- a/apps/web/src/components/ProfileDrawer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useRef, useState } from "react"; -import { - Drawer, - DrawerContent, - DrawerOverlay, - DrawerTrigger, -} from "./ui/drawer"; -import { cn } from "@/lib/utils"; -import { SettingsTab } from "./Sidebar/SettingsTab"; -import { useSession } from "next-auth/react"; - -export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - hide?: boolean; -} - -export function ProfileDrawer({ className, hide = false, ...props }: Props) { - const { data: session } = useSession(); - - return ( - <Drawer snapPoints={[0.9]} shouldScaleBackground={false}> - <DrawerTrigger> - <img - src={session?.user?.image ?? "/icons/white_without_bg.png"} - className="h-10 w-10 rounded-full" - /> - </DrawerTrigger> - <DrawerContent - overlay={false} - className={cn( - "border-rgray-6 DrawerContent data-[expanded=true]:bg-rgray-3 z-[101] h-full w-screen border bg-white transition-[background] focus-visible:outline-none", - hide ? "hidden" : "", - )} - > - <div className="h-[85vh] w-full overflow-y-auto"> - <SettingsTab open={true} /> - </div> - </DrawerContent> - </Drawer> - ); -} diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx deleted file mode 100644 index d348814e..00000000 --- a/apps/web/src/components/SearchResults.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import React from "react"; -import { Card, CardContent } from "./ui/card"; -import Markdown from "react-markdown"; -import remarkGfm from "remark-gfm"; - -function SearchResults({ - aiResponse, - sources, -}: { - aiResponse: string; - sources: string[]; -}) { - return ( - <div - style={{ - backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`, - }} - className="mx-auto mt-4 w-full max-w-2xl space-y-6 rounded-xl border px-4 py-6" - > - <div className="text-start"> - <div className="text-xl text-black"> - <Markdown remarkPlugins={[remarkGfm]}> - {aiResponse.replace("</s>", "")} - </Markdown> - </div> - </div> - <div className="grid gap-6"> - {sources.map((value, index) => ( - <Card key={index}> - <CardContent className="space-y-2">{value}</CardContent> - </Card> - ))} - </div> - </div> - ); -} - -export default SearchResults; 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> - ); -} diff --git a/apps/web/src/components/WordMark.tsx b/apps/web/src/components/WordMark.tsx deleted file mode 100644 index eb55647c..00000000 --- a/apps/web/src/components/WordMark.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { cn } from "@/lib/utils"; -import React from "react"; - -function WordMark({ className }: { className?: string }) { - return ( - <span className={cn(`text-xl font-bold tracking-tight ${className}`)}> - smort. - </span> - ); -} - -export default WordMark; diff --git a/apps/web/src/components/dev/SessionProviderWrapper.tsx b/apps/web/src/components/dev/SessionProviderWrapper.tsx deleted file mode 100644 index 71f77886..00000000 --- a/apps/web/src/components/dev/SessionProviderWrapper.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { SessionProvider } from "next-auth/react"; -import React from "react"; - -function SessionProviderWrapper({ children }: { children: React.ReactNode }) { - if (typeof window === "undefined") { - return <>{children}</>; - } else { - return <SessionProvider>{children}</SessionProvider>; - } -} - -export default SessionProviderWrapper; diff --git a/apps/web/src/components/dev/tailwindindicator.tsx b/apps/web/src/components/dev/tailwindindicator.tsx deleted file mode 100644 index fd70276d..00000000 --- a/apps/web/src/components/dev/tailwindindicator.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export function TailwindIndicator() { - if (process.env.NODE_ENV === "production") return null; - - return ( - <div> - <div className="fixed bottom-1 left-1 z-[999] flex size-6 items-center justify-center rounded-full border-2 bg-white p-3 font-mono text-xs text-black"> - <div className="block sm:hidden">xs</div> - <div className="hidden sm:block md:hidden">sm</div> - <div className="hidden md:block lg:hidden">md</div> - <div className="hidden lg:block xl:hidden">lg</div> - <div className="hidden xl:block 2xl:hidden">xl</div> - <div className="hidden 2xl:block">2xl</div> - </div> - </div> - ); -} diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx deleted file mode 100644 index b36abf28..00000000 --- a/apps/web/src/components/ui/avatar.tsx +++ /dev/null @@ -1,50 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as AvatarPrimitive from "@radix-ui/react-avatar"; - -import { cn } from "@/lib/utils"; - -const Avatar = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Root - ref={ref} - className={cn( - "relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", - className, - )} - {...props} - /> -)); -Avatar.displayName = AvatarPrimitive.Root.displayName; - -const AvatarImage = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Image>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Image - ref={ref} - className={cn("aspect-square h-full w-full", className)} - {...props} - /> -)); -AvatarImage.displayName = AvatarPrimitive.Image.displayName; - -const AvatarFallback = React.forwardRef< - React.ElementRef<typeof AvatarPrimitive.Fallback>, - React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback> ->(({ className, ...props }, ref) => ( - <AvatarPrimitive.Fallback - ref={ref} - className={cn( - "flex h-full w-full items-center justify-center rounded-full bg-gray-100", - className, - )} - {...props} - /> -)); -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; - -export { Avatar, AvatarImage, AvatarFallback }; diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx deleted file mode 100644 index fa390bec..00000000 --- a/apps/web/src/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes<HTMLDivElement>, - VariantProps<typeof badgeVariants> {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( - <div className={cn(badgeVariants({ variant }), className)} {...props} /> - ); -} - -export { Badge, badgeVariants }; diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx deleted file mode 100644 index 24fa903e..00000000 --- a/apps/web/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes<HTMLButtonElement>, - VariantProps<typeof buttonVariants> { - asChild?: boolean; -} - -const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - <button - className={cn(buttonVariants({ variant, size, className }))} - ref={ref} - {...props} - /> - ); - }, -); -Button.displayName = "Button"; - -export { Button, buttonVariants }; diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx deleted file mode 100644 index e98d500c..00000000 --- a/apps/web/src/components/ui/card.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn( - "rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50", - className, - )} - {...props} - /> -)); -Card.displayName = "Card"; - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex flex-col space-y-1.5 p-6", className)} - {...props} - /> -)); -CardHeader.displayName = "CardHeader"; - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLHeadingElement> ->(({ className, ...props }, ref) => ( - <h3 - ref={ref} - className={cn( - "text-2xl font-semibold leading-none tracking-tight", - className, - )} - {...props} - /> -)); -CardTitle.displayName = "CardTitle"; - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes<HTMLParagraphElement> ->(({ className, ...props }, ref) => ( - <p - ref={ref} - className={cn("text-sm text-gray-500 dark:text-gray-400", className)} - {...props} - /> -)); -CardDescription.displayName = "CardDescription"; - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div ref={ref} className={cn("p-6 pt-0", className)} {...props} /> -)); -CardContent.displayName = "CardContent"; - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes<HTMLDivElement> ->(({ className, ...props }, ref) => ( - <div - ref={ref} - className={cn("flex items-center p-6 pt-0", className)} - {...props} - /> -)); -CardFooter.displayName = "CardFooter"; - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardDescription, - CardContent, -}; diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx deleted file mode 100644 index afc2cf46..00000000 --- a/apps/web/src/components/ui/command.tsx +++ /dev/null @@ -1,161 +0,0 @@ -"use client"; - -import * as React from "react"; -import { type DialogProps } from "@radix-ui/react-dialog"; -import { Command as CommandPrimitive } from "cmdk"; -import { Loader, Search } from "lucide-react"; - -import { cn } from "@/lib/utils"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; - -const Command = React.forwardRef< - React.ElementRef<typeof CommandPrimitive>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive> ->(({ className, ...props }, ref) => ( - <CommandPrimitive - ref={ref} - className={cn( - "bg-rgray-3 text-rgray-11 flex h-full w-full flex-col overflow-hidden rounded-md focus-visible:outline-none [&>[cmdk-list-sizer]]:max-h-[250px] [&>[cmdk-list-sizer]]:overflow-y-scroll", - className, - )} - {...props} - /> -)); -Command.displayName = CommandPrimitive.displayName; - -interface CommandDialogProps extends DialogProps {} - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - <Dialog {...props}> - <DialogContent className="overflow-hidden p-0 shadow-lg"> - <Command className="[&_[cmdk-group-heading]]:text-rgray-11 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"> - {children} - </Command> - </DialogContent> - </Dialog> - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Input>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input> & { - isSearching?: boolean; - } ->(({ className, isSearching = false, ...props }, ref) => ( - <div - className="border-rgray-6 flex items-center border-b px-3" - cmdk-input-wrapper="" - > - {isSearching ? ( - <Loader className="shrink-9 mr-2 h-4 w-4 animate-spin opacity-50" /> - ) : ( - <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> - )} - <CommandPrimitive.Input - ref={ref} - className={cn( - "placeholder:text-rgray-11/50 flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - </div> -)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.List>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.List> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.List - ref={ref} - className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)} - {...props} - /> -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Empty>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty> ->((props, ref) => ( - <CommandPrimitive.Empty - ref={ref} - className="py-6 text-center text-sm" - {...props} - /> -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Group>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Group - ref={ref} - className={cn( - "text-rgray-12 [&_[cmdk-group-heading]]:text-rgray-11 overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium", - className, - )} - {...props} - /> -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Separator - ref={ref} - className={cn("bg-rgray-3 -mx-1 h-px", className)} - {...props} - /> -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef<typeof CommandPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item> ->(({ className, ...props }, ref) => ( - <CommandPrimitive.Item - ref={ref} - className={cn( - "aria-selected:bg-rgray-5 aria-selected:text-rgray-12 relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm opacity-70 outline-none data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50", - className, - )} - {...props} - /> -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("text-gray-11 ml-auto text-xs tracking-widest", className)} - {...props} - /> - ); -}; -CommandShortcut.displayName = "CommandShortcut"; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/web/src/components/ui/dialog.tsx b/apps/web/src/components/ui/dialog.tsx deleted file mode 100644 index 0da54769..00000000 --- a/apps/web/src/components/ui/dialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const Dialog = DialogPrimitive.Root; - -const DialogTrigger = DialogPrimitive.Trigger; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogClose = DialogPrimitive.Close; - -const DialogOverlay = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Overlay - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80", - className, - )} - {...props} - /> -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> ->(({ className, children, ...props }, ref) => ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Content - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] border-rgray-6 fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] rounded-lg border bg-[#F4F3F2] p-6 text-black shadow-lg duration-200", - className, - )} - {...props} - > - {children} - <DialogPrimitive.Close className="ring-offset-rgray-2 focus:ring-rgray-7 absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:text-black"> - <X className="h-4 w-4" /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - </DialogPrimitive.Content> - </DialogPortal> -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("flex flex-col space-y-1.5 text-left", className)} - {...props} - /> -); -DialogHeader.displayName = "DialogHeader"; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn( - "mt-5 flex flex-row sm:flex-row sm:justify-end sm:space-x-2", - className, - )} - {...props} - /> -); -DialogFooter.displayName = "DialogFooter"; - -const DialogTitle = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Title - ref={ref} - className={cn( - "mb-1 text-xl font-medium leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef<typeof DialogPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DialogPrimitive.Description - ref={ref} - className={cn("text-sm text-slate-800", className)} - {...props} - /> -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/web/src/components/ui/drawer.tsx b/apps/web/src/components/ui/drawer.tsx deleted file mode 100644 index 8ba01253..00000000 --- a/apps/web/src/components/ui/drawer.tsx +++ /dev/null @@ -1,124 +0,0 @@ -"use client"; - -import * as React from "react"; -import { Drawer as DrawerPrimitive } from "vaul"; - -import { cn } from "@/lib/utils"; - -const Drawer = ({ - shouldScaleBackground = true, - ...props -}: React.ComponentProps<typeof DrawerPrimitive.Root>) => ( - <DrawerPrimitive.Root - shouldScaleBackground={shouldScaleBackground} - {...props} - /> -); -Drawer.displayName = "Drawer"; - -const DrawerTrigger = DrawerPrimitive.Trigger; - -const DrawerPortal = DrawerPrimitive.Portal; - -const DrawerClose = DrawerPrimitive.Close; - -const DrawerOverlay = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Overlay>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Overlay - ref={ref} - data-drawer-overlay - className={cn("fixed inset-0 z-50 bg-black/80", className)} - {...props} - /> -)); -DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName; - -const DrawerContent = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content> & { - overlay?: boolean; - handle?: boolean; - } ->(({ className, children, overlay = true, handle = true, ...props }, ref) => ( - <DrawerPortal> - {overlay && <DrawerOverlay />} - <DrawerPrimitive.Content - ref={ref} - className={cn( - "border-rgray-6 bg-rgray-2 fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border", - className, - )} - {...props} - > - {handle && ( - <div className="bg-rgray-4 mx-auto mb-1 h-2 w-[100px] rounded-full " /> - )} - {children} - </DrawerPrimitive.Content> - </DrawerPortal> -)); -DrawerContent.displayName = "DrawerContent"; - -const DrawerHeader = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} - {...props} - /> -); -DrawerHeader.displayName = "DrawerHeader"; - -const DrawerFooter = ({ - className, - ...props -}: React.HTMLAttributes<HTMLDivElement>) => ( - <div - className={cn("mt-auto flex flex-col gap-2 p-4", className)} - {...props} - /> -); -DrawerFooter.displayName = "DrawerFooter"; - -const DrawerTitle = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Title>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Title - ref={ref} - className={cn( - "text-rgray-12 text-xl font-medium leading-none tracking-tight", - className, - )} - {...props} - /> -)); -DrawerTitle.displayName = DrawerPrimitive.Title.displayName; - -const DrawerDescription = React.forwardRef< - React.ElementRef<typeof DrawerPrimitive.Description>, - React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description> ->(({ className, ...props }, ref) => ( - <DrawerPrimitive.Description - ref={ref} - className={cn("text-rgray-11 text-md", className)} - {...props} - /> -)); -DrawerDescription.displayName = DrawerPrimitive.Description.displayName; - -export { - Drawer, - DrawerPortal, - DrawerOverlay, - DrawerTrigger, - DrawerClose, - DrawerContent, - DrawerHeader, - DrawerFooter, - DrawerTitle, - DrawerDescription, -}; diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index fbe2d99c..00000000 --- a/apps/web/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,200 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; -import { Check, ChevronRight, Circle } from "lucide-react"; - -import { cn } from "@/lib/utils"; - -const DropdownMenu = DropdownMenuPrimitive.Root; - -const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; - -const DropdownMenuGroup = DropdownMenuPrimitive.Group; - -const DropdownMenuPortal = DropdownMenuPrimitive.Portal; - -const DropdownMenuSub = DropdownMenuPrimitive.Sub; - -const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; - -const DropdownMenuSubTrigger = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & { - inset?: boolean; - } ->(({ className, inset, children, ...props }, ref) => ( - <DropdownMenuPrimitive.SubTrigger - ref={ref} - className={cn( - "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-gray-100 data-[state=open]:bg-gray-100 dark:focus:bg-gray-800 dark:data-[state=open]:bg-gray-800", - inset && "pl-8", - className, - )} - {...props} - > - {children} - <ChevronRight className="ml-auto h-4 w-4" /> - </DropdownMenuPrimitive.SubTrigger> -)); -DropdownMenuSubTrigger.displayName = - DropdownMenuPrimitive.SubTrigger.displayName; - -const DropdownMenuSubContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.SubContent>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.SubContent - ref={ref} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-gray-200 bg-white p-1 text-gray-950 shadow-lg", - className, - )} - {...props} - /> -)); -DropdownMenuSubContent.displayName = - DropdownMenuPrimitive.SubContent.displayName; - -const DropdownMenuContent = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> ->(({ className, sideOffset = 4, ...props }, ref) => ( - <DropdownMenuPrimitive.Portal> - <DropdownMenuPrimitive.Content - ref={ref} - sideOffset={sideOffset} - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 border-rgray-6 z-50 min-w-[9rem] overflow-hidden rounded-md border bg-white p-1 text-black shadow-md", - className, - )} - {...props} - /> - </DropdownMenuPrimitive.Portal> -)); -DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; - -const DropdownMenuItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Item>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Item - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-stone-200 focus:text-slate-800 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 ", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; - -const DropdownMenuCheckboxItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem> ->(({ className, children, checked, ...props }, ref) => ( - <DropdownMenuPrimitive.CheckboxItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - checked={checked} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Check className="h-4 w-4" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.CheckboxItem> -)); -DropdownMenuCheckboxItem.displayName = - DropdownMenuPrimitive.CheckboxItem.displayName; - -const DropdownMenuRadioItem = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem> ->(({ className, children, ...props }, ref) => ( - <DropdownMenuPrimitive.RadioItem - ref={ref} - className={cn( - "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-gray-100 focus:text-gray-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-gray-800 dark:focus:text-gray-50", - className, - )} - {...props} - > - <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> - <DropdownMenuPrimitive.ItemIndicator> - <Circle className="h-2 w-2 fill-current" /> - </DropdownMenuPrimitive.ItemIndicator> - </span> - {children} - </DropdownMenuPrimitive.RadioItem> -)); -DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; - -const DropdownMenuLabel = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Label>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & { - inset?: boolean; - } ->(({ className, inset, ...props }, ref) => ( - <DropdownMenuPrimitive.Label - ref={ref} - className={cn( - "px-2 py-1.5 text-sm font-semibold", - inset && "pl-8", - className, - )} - {...props} - /> -)); -DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; - -const DropdownMenuSeparator = React.forwardRef< - React.ElementRef<typeof DropdownMenuPrimitive.Separator>, - React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator> ->(({ className, ...props }, ref) => ( - <DropdownMenuPrimitive.Separator - ref={ref} - className={cn("-mx-1 my-1 h-px bg-gray-100", className)} - {...props} - /> -)); -DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; - -const DropdownMenuShortcut = ({ - className, - ...props -}: React.HTMLAttributes<HTMLSpanElement>) => { - return ( - <span - className={cn("ml-auto text-xs tracking-widest opacity-60", className)} - {...props} - /> - ); -}; -DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; - -export { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuGroup, - DropdownMenuPortal, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuRadioGroup, -}; diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx deleted file mode 100644 index 9d925512..00000000 --- a/apps/web/src/components/ui/input.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; - -export interface InputProps - extends React.InputHTMLAttributes<HTMLInputElement> {} - -const Input = React.forwardRef<HTMLInputElement, InputProps>( - ({ className, type, ...props }, ref) => { - return ( - <input - type={type} - className={cn( - "border-rgray-6 focus-visible:ring-rgray-7 flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); - -export interface InputWithIconProps - extends React.InputHTMLAttributes<HTMLInputElement> { - icon: React.ReactNode; -} - -const InputWithIcon = React.forwardRef<HTMLInputElement, InputWithIconProps>( - ({ className, type, icon, ...props }, ref) => { - return ( - <div - className={cn( - "border-rgray-1/70 text-rgray-11 focus-within:ring-rgray-7 flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 ", // TODO: change to black - className, - )} - > - {icon} - <input - type={type} - className={ - "w-full bg-transparent font-normal file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-black/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" - } - ref={ref} - {...props} - /> - </div> - ); - }, -); -InputWithIcon.displayName = "Input"; - -export { Input, InputWithIcon }; diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx deleted file mode 100644 index 84f8b0c7..00000000 --- a/apps/web/src/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as LabelPrimitive from "@radix-ui/react-label"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70", -); - -const Label = React.forwardRef< - React.ElementRef<typeof LabelPrimitive.Root>, - React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & - VariantProps<typeof labelVariants> ->(({ className, ...props }, ref) => ( - <LabelPrimitive.Root - ref={ref} - className={cn(labelVariants(), className)} - {...props} - /> -)); -Label.displayName = LabelPrimitive.Root.displayName; - -export { Label }; diff --git a/apps/web/src/components/ui/popover.tsx b/apps/web/src/components/ui/popover.tsx deleted file mode 100644 index cabe76a9..00000000 --- a/apps/web/src/components/ui/popover.tsx +++ /dev/null @@ -1,40 +0,0 @@ -"use client"; - -import * as React from "react"; -import * as PopoverPrimitive from "@radix-ui/react-popover"; - -import { cn } from "@/lib/utils"; - -const Popover = PopoverPrimitive.Root; - -const PopoverTrigger = PopoverPrimitive.Trigger; - -const PopoverContent = React.forwardRef< - React.ElementRef<typeof PopoverPrimitive.Content>, - React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & { - animate?: boolean; - } ->( - ( - { className, align = "center", animate = true, sideOffset = 4, ...props }, - ref, - ) => ( - <PopoverPrimitive.Portal> - <PopoverPrimitive.Content - ref={ref} - align={align} - sideOffset={sideOffset} - className={cn( - "border-rgray-6 bg-rgray-3 text-rgray-11 z-50 w-72 rounded-md border p-4 shadow-md outline-none", - animate && - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - className, - )} - {...props} - /> - </PopoverPrimitive.Portal> - ), -); -PopoverContent.displayName = PopoverPrimitive.Content.displayName; - -export { Popover, PopoverTrigger, PopoverContent }; diff --git a/apps/web/src/components/ui/textarea.tsx b/apps/web/src/components/ui/textarea.tsx deleted file mode 100644 index 3b2c9ddd..00000000 --- a/apps/web/src/components/ui/textarea.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from "react"; - -import { cn } from "@/lib/utils"; -import { HTMLMotionProps, motion } from "framer-motion"; - -export interface TextareaProps - extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {} - -const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( - ({ className, ...props }, ref) => { - return ( - <textarea - className={cn( - "border-rgray-6 text-rgray-11 placeholder:text-rgray-11/70 focus-within:ring-rgray-7 flex min-h-[80px] w-full rounded-md border bg-transparent px-3 py-2 text-sm font-normal transition focus-within:outline-none focus-within:ring-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - ref={ref} - {...props} - /> - ); - }, -); -Textarea.displayName = "Textarea"; - -export interface Textarea2Props extends HTMLMotionProps<"div"> { - textAreaProps?: TextareaProps; - children: React.ReactNode | React.ReactNode[]; -} - -const Textarea2 = React.forwardRef<HTMLDivElement, Textarea2Props>( - ({ className, children, textAreaProps: _textAreaProps, ...props }, ref) => { - const { className: textAreaClassName, ...textAreaProps } = - _textAreaProps || {}; - return ( - <motion.div - ref={ref} - className={cn( - "border-rgray-6 text-rgray-11 has-[textarea:focus-visible]:ring-rgray-7 flex h-auto min-h-[80px] w-full flex-col items-start justify-center rounded-md border bg-transparent px-3 py-2 text-sm transition has-[textarea:focus-visible]:ring-2", - className, - )} - {...props} - > - <textarea - className={cn( - "text-rgray-11 h-full w-full resize-none bg-transparent placeholder:text-white/50 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50", - textAreaClassName, - )} - {...textAreaProps} - /> - {children} - </motion.div> - ); - }, -); -Textarea2.displayName = "Textarea2"; - -export { Textarea, Textarea2 }; |