aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src/components
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-04-13 09:55:29 -0700
committerDhravya <[email protected]>2024-04-13 09:55:29 -0700
commit57e699a6ee35b161cf69aa82bec6c50f114b1055 (patch)
treea40d3c983c4d7661797e1c18591019cc09c3f083 /apps/web/src/components
parentmerge (diff)
parentfix edge case for getting metadata (diff)
downloadsupermemory-57e699a6ee35b161cf69aa82bec6c50f114b1055.tar.xz
supermemory-57e699a6ee35b161cf69aa82bec6c50f114b1055.zip
conflicts
Diffstat (limited to 'apps/web/src/components')
-rw-r--r--apps/web/src/components/Sidebar/AddMemoryDialog.tsx163
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx172
-rw-r--r--apps/web/src/components/Sidebar/MemoriesBar.tsx110
-rw-r--r--apps/web/src/components/ui/command.tsx15
4 files changed, 347 insertions, 113 deletions
diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx
index 08b9a750..d0523581 100644
--- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx
+++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx
@@ -10,8 +10,12 @@ import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Markdown } from "tiptap-markdown";
import { useEffect, useRef, useState } from "react";
-import { FilterSpaces } from "./FilterCombobox";
+import { FilterMemories, FilterSpaces } from "./FilterCombobox";
import { useMemory } from "@/contexts/MemoryContext";
+import { Loader, Plus, X } from "lucide-react";
+import { StoredContent } from "@/server/db/schema";
+import { cleanUrl } from "@/lib/utils";
+import { motion } from "framer-motion";
export function AddMemoryPage() {
const { addMemory } = useMemory();
@@ -73,6 +77,8 @@ export function AddMemoryPage() {
}
export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
+ const { addMemory } = useMemory();
+
const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]);
const inputRef = useRef<HTMLInputElement>(null);
@@ -112,6 +118,7 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
placeholder="Title of the note"
data-modal-autofocus
value={name}
+ disabled={loading}
onChange={(e) => setName(e.target.value)}
/>
<Editor
@@ -134,16 +141,41 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
<button
onClick={() => {
if (check()) {
- closeDialog();
+ setLoading(true);
+ addMemory(
+ {
+ content,
+ title: name,
+ type: "note",
+ url: "https://notes.supermemory.dhr.wtf/",
+ image: "",
+ savedAt: new Date(),
+ },
+ selectedSpacesId,
+ ).then(closeDialog);
}
}}
- className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"
+ 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"
>
- Add
+ <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}
- 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={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>
@@ -153,6 +185,37 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) {
}
export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
+ const { addSpace } = useMemory();
+
+ const inputRef = useRef<HTMLInputElement>(null);
+ const [name, setName] = useState("");
+
+ const [loading, setLoading] = useState(false);
+
+ const [selected, setSelected] = useState<StoredContent[]>([]);
+
+ function check(): boolean {
+ const data = {
+ name: name.trim(),
+ };
+ if (!data.name || data.name.length < 1) {
+ if (!inputRef.current) {
+ alert("Please enter a name for the note");
+ return false;
+ }
+ inputRef.current.value = "";
+ inputRef.current.placeholder = "Please enter a title for the space";
+ inputRef.current.dataset["error"] = "true";
+ setTimeout(() => {
+ inputRef.current!.placeholder = "Enter the name of the space";
+ inputRef.current!.dataset["error"] = "false";
+ }, 500);
+ inputRef.current.focus();
+ return false;
+ }
+ return true;
+ }
+
return (
<div className="md:w-[40vw]">
<DialogHeader>
@@ -160,23 +223,99 @@ export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) {
</DialogHeader>
<Label className="mt-5 block">Name</Label>
<Input
+ ref={inputRef}
placeholder="Enter the name of the space"
type="url"
data-modal-autofocus
- className="bg-rgray-4 mt-2 w-full"
+ value={name}
+ disabled={loading}
+ onChange={(e) => setName(e.target.value)}
+ className="bg-rgray-4 mt-2 w-full placeholder:transition placeholder:duration-500 data-[error=true]:placeholder:text-red-400 focus-visible:data-[error=true]:ring-red-500/10"
/>
- <Label className="mt-5 block">Memories</Label>
+ {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>
- <DialogClose
+ <FilterMemories
+ selected={selected}
+ setSelected={setSelected}
+ disabled={loading}
+ className="hover:bg-rgray-4 focus-visible:bg-rgray-4 mr-auto bg-white/5 disabled:cursor-not-allowed disabled:opacity-70"
+ >
+ <Plus className="h-5 w-5" />
+ Memory
+ </FilterMemories>
+ <button
type={undefined}
- className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2"
+ onClick={() => {
+ if (check()) {
+ setLoading(true);
+ addSpace(
+ name,
+ selected.map((s) => s.id),
+ ).then(() => closeDialog());
+ }
+ }}
+ disabled={loading}
+ className="bg-rgray-4 hover:bg-rgray-5 focus-visible:bg-rgray-5 focus-visible:ring-rgray-7 relative rounded-md px-4 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
+ >
+ <motion.div
+ initial={{ x: "-50%", y: "-100%" }}
+ animate={loading && { y: "-50%", x: "-50%", opacity: 1 }}
+ className="absolute left-1/2 top-1/2 -translate-x-1/2 translate-y-[-100%] opacity-0"
+ >
+ <Loader className="text-rgray-11 h-5 w-5 animate-spin" />
+ </motion.div>
+ <motion.div
+ initial={{ y: "0%" }}
+ animate={loading && { opacity: 0, y: "30%" }}
+ >
+ Add
+ </motion.div>
+ </button>
+ <DialogClose
+ disabled={loading}
+ className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-70"
>
- Add
- </DialogClose>
- <DialogClose className="hover:bg-rgray-4 focus-visible:bg-rgray-4 focus-visible:ring-rgray-7 rounded-md px-3 py-2 ring-transparent transition focus-visible:outline-none focus-visible:ring-2">
Cancel
</DialogClose>
</DialogFooter>
</div>
);
}
+
+export function MemorySelectedItem({
+ id,
+ title,
+ url,
+ image,
+ onRemove,
+}: StoredContent & { onRemove: () => void }) {
+ return (
+ <div className="hover:bg-rgray-4 focus-within-bg-rgray-4 flex w-full items-center justify-start gap-2 rounded-md p-1 px-2 text-sm [&:hover>[data-icon]]:block [&:hover>img]:hidden">
+ <img src={image ?? "/icons/logo_without_bg.png"} className="h-5 w-5" />
+ <button
+ onClick={onRemove}
+ data-icon
+ className="m-0 hidden h-5 w-5 p-0 focus-visible:outline-none"
+ >
+ <X className="h-5 w-5 scale-90" />
+ </button>
+ <span>{title}</span>
+ <span className="ml-auto block opacity-50">{cleanUrl(url)}</span>
+ </div>
+ );
+}
diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx
index bd432215..80319ab1 100644
--- a/apps/web/src/components/Sidebar/FilterCombobox.tsx
+++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx
@@ -20,9 +20,12 @@ import {
} from "@/components/ui/popover";
import { SpaceIcon } from "@/assets/Memories";
import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
-import { useMemory } from "@/contexts/MemoryContext";
+import { SearchResult, useMemory } from "@/contexts/MemoryContext";
+import { useDebounce } from "@/hooks/useDebounce";
+import { StoredContent } from "@/server/db/schema";
-export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
+export interface FilterSpacesProps
+ extends React.ButtonHTMLAttributes<HTMLButtonElement> {
side?: "top" | "bottom";
align?: "end" | "start" | "center";
onClose?: () => void;
@@ -42,7 +45,7 @@ export function FilterSpaces({
setSelectedSpaces,
name,
...props
-}: Props) {
+}: FilterSpacesProps) {
const { spaces } = useMemory();
const [open, setOpen] = React.useState(false);
@@ -150,26 +153,57 @@ export function FilterSpaces({
);
}
+export type FilterMemoriesProps = {
+ side?: "top" | "bottom";
+ align?: "end" | "start" | "center";
+ onClose?: () => void;
+ selected: StoredContent[];
+ setSelected: React.Dispatch<React.SetStateAction<StoredContent[]>>;
+} & React.ButtonHTMLAttributes<HTMLButtonElement>;
+
export function FilterMemories({
className,
side = "bottom",
align = "center",
onClose,
- selectedSpaces,
- setSelectedSpaces,
- name,
+ selected,
+ setSelected,
...props
-}: Props) {
- const { spaces } = useMemory();
+}: FilterMemoriesProps) {
+ const { search } = useMemory();
+
const [open, setOpen] = React.useState(false);
+ const [searchQuery, setSearchQuery] = React.useState("");
+ const query = useDebounce(searchQuery, 500);
- const sortedSpaces = spaces.sort(({ id: a }, { id: b }) =>
- selectedSpaces.includes(a) && !selectedSpaces.includes(b)
- ? -1
- : selectedSpaces.includes(b) && !selectedSpaces.includes(a)
- ? 1
- : 0,
- );
+ const [searchResults, setSearchResults] = React.useState<SearchResult[]>([]);
+ const [isSearching, setIsSearching] = React.useState(false);
+
+ const results = React.useMemo(() => {
+ console.log("use memo");
+ 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,
+ },
+ });
+ setSearchResults(results);
+ setIsSearching(false);
+ })();
+ } else {
+ setSearchResults([]);
+ }
+ }, [query]);
React.useEffect(() => {
if (!open) {
@@ -177,6 +211,7 @@ export function FilterMemories({
}
}, [open]);
+ console.log(searchResults);
return (
<AnimatePresence mode="popLayout">
<LayoutGroup>
@@ -191,15 +226,7 @@ export function FilterMemories({
)}
{...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>
+ {props.children}
</button>
</PopoverTrigger>
<PopoverContent
@@ -208,56 +235,53 @@ export function FilterMemories({
side={side}
className="w-[200px] p-0"
>
- <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}
- {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>
+ <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.image ?? "/icons/logo_without_bg.png"}
+ className="mr-2 h-4 w-4"
+ />
+ {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>
diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx
index f671b72f..970deb68 100644
--- a/apps/web/src/components/Sidebar/MemoriesBar.tsx
+++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx
@@ -10,6 +10,7 @@ import { Input, InputWithIcon } from "../ui/input";
import {
ArrowUpRight,
Edit3,
+ Loader,
MoreHorizontal,
Plus,
Search,
@@ -25,7 +26,7 @@ import {
} from "../ui/dropdown-menu";
import { useEffect, useMemo, useRef, useState } from "react";
import { Variant, useAnimate, motion } from "framer-motion";
-import { useMemory } from "@/contexts/MemoryContext";
+import { SearchResult, useMemory } from "@/contexts/MemoryContext";
import { SpaceIcon } from "@/assets/Memories";
import {
Dialog,
@@ -44,10 +45,12 @@ import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog";
import { ExpandedSpace } from "./ExpandedSpace";
import { StoredContent, StoredSpace } from "@/server/db/schema";
import Image from "next/image";
+import { useDebounce } from "@/hooks/useDebounce";
+import { searchMemoriesAndSpaces } from "@/actions/db";
export function MemoriesBar() {
const [parent, enableAnimations] = useAutoAnimate();
- const { spaces, deleteSpace, freeMemories } = useMemory();
+ const { spaces, deleteSpace, freeMemories, search } = useMemory();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [addMemoryState, setAddMemoryState] = useState<
@@ -55,6 +58,11 @@ export function MemoriesBar() {
>(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[]>([]);
if (expandedSpace) {
return (
@@ -65,14 +73,37 @@ export function MemoriesBar() {
);
}
+ useEffect(() => {
+ const q = query.trim();
+ if (q.length < 1) {
+ setSearchResults([]);
+ return;
+ }
+
+ setSearchLoading(true);
+
+ (async () => {
+ setSearchResults(await search(q));
+ setSearchLoading(false);
+ })();
+ }, [query]);
+
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={<Search className="text-rgray-11 h-5 w-5 opacity-50" />}
+ 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">
@@ -123,17 +154,32 @@ export function MemoriesBar() {
ref={parent}
className="grid w-full grid-flow-row grid-cols-3 gap-1 px-2 py-5"
>
- {spaces.map((space) => (
- <SpaceItem
- onDelete={() => {}}
- key={space.id}
- //onClick={() => setExpandedSpace(space.id)}
- {...space}
- />
- ))}
- {freeMemories.map((m) => (
- <MemoryItem {...m} key={m.id} />
- ))}
+ {query.trim().length > 0 ? (
+ <>
+ {searchResults.map(({ type, space, memory }, i) => (
+ <>
+ {type === "memory" && <MemoryItem {...memory!} key={i} />}
+ {type === "space" && (
+ <SpaceItem {...space!} key={i} onDelete={() => {}} />
+ )}
+ </>
+ ))}
+ </>
+ ) : (
+ <>
+ {spaces.map((space) => (
+ <SpaceItem
+ onDelete={() => {}}
+ key={space.id}
+ //onClick={() => setExpandedSpace(space.id)}
+ {...space}
+ />
+ ))}
+ {freeMemories.map((m) => (
+ <MemoryItem {...m} key={m.id} />
+ ))}
+ </>
+ )}
</div>
</div>
);
@@ -149,15 +195,29 @@ const SpaceExitVariant: Variant = {
},
};
-export function MemoryItem({ id, title, image }: StoredContent) {
+export function MemoryItem({ id, title, image, type }: StoredContent) {
+ const name = title
+ ? title.length > 10
+ ? title.slice(0, 10) + "..."
+ : title
+ : "<no title>";
+
return (
<div 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 data-space-text className="focus-visible:outline-none">
- {title}
+ {name}
</button>
<div className="flex h-24 w-24 items-center justify-center">
- <img className="h-16 w-16" id={id.toString()} src={image!} />
+ {type === "page" ? (
+ <img className="h-16 w-16" id={id.toString()} src={image!} />
+ ) : type === "note" ? (
+ <div className="bg-rgray-4 flex items-center justify-center rounded-md p-2 shadow-md">
+ <Text className="h-10 w-10" />
+ </div>
+ ) : (
+ <></>
+ )}
</div>
</div>
);
@@ -186,6 +246,8 @@ export function SpaceItem({
return cachedMemories.filter((m) => m.space === id);
}, [cachedMemories]);
+ const _name = name.length > 10 ? name.slice(0, 10) + "..." : name;
+
return (
<motion.div
ref={itemRef}
@@ -194,7 +256,7 @@ export function SpaceItem({
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 data-space-text className="focus-visible:outline-none">
- {name}
+ {_name}
</button>
<SpaceMoreButton
isOpen={moreDropdownOpen}
@@ -287,6 +349,12 @@ export function SpaceItem({
id={id.toString()}
images={spaceMemories.map((c) => c.image).reverse() as string[]}
/>
+ ) : spaceMemories.length > 1 ? (
+ <MemoryWithImages2
+ className="h-24 w-24"
+ id={id.toString()}
+ images={spaceMemories.map((c) => c.image).reverse() as string[]}
+ />
) : spaceMemories.length === 1 ? (
<MemoryWithImage
className="h-24 w-24"
@@ -294,11 +362,7 @@ export function SpaceItem({
image={spaceMemories[0].image!}
/>
) : (
- <MemoryWithImages2
- className="h-24 w-24"
- id={id.toString()}
- images={spaceMemories.map((c) => c.image).reverse() as string[]}
- />
+ <div className="bg-rgray-4 shadow- h-24 w-24 scale-50 rounded-full opacity-30"></div>
)}
</motion.div>
);
diff --git a/apps/web/src/components/ui/command.tsx b/apps/web/src/components/ui/command.tsx
index 74b7f2e8..5fd64a6c 100644
--- a/apps/web/src/components/ui/command.tsx
+++ b/apps/web/src/components/ui/command.tsx
@@ -3,10 +3,11 @@
import * as React from "react";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
-import { Search } from "lucide-react";
+import { Loader, Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { Dialog, DialogContent } from "@/components/ui/dialog";
+import { isSea } from "node:sea";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
@@ -39,13 +40,19 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
->(({ className, ...props }, ref) => (
+ 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=""
>
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
+ {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(