diff options
| author | Dhravya <[email protected]> | 2024-04-13 09:14:37 -0700 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-04-13 09:14:37 -0700 |
| commit | 75cae41f2aebd6343ce95cd0b6910870724ed374 (patch) | |
| tree | 44c9a92202801aea87cdaffd51bcd8b836fa69ea /apps | |
| parent | CloudflareAI for better streaming (diff) | |
| download | supermemory-75cae41f2aebd6343ce95cd0b6910870724ed374.tar.xz supermemory-75cae41f2aebd6343ce95cd0b6910870724ed374.zip | |
merge
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/extension/package.json | 1 | ||||
| -rw-r--r-- | apps/extension/src/SideBar.tsx | 175 | ||||
| -rw-r--r-- | apps/extension/src/background.ts | 46 | ||||
| -rw-r--r-- | apps/extension/src/components/FilterCombobox.tsx | 175 | ||||
| -rw-r--r-- | apps/extension/src/components/ui/dropdown-menu.tsx | 204 | ||||
| -rw-r--r-- | apps/web/src/actions/db.ts | 64 | ||||
| -rw-r--r-- | apps/web/src/app/page.tsx | 39 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/AddMemoryDialog.tsx | 31 | ||||
| -rw-r--r-- | apps/web/src/components/Sidebar/MemoriesBar.tsx | 232 | ||||
| -rw-r--r-- | apps/web/src/contexts/MemoryContext.tsx | 80 | ||||
| -rw-r--r-- | apps/web/src/server/db/schema.ts | 5 | ||||
| -rw-r--r-- | apps/web/src/server/db/test.ts | 6 | ||||
| -rw-r--r-- | apps/web/types/memory.tsx | 58 |
13 files changed, 677 insertions, 439 deletions
diff --git a/apps/extension/package.json b/apps/extension/package.json index 4c12ef20..a7f34ea6 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-tooltip": "^1.0.7", "cmdk": "^1.0.0", diff --git a/apps/extension/src/SideBar.tsx b/apps/extension/src/SideBar.tsx index 34992386..9704511b 100644 --- a/apps/extension/src/SideBar.tsx +++ b/apps/extension/src/SideBar.tsx @@ -7,19 +7,20 @@ import { TooltipProvider, TooltipTrigger, } from "./components/ui/tooltip"; -// import { FilterSpaces } from "./components/FilterCombobox"; -// import { -// Dialog, -// DialogContent, -// DialogHeader, -// DialogTitle, -// DialogDescription, -// DialogTrigger, -// DialogFooter, -// DialogClose, -// } from "./components/ui/dialog"; +import { FilterSpaces } from "./components/FilterCombobox"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogTrigger, + DialogFooter, + DialogClose, +} from "./components/ui/dialog"; +import { Space } from "./types/memory"; -function sendUrlToAPI() { +function sendUrlToAPI(spaces: number[]) { // get the current URL const url = window.location.href; @@ -31,7 +32,7 @@ function sendUrlToAPI() { } else { // const content = Entire page content, but cleaned up for the LLM. No ads, no scripts, no styles, just the text. if article, just the importnat info abou tit. const content = document.documentElement.innerText; - chrome.runtime.sendMessage({ type: "urlChange", content, url }); + chrome.runtime.sendMessage({ type: "urlChange", content, url, spaces }); } } @@ -50,7 +51,9 @@ function SideBar({ jwt }: { jwt: string }) { const [isSendingData, setIsSendingData] = useState(false); - // const [selectedSpaces, setSelectedSpaces] = useState<number[]>([0, 1]); + const [loading, setLoading] = useState(false); + const [spaces, setSpaces] = useState<Space[]>(); + const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); const [isImportingTweets, setIsImportingTweets] = useState(false); @@ -73,6 +76,15 @@ function SideBar({ jwt }: { jwt: string }) { }); } + const fetchSpaces = async () => { + setLoading(true); + chrome.runtime.sendMessage({ type: "fetchSpaces" }, (resp) => { + console.log(resp); + setSpaces(resp); + setLoading(false); + }); + }; + const fetchBookmarks = () => { const tweets: TweetData[] = []; // Initialize an empty array to hold all tweet elements @@ -253,67 +265,58 @@ function SideBar({ jwt }: { jwt: string }) { ) : ( <></> )} - {/* <Dialog> */} - <Tooltip delayDuration={300}> - <TooltipTrigger - className="anycontext-bg-transparent + <Dialog onOpenChange={(open) => open === true && fetchSpaces()}> + <Tooltip delayDuration={300}> + <TooltipTrigger + className="anycontext-bg-transparent anycontext-border-none anycontext-m-0 anycontext-p-0 " - > - {/* <DialogTrigger asChild> */} - <button - onClick={() => { - // return; - sendUrlToAPI(); - setIsSendingData(true); - setTimeout(() => { - setIsSendingData(false); - setSavedWebsites([...savedWebsites, window.location.href]); - }, 1000); - }} - disabled={savedWebsites.includes(window.location.href)} - className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent - anycontext-border-none anycontext-m-0 anycontext-p-0" > - {savedWebsites.includes(window.location.href) ? ( - <svg - xmlns="http://www.w3.org/2000/svg" - width="24" - height="24" - viewBox="0 0 24 24" - fill="none" - stroke="currentColor" - strokeWidth="2" - strokeLinecap="round" - strokeLinejoin="round" - className="lucide lucide-file-check-2" - > - <path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4" /> - <path d="M14 2v4a2 2 0 0 0 2 2h4" /> - <path d="m3 15 2 2 4-4" /> - </svg> - ) : ( - <svg - xmlns="http://www.w3.org/2000/svg" - viewBox="0 0 20 20" - fill="currentColor" - className={`anycontext-w-5 anycontext-h-5 ${isSendingData ? "anycontext-animate-spin" : ""}`} + <DialogTrigger asChild> + <button + disabled={savedWebsites.includes(window.location.href)} + className="anycontext-open-button disabled:anycontext-opacity-30 anycontext-bg-transparent + anycontext-border-none anycontext-m-0 anycontext-p-0" > - <path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" /> - </svg> - )} - </button> - {/* </DialogTrigger> */} - </TooltipTrigger> - <TooltipContent className="anycontext-p-0" side="left"> - <p className="anycontext-p-0 anycontext-m-0"> - {savedWebsites.includes(window.location.href) - ? "Added to memory" - : "Add to memory"} - </p> - </TooltipContent> - </Tooltip> - {/* <DialogContent> + {savedWebsites.includes(window.location.href) ? ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + strokeWidth="2" + strokeLinecap="round" + strokeLinejoin="round" + className="lucide lucide-file-check-2" + > + <path d="M4 22h14a2 2 0 0 0 2-2V7l-5-5H6a2 2 0 0 0-2 2v4" /> + <path d="M14 2v4a2 2 0 0 0 2 2h4" /> + <path d="m3 15 2 2 4-4" /> + </svg> + ) : ( + <svg + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 20 20" + fill="currentColor" + className={`anycontext-w-5 anycontext-h-5 ${isSendingData ? "anycontext-animate-spin" : ""}`} + > + <path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" /> + </svg> + )} + </button> + </DialogTrigger> + </TooltipTrigger> + <TooltipContent className="anycontext-p-0" side="left"> + <p className="anycontext-p-0 anycontext-m-0"> + {savedWebsites.includes(window.location.href) + ? "Added to memory" + : "Add to memory"} + </p> + </TooltipContent> + </Tooltip> + <DialogContent> <DialogHeader> <DialogTitle>Add to Memory</DialogTitle> <DialogDescription> @@ -322,27 +325,33 @@ function SideBar({ jwt }: { jwt: string }) { </DialogHeader> <FilterSpaces + loading={loading} className="anycontext-mr-auto" selectedSpaces={selectedSpaces} setSelectedSpaces={setSelectedSpaces} name={"Add to Spaces"} - spaces={[ - { - name: "cool tech", - id: 0, - }, - { - name: "cool libs", - id: 1, - }, - ]} + spaces={spaces ?? []} /> <DialogFooter className="anycontext-w-full anycontext-text-sm"> - <DialogClose>Add</DialogClose> + <DialogClose + onClick={() => { + sendUrlToAPI(selectedSpaces); + setIsSendingData(true); + setTimeout(() => { + setIsSendingData(false); + setSavedWebsites([ + ...savedWebsites, + window.location.href, + ]); + }, 1000); + }} + > + Add + </DialogClose> <DialogClose>Cancel</DialogClose> </DialogFooter> </DialogContent> - </Dialog> */} + </Dialog> </div> </TooltipProvider> </> diff --git a/apps/extension/src/background.ts b/apps/extension/src/background.ts index 7e12bba4..2c67936b 100644 --- a/apps/extension/src/background.ts +++ b/apps/extension/src/background.ts @@ -1,4 +1,5 @@ import { getEnv } from "./util"; +import { Space } from "./types/memory"; const backendUrl = getEnv() === "development" @@ -48,22 +49,51 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (request.type === "urlChange") { const content = request.content; const url = request.url; - - (async () => { - chrome.storage.local.get(["jwt"], ({ jwt }) => { + const spaces = request.spaces( + // eslint-disable-next-line no-unexpected-multiline + async () => { + chrome.storage.local.get(["jwt"], ({ jwt }) => { + if (!jwt) { + console.error("No JWT found"); + return; + } + fetch(`${backendUrl}/api/store`, { + method: "POST", + headers: { + Authorization: `Bearer ${jwt}`, + }, + body: JSON.stringify({ pageContent: content, url, spaces }), + }).then((ers) => console.log(ers.status)); + }); + }, + )(); + return true; + } else if (request.type === "fetchSpaces") { + const run = () => + chrome.storage.local.get(["jwt"], async ({ jwt }) => { if (!jwt) { console.error("No JWT found"); return; } - fetch(`${backendUrl}/api/store`, { - method: "POST", + const resp = await fetch(`${backendUrl}/api/spaces`, { headers: { Authorization: `Bearer ${jwt}`, }, - body: JSON.stringify({ pageContent: content, url }), - }).then((ers) => console.log(ers.status)); + }); + + const data: { + message: "OK" | string; + data: Space[] | undefined; + } = await resp.json(); + + if (data.message === "OK" && data.data) { + sendResponse(data.data); + } }); - })(); + + run(); + + return true; } else if (request.type === "queryApi") { const input = request.input; const jwt = request.jwt; diff --git a/apps/extension/src/components/FilterCombobox.tsx b/apps/extension/src/components/FilterCombobox.tsx index 5467655b..ae9c45ae 100644 --- a/apps/extension/src/components/FilterCombobox.tsx +++ b/apps/extension/src/components/FilterCombobox.tsx @@ -1,152 +1,91 @@ import * as React from "react"; -import { Check, ChevronsUpDown, X } from "lucide-react"; - -import { cn } from "../lib/utils"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "../components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "../components/ui/popover"; +import { PlusCircleIcon, X } from "lucide-react"; import { Space } from "../types/memory"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, +} from "./ui/dropdown-menu"; +import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"; export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> { - side?: "top" | "bottom"; - align?: "end" | "start" | "center"; - onClose?: () => void; selectedSpaces: number[]; setSelectedSpaces: ( spaces: number[] | ((prev: number[]) => number[]), ) => void; name: string; spaces: Space[]; + loading: boolean; } export function FilterSpaces({ - className, - side = "bottom", - align = "center", - onClose, + loading, selectedSpaces, setSelectedSpaces, - name, spaces, - ...props }: Props) { - const [open, setOpen] = React.useState(false); - console.log(selectedSpaces, spaces); - const sortedSpaces = spaces.sort(({ id: a }, { id: b }) => - selectedSpaces.includes(a) && !selectedSpaces.includes(b) - ? -1 - : selectedSpaces.includes(b) && !selectedSpaces.includes(a) - ? 1 - : 0, + const filteredSpaces = spaces.filter((space) => + selectedSpaces.includes(space.id), + ); + const leftSpaces = spaces.filter( + (space) => !selectedSpaces.includes(space.id), ); - React.useEffect(() => { - if (!open) { - onClose?.(); - } - }, [open]); + if (loading) { + return "Loading..."; + } return ( <div className="anycontext-flex anycontext-flex-wrap anycontext-gap-1 anycontext-text-sm anycontext-"> - {selectedSpaces.map((spaceid) => { - const space = spaces.find((s) => s.id === spaceid)!; - return <SpaceItem {...space} key={spaceid} />; - })} + {filteredSpaces.length < 1 && "Add to a space"} + {filteredSpaces.map((space) => ( + <SpaceItem + {...space} + key={space.id} + // onRemove={() => setSelectedSpaces(prev => prev.filter(s => s !== space.id))} + /> + ))} + {leftSpaces.length > 0 && ( + <DropdownMenu> + <DropdownMenuTrigger className="anycontext-rounded-full"> + <PlusCircleIcon + className="anycontext-w-5 anycontext-h-5 [--anycontext-icon-stroke:white] dark:[--anycontext-icon-stroke:black]" + stroke="var(--anycontext-icon-stroke)" + fill="currentColor" + /> + </DropdownMenuTrigger> + <DropdownMenuContent> + {leftSpaces.map((space) => ( + <> + {loading && "Loading..."} + <DropdownMenuItem + onClick={() => + setSelectedSpaces((prev) => [...prev, space.id]) + } + > + {space.name} + </DropdownMenuItem> + </> + ))} + </DropdownMenuContent> + </DropdownMenu> + )} </div> ); - - return ( - <Popover open={open} onOpenChange={setOpen}> - <PopoverTrigger asChild> - <button - type={undefined} - data-state-on={open} - className={cn( - "anycontext-combobox-button anycontext-w-fit", - className, - )} - {...props} - > - {name} - <ChevronsUpDown className="anycontext-h-4 anycontext-w-4" /> - <div - data-state-on={selectedSpaces.length > 0} - className="on:anycontext-flex anycontext-text-rgray-11 anycontext-border-rgray-6 anycontext-bg-rgray-2 anycontext-absolute anycontext-left-0 anycontext-top-0 anycontext-hidden anycontext-aspect-[1] anycontext-h-4 anycontext-w-4 anycontext--translate-x-1/3 anycontext--translate-y-1/3 anycontext-items-center anycontext-justify-center anycontext-rounded-full anycontext-border anycontext-text-center anycontext-text-[9px]" - > - {selectedSpaces.length} - </div> - </button> - </PopoverTrigger> - <PopoverContent - onCloseAutoFocus={(e) => e.preventDefault()} - align={align} - side={side} - className="anycontext-w-[200px] anycontext-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> - <div> - <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 - > - <div className="anycontext-text-black/90 dark:anycontext-text-white/90"> - {space.name} - <Check - data-state-on={selectedSpaces.includes(space.id)} - className={cn( - "on:anycontext-opacity-100 anycontext-ml-auto anycontext-h-4 anycontext-w-4 anycontext-opacity-0", - )} - /> - </div> - </CommandItem> - ))} - </CommandGroup> - </div> - </CommandList> - </Command> - </PopoverContent> - </Popover> - ); } function SpaceItem({ name }: Space) { return ( <div className="anycontext-flex anycontext-justify-center anycontext-items-center anycontext-gap-2 anycontext-p-1 anycontext-pl-2 anycontext-pr-3 anycontext-rounded-full anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-border-white/20 dark:anycontext-border-black/20 border"> - <button className="anycontext-flex hover:anycontext-bg-transparent anycontext-justify-center anycontext-scale-110 anycontext-items-center focus-visible:anycontext-outline-none anycontext-rounded-full anycontext-w-3 anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-h-3 anycontext-text-transparent hover:anycontext-text-black dark:hover:anycontext-text-white"> + <button + onClick={ + // onRemove + (e) => e.stopPropagation() + } + className="anycontext-flex hover:anycontext-bg-transparent anycontext-justify-center anycontext-scale-110 anycontext-items-center focus-visible:anycontext-outline-none anycontext-rounded-full anycontext-w-3 anycontext-bg-black/5 dark:anycontext-bg-white/5 anycontext-h-3 anycontext-text-transparent hover:anycontext-text-black dark:hover:anycontext-text-white" + > <X className="anycontext-w-3 anycontext-h-3" /> </button> {name} diff --git a/apps/extension/src/components/ui/dropdown-menu.tsx b/apps/extension/src/components/ui/dropdown-menu.tsx new file mode 100644 index 00000000..fcc1edb2 --- /dev/null +++ b/apps/extension/src/components/ui/dropdown-menu.tsx @@ -0,0 +1,204 @@ +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( + "anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none focus:anycontext-bg-stone-100 data-[state=open]:anycontext-bg-stone-100 dark:focus:anycontext-bg-stone-800 dark:data-[state=open]:anycontext-bg-stone-800", + inset && "anycontext-pl-8", + className, + )} + {...props} + > + {children} + <ChevronRight className="anycontext-ml-auto anycontext-h-4 anycontext-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( + "anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-lg data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", + 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( + "anycontext-z-50 anycontext-min-w-[8rem] anycontext-overflow-hidden anycontext-rounded-md anycontext-border anycontext-border-stone-200 anycontext-bg-white anycontext-p-1 anycontext-text-stone-950 anycontext-shadow-md data-[state=open]:anycontext-animate-in data-[state=closed]:anycontext-animate-out data-[state=closed]:anycontext-fade-out-0 data-[state=open]:anycontext-fade-in-0 data-[state=closed]:anycontext-zoom-out-95 data-[state=open]:anycontext-zoom-in-95 data-[side=bottom]:anycontext-slide-in-from-top-2 data-[side=left]:anycontext-slide-in-from-right-2 data-[side=right]:anycontext-slide-in-from-left-2 data-[side=top]:anycontext-slide-in-from-bottom-2 dark:anycontext-border-stone-800 dark:anycontext-bg-stone-950 dark:anycontext-text-stone-50", + 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( + "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", + inset && "anycontext-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( + "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", + className, + )} + checked={checked} + {...props} + > + <span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Check className="anycontext-h-4 anycontext-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( + "anycontext-relative anycontext-flex anycontext-cursor-default anycontext-select-none anycontext-items-center anycontext-rounded-sm anycontext-py-1.5 anycontext-pl-8 anycontext-pr-2 anycontext-text-sm anycontext-outline-none anycontext-transition-colors focus:anycontext-bg-stone-100 focus:anycontext-text-stone-900 data-[disabled]:anycontext-pointer-events-none data-[disabled]:anycontext-opacity-50 dark:focus:anycontext-bg-stone-800 dark:focus:anycontext-text-stone-50", + className, + )} + {...props} + > + <span className="anycontext-absolute anycontext-left-2 anycontext-flex anycontext-h-3.5 anycontext-w-3.5 anycontext-items-center anycontext-justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Circle className="anycontext-h-2 anycontext-w-2 anycontext-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( + "anycontext-px-2 anycontext-py-1.5 anycontext-text-sm anycontext-font-semibold", + inset && "anycontext-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( + "anycontext--mx-1 anycontext-my-1 anycontext-h-px anycontext-bg-stone-100 dark:anycontext-bg-stone-800", + className, + )} + {...props} + /> +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes<HTMLSpanElement>) => { + return ( + <span + className={cn( + "anycontext-ml-auto anycontext-text-xs anycontext-tracking-widest anycontext-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/actions/db.ts b/apps/web/src/actions/db.ts index 46e3ddf6..35ebe423 100644 --- a/apps/web/src/actions/db.ts +++ b/apps/web/src/actions/db.ts @@ -7,35 +7,40 @@ import { StoredContent, storedContent, users, - space + space, } from "@/server/db/schema"; import { like, eq, and, sql } from "drizzle-orm"; -import { union } from "drizzle-orm/sqlite-core" +import { union } from "drizzle-orm/sqlite-core"; import { auth as authOptions } from "@/server/auth"; +import { FormEvent } from "react"; +import { revalidatePath } from "next/cache"; // @todo: (future) pagination not yet needed export async function searchMemoriesAndSpaces(userId: string, query: string) { - const searchMemoriesQuery = db.select({ - type: sql<string>`'memory'`, - space: sql`NULL`, - memory: storedContent as any - }).from(storedContent).where(and( - eq(storedContent.user, userId), - like(storedContent.title, `%${query}%`) - )) - - const searchSpacesQuery = db.select({ - type: sql<string>`'space'`, - space: space as any, - memory: sql`NULL`, - }).from(space).where( - and( - eq(space.user, userId), - like(space.name, `%${query}%`) - ) - ) - - return await union(searchMemoriesQuery, searchSpacesQuery) + const searchMemoriesQuery = db + .select({ + type: sql<string>`'memory'`, + space: sql`NULL`, + memory: storedContent as any, + }) + .from(storedContent) + .where( + and( + eq(storedContent.user, userId), + like(storedContent.title, `%${query}%`), + ), + ); + + const searchSpacesQuery = db + .select({ + type: sql<string>`'space'`, + space: space as any, + memory: sql`NULL`, + }) + .from(space) + .where(and(eq(space.user, userId), like(space.name, `%${query}%`))); + + return await union(searchMemoriesQuery, searchSpacesQuery); } async function getUser() { @@ -46,7 +51,7 @@ async function getUser() { headers().get("Authorization")?.replace("Bearer ", ""); if (!token) { - return null + return null; } const session = await db @@ -55,7 +60,7 @@ async function getUser() { .where(eq(sessions.sessionToken, token!)); if (!session || session.length === 0) { - return null + return null; } const [userData] = await db @@ -65,17 +70,17 @@ async function getUser() { .limit(1); if (!userData) { - return null + return null; } - return userData + return userData; } export async function getMemory(title: string) { const user = await getUser(); if (!user) { - return null + return null; } return await db @@ -93,11 +98,10 @@ export async function addMemory( content: typeof storedContent.$inferInsert, spaces: number[], ) { - const user = await getUser(); if (!user) { - return null + return null; } content.user = user.id; diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 419daa5a..3112e71e 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,5 +1,6 @@ import { db } from "@/server/db"; import { + ChachedSpaceContent, contentToSpace, sessions, space, @@ -18,6 +19,7 @@ import { import { MemoryProvider } from "@/contexts/MemoryContext"; import Content from "./content"; import { searchMemoriesAndSpaces } from "@/actions/db"; +import { getMetaData } from "@/server/helpers"; export const runtime = "edge"; @@ -56,36 +58,37 @@ export default async function Home() { const collectedSpaces = await db .select() .from(space) - .where(and(eq(space.user, userData.id), not(eq(space.name, "none")))); + .where(eq(space.user, userData.id)) + .all(); + + console.log(collectedSpaces); // Fetch only first 3 content of each spaces - let contents: (typeof storedContent.$inferSelect)[] = []; + let contents: ChachedSpaceContent[] = []; + + //console.log(await db.select().from(storedContent).) await Promise.all([ collectedSpaces.forEach(async (space) => { - contents = [ - ...contents, - ...(await fetchContentForSpace(space.id, { + console.log("fetching "); + const data = ( + await fetchContentForSpace(space.id, { offset: 0, limit: 3, - })), - ]; + }) + ).map((data) => ({ + ...data, + space: space.id, + })); + contents = [...contents, ...data]; }), ]); + console.log(contents); + // freeMemories const freeMemories = await fetchFreeMemories(userData.id); - - // @dhravya test these 3 functions - fetchFreeMemories; - fetchContentForSpace; - searchMemoriesAndSpaces; - - collectedSpaces.push({ - id: 1, - name: "Cool tech", - user: null, - }); + console.log("free", freeMemories); return ( <MemoryProvider diff --git a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx index 886507ff..08b9a750 100644 --- a/apps/web/src/components/Sidebar/AddMemoryDialog.tsx +++ b/apps/web/src/components/Sidebar/AddMemoryDialog.tsx @@ -153,37 +153,6 @@ export function NoteAddPage({ closeDialog }: { closeDialog: () => void }) { } export function SpaceAddPage({ closeDialog }: { closeDialog: () => void }) { - const [selectedSpacesId, setSelectedSpacesId] = useState<number[]>([]); - - const inputRef = useRef<HTMLInputElement>(null); - const [name, setName] = useState(""); - const [content, setContent] = useState(""); - const [loading, setLoading] = useState(false); - - function check(): boolean { - const data = { - name: name.trim(), - content, - }; - console.log(name); - 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="md:w-[40vw]"> <DialogHeader> diff --git a/apps/web/src/components/Sidebar/MemoriesBar.tsx b/apps/web/src/components/Sidebar/MemoriesBar.tsx index 769e6296..f671b72f 100644 --- a/apps/web/src/components/Sidebar/MemoriesBar.tsx +++ b/apps/web/src/components/Sidebar/MemoriesBar.tsx @@ -23,7 +23,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { Variant, useAnimate, motion } from "framer-motion"; import { useMemory } from "@/contexts/MemoryContext"; import { SpaceIcon } from "@/assets/Memories"; @@ -42,11 +42,12 @@ import useTouchHold from "@/hooks/useTouchHold"; import { DialogTrigger } from "@radix-ui/react-dialog"; import { AddMemoryPage, NoteAddPage, SpaceAddPage } from "./AddMemoryDialog"; import { ExpandedSpace } from "./ExpandedSpace"; -import { StoredSpace } from "@/server/db/schema"; +import { StoredContent, StoredSpace } from "@/server/db/schema"; +import Image from "next/image"; export function MemoriesBar() { const [parent, enableAnimations] = useAutoAnimate(); - const { spaces, deleteSpace } = useMemory(); + const { spaces, deleteSpace, freeMemories } = useMemory(); const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [addMemoryState, setAddMemoryState] = useState< @@ -124,12 +125,15 @@ export function MemoriesBar() { > {spaces.map((space) => ( <SpaceItem - onDelete={() => deleteSpace(space.id)} + onDelete={() => {}} key={space.id} - onClick={() => setExpandedSpace(space.id)} + //onClick={() => setExpandedSpace(space.id)} {...space} /> ))} + {freeMemories.map((m) => ( + <MemoryItem {...m} key={m.id} /> + ))} </div> </div> ); @@ -145,12 +149,28 @@ const SpaceExitVariant: Variant = { }, }; +export function MemoryItem({ id, title, image }: StoredContent) { + 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} + </button> + + <div className="flex h-24 w-24 items-center justify-center"> + <img className="h-16 w-16" id={id.toString()} src={image!} /> + </div> + </div> + ); +} + export function SpaceItem({ name, id, onDelete, onClick, }: StoredSpace & { onDelete: () => void; onClick?: () => void }) { + const { cachedMemories } = useMemory(); + const [itemRef, animateItem] = useAnimate(); const { width } = useViewport(); @@ -162,6 +182,10 @@ export function SpaceItem({ }, }); + const spaceMemories = useMemo(() => { + return cachedMemories.filter((m) => m.space === id); + }, [cachedMemories]); + return ( <motion.div ref={itemRef} @@ -176,104 +200,106 @@ export function SpaceItem({ isOpen={moreDropdownOpen} setIsOpen={setMoreDropdownOpen} 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(); - }; - }; + // 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(); + // }; + // }; }} /> - {/* {content.length > 2 ? ( + {spaceMemories.length > 2 ? ( <MemoryWithImages3 className="h-24 w-24" id={id.toString()} - images={content.map((c) => c.image).reverse() as string[]} + images={spaceMemories.map((c) => c.image).reverse() as string[]} /> - ) : content.length === 1 ? ( + ) : spaceMemories.length === 1 ? ( <MemoryWithImage className="h-24 w-24" id={id.toString()} - image={content[0].image!} + image={spaceMemories[0].image!} /> ) : ( <MemoryWithImages2 className="h-24 w-24" id={id.toString()} - images={content.map((c) => c.image).reverse() as string[]} + images={spaceMemories.map((c) => c.image).reverse() as string[]} /> - )} */} + )} </motion.div> ); } @@ -288,7 +314,7 @@ export function SpaceMoreButton({ setIsOpen?: (open: boolean) => void; }) { return ( - <> + <Dialog> <DropdownMenu open={isOpen} onOpenChange={setIsOpen}> <DropdownMenuTrigger asChild> <button @@ -310,16 +336,36 @@ export function SpaceMoreButton({ <Edit3 className="mr-2 h-4 w-4" strokeWidth={1.5} /> Edit </DropdownMenuItem> - <DropdownMenuItem - onClick={onDelete} - className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10" - > - <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> - Move to Trash - </DropdownMenuItem> + <DialogTrigger asChild> + <DropdownMenuItem + onClick={onDelete} + className="focus:bg-red-100 focus:text-red-400 dark:focus:bg-red-100/10" + > + <Trash2 className="mr-2 h-4 w-4" strokeWidth={1.5} /> + Move to Trash + </DropdownMenuItem> + </DialogTrigger> </DropdownMenuContent> </DropdownMenu> - </> + <DialogContent> + <DialogTitle className="text-xl">Are you sure?</DialogTitle> + <DialogDescription className="text-md"> + You will not be able to recover this space + </DialogDescription> + <DialogFooter> + <DialogClose + type={undefined} + onClick={onDelete} + className="ml-auto flex items-center justify-center rounded-md bg-red-500/40 px-3 py-2 transition hover:bg-red-500/60 focus-visible:bg-red-500/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500" + > + 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/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx index aa9cab81..0438d75e 100644 --- a/apps/web/src/contexts/MemoryContext.tsx +++ b/apps/web/src/contexts/MemoryContext.tsx @@ -1,15 +1,20 @@ "use client"; import React, { useCallback } from "react"; import { CollectedSpaces } from "../../types/memory"; -import { StoredContent, storedContent, StoredSpace } from "@/server/db/schema"; +import { + ChachedSpaceContent, + StoredContent, + storedContent, + StoredSpace, +} from "@/server/db/schema"; import { addMemory, searchMemoriesAndSpaces } from "@/actions/db"; import { User } from "next-auth"; export type SearchResult = { - type: "memory" | "space", - space: StoredSpace, - memory: StoredContent -} + type: "memory" | "space"; + space: StoredSpace; + memory: StoredContent; +}; // temperory (will change) export const MemoryContext = React.createContext<{ @@ -21,8 +26,8 @@ export const MemoryContext = React.createContext<{ memory: typeof storedContent.$inferInsert, spaces?: number[], ) => Promise<void>; - cachedMemories: StoredContent[]; - search: (query: string) => Promise<SearchResult[]>; + cachedMemories: ChachedSpaceContent[]; + search: (query: string) => Promise<SearchResult[]>; }>({ spaces: [], freeMemories: [], @@ -30,57 +35,62 @@ export const MemoryContext = React.createContext<{ addSpace: async () => {}, deleteSpace: async () => {}, cachedMemories: [], - search: async () => [] + search: async () => [], }); export const MemoryProvider: React.FC< { spaces: StoredSpace[]; freeMemories: StoredContent[]; - cachedMemories: StoredContent[]; - user: User; + cachedMemories: ChachedSpaceContent[]; + user: User; } & React.PropsWithChildren -> = ({ children, user, spaces: initalSpaces, freeMemories: initialFreeMemories, cachedMemories: initialCachedMemories }) => { - +> = ({ + children, + user, + spaces: initalSpaces, + freeMemories: initialFreeMemories, + cachedMemories: initialCachedMemories, +}) => { const [spaces, setSpaces] = React.useState<StoredSpace[]>(initalSpaces); const [freeMemories, setFreeMemories] = React.useState<StoredContent[]>(initialFreeMemories); - const [cachedMemories, setCachedMemories] = React.useState<StoredContent[]>( - initialCachedMemories - ); + const [cachedMemories, setCachedMemories] = React.useState< + ChachedSpaceContent[] + >(initialCachedMemories); const addSpace = async (space: StoredSpace) => { - setSpaces((prev) => [...prev, space]); - } - - const deleteSpace = async (id: number) => { - setSpaces((prev) => prev.filter((s) => s.id !== id)); - } + setSpaces((prev) => [...prev, space]); + }; - const search = async (query: string) => { - if (!user.id) { - throw new Error('user id is not define') - } - const data = await searchMemoriesAndSpaces(user.id, query) - return data as SearchResult[] - } + const deleteSpace = async (id: number) => { + setSpaces((prev) => prev.filter((s) => s.id !== id)); + }; + + const search = async (query: string) => { + if (!user.id) { + throw new Error("user id is not define"); + } + const data = await searchMemoriesAndSpaces(user.id, query); + return data as SearchResult[]; + }; // const fetchMemories = useCallback(async (query: string) => { // const response = await fetch(`/api/memories?${query}`); // }, []); - const _addMemory = async ( - memory: typeof storedContent.$inferInsert, - spaces: number[] = [], - ) => { - const content = await addMemory(memory, spaces); - } + const _addMemory = async ( + memory: typeof storedContent.$inferInsert, + spaces: number[] = [], + ) => { + const content = await addMemory(memory, spaces); + }; return ( <MemoryContext.Provider value={{ - search, + search, spaces, addSpace, deleteSpace, diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts index daac595c..4ca4332f 100644 --- a/apps/web/src/server/db/schema.ts +++ b/apps/web/src/server/db/schema.ts @@ -21,7 +21,7 @@ export const users = createTable("user", { image: text("image", { length: 255 }), }); -export type User = typeof users.$inferSelect +export type User = typeof users.$inferSelect; export const usersRelations = relations(users, ({ many }) => ({ accounts: many(accounts), @@ -134,3 +134,6 @@ export const space = createTable( export type StoredContent = Omit<typeof storedContent.$inferSelect, "user">; export type StoredSpace = typeof space.$inferSelect; +export type ChachedSpaceContent = StoredContent & { + space: number; +}; diff --git a/apps/web/src/server/db/test.ts b/apps/web/src/server/db/test.ts new file mode 100644 index 00000000..37969e5e --- /dev/null +++ b/apps/web/src/server/db/test.ts @@ -0,0 +1,6 @@ +import { db } from "."; +import { space, user } from "./schema"; + +const user = await db.select(user).all(); + +await db.insert(space).values([{}]); diff --git a/apps/web/types/memory.tsx b/apps/web/types/memory.tsx index ff0dc94c..6bfda971 100644 --- a/apps/web/types/memory.tsx +++ b/apps/web/types/memory.tsx @@ -5,7 +5,7 @@ import { storedContent, StoredContent, } from "@/server/db/schema"; -import { asc, and, eq, inArray, notExists } from "drizzle-orm"; +import { asc, and, eq, inArray, notExists, sql, exists } from "drizzle-orm"; export async function fetchContentForSpace( spaceId: number, @@ -14,41 +14,55 @@ export async function fetchContentForSpace( limit: number; }, ) { - const query = db .select() .from(storedContent) .where( - inArray( - storedContent.id, - db.select().from(space).where(eq(space.id, spaceId)), + exists( + db + .select() + .from(contentToSpace) + .where( + and( + eq(contentToSpace.spaceId, spaceId), + eq(contentToSpace.contentId, storedContent.id), + ), + ), ), - ).orderBy(asc(storedContent.title)) + ) + .orderBy(asc(storedContent.title)); - return range ? await query.limit(range.limit).offset(range.offset) : await query.all() + return range + ? await query.limit(range.limit).offset(range.offset) + : await query.all(); } export async function fetchFreeMemories( - userId: string, - range?: { - offset: number; - limit: number; - } + userId: string, + range?: { + offset: number; + limit: number; + }, ) { - const query = db + const query = db .select() .from(storedContent) .where( - and( - notExists( - db.select().from(contentToSpace).where(eq(contentToSpace.contentId, storedContent.id)), - ), - eq(storedContent.user, userId), - ) - - ).orderBy(asc(storedContent.title)) + and( + notExists( + db + .select() + .from(contentToSpace) + .where(eq(contentToSpace.contentId, storedContent.id)), + ), + eq(storedContent.user, userId), + ), + ) + .orderBy(asc(storedContent.title)); - return range ? await query.limit(range.limit).offset(range.offset) : await query.all() + return range + ? await query.limit(range.limit).offset(range.offset) + : await query.all(); } export const transformContent = async ( |