diff options
| author | codetorso <[email protected]> | 2024-07-19 07:07:52 +0530 |
|---|---|---|
| committer | codetorso <[email protected]> | 2024-07-19 07:07:52 +0530 |
| commit | 9ab9579b5ef5c041af6c8ee67bf578d1e779cbde (patch) | |
| tree | 98525edd9415f1fa439419e7ebae96e90d6998fb | |
| parent | skeleton loaders for recent chats (diff) | |
| download | supermemory-9ab9579b5ef5c041af6c8ee67bf578d1e779cbde.tar.xz supermemory-9ab9579b5ef5c041af6c8ee67bf578d1e779cbde.zip | |
a server rendered navbar
| -rw-r--r-- | apps/web/app/(dash)/dialogContentContainer.tsx | 224 | ||||
| -rw-r--r-- | apps/web/app/(dash)/dialogTriggerWrapper.tsx | 46 |
2 files changed, 270 insertions, 0 deletions
diff --git a/apps/web/app/(dash)/dialogContentContainer.tsx b/apps/web/app/(dash)/dialogContentContainer.tsx new file mode 100644 index 00000000..7ad68f17 --- /dev/null +++ b/apps/web/app/(dash)/dialogContentContainer.tsx @@ -0,0 +1,224 @@ +import { StoredSpace } from "@/server/db/schema"; +import { useEffect, useMemo, useState } from "react"; +import { createMemory, createSpace } from "../actions/doers"; +import ComboboxWithCreate from "@repo/ui/shadcn/combobox"; +import { toast } from "sonner"; +import { getSpaces } from "../actions/fetchers"; +import { MinusIcon, PlusCircleIcon } from "lucide-react"; +import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@repo/ui/shadcn/dialog"; +import { Label } from "@repo/ui/shadcn/label"; +import { Textarea } from "@repo/ui/shadcn/textarea"; +import { Button } from "@repo/ui/shadcn/button"; + +export function DialogContentContainer({DialogClose}: {DialogClose: ()=> void}) { + const [spaces, setSpaces] = useState<StoredSpace[]>([]); +const [content, setContent] = useState(""); +const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]); + +const options = useMemo( + () => + spaces.map((x) => ({ + label: x.name, + value: x.id.toString(), + })), + [spaces], +); + +const autoDetectedType = useMemo(() => { + if (content.length === 0) { + return "none"; + } + + if ( + content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/) + ) { + return "tweet"; + } else if (content.match(/https?:\/\/[\w\.]+/)) { + return "page"; + } else if (content.match(/https?:\/\/www\.[\w\.]+/)) { + return "page"; + } else { + return "note"; + } +}, [content]); + +const handleSubmit = async (content?: string, spaces?: number[]) => { + DialogClose(); + + toast.info("Creating memory...", { + icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />, + duration: 7500, + }); + + if (!content || content.length === 0) { + toast.error("Content is required"); + return; + } + + console.log(spaces); + + const cont = await createMemory({ + content: content, + spaces: spaces ?? undefined, + }); + + setContent(""); + setSelectedSpaces([]); + + if (cont.success) { + toast.success("Memory created", { + richColors: true, + }); + } else { + toast.error(`Memory creation failed: ${cont.error}`); + } +}; + +useEffect(() => { + (async () => { + let spaces = await getSpaces(); + + if (!spaces.success || !spaces.data) { + toast.warning("Unable to get spaces", { + richColors: true, + }); + setSpaces([]); + return; + } + setSpaces(spaces.data); + })(); +}, []); + +return ( + <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md"> + <form + action={async (e: FormData) => { + const content = e.get("content")?.toString(); + + await handleSubmit(content, selectedSpaces); + }} + className="flex flex-col gap-4 " + > + <DialogHeader> + <DialogTitle>Add memory</DialogTitle> + <DialogDescription className="text-[#F2F3F5]"> + A "Memory" is a bookmark, something you want to remember. + </DialogDescription> + </DialogHeader> + + <div> + <Label htmlFor="name">Resource (URL or content)</Label> + <Textarea + className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`} + id="content" + name="content" + rows={8} + placeholder="Start typing a note or paste a URL here. I'll remember it." + value={content} + onChange={(e) => setContent(e.target.value)} + onKeyDown={(e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSubmit(content, selectedSpaces); + } + }} + /> + </div> + + <div> + <Label className="space-y-1" htmlFor="space"> + <h3 className="font-semibold text-lg tracking-tight"> + Spaces (Optional) + </h3> + <p className="leading-normal text-[#F2F3F5] text-sm"> + A space is a collection of memories. It's a way to organise your + memories. + </p> + </Label> + + <ComboboxWithCreate + options={spaces.map((x) => ({ + label: x.name, + value: x.id.toString(), + }))} + onSelect={(v) => + setSelectedSpaces((prev) => { + if (v === "") { + return []; + } + return [...prev, parseInt(v)]; + }) + } + onSubmit={async (spaceName) => { + const space = options.find((x) => x.label === spaceName); + toast.info("Creating space..."); + + if (space) { + toast.error("A space with that name already exists."); + } + + const creationTask = await createSpace(spaceName); + if (creationTask.success && creationTask.data) { + toast.success("Space created " + creationTask.data); + setSpaces((prev) => [ + ...prev, + { + name: spaceName, + id: creationTask.data!, + createdAt: new Date(), + user: null, + numItems: 0, + }, + ]); + setSelectedSpaces((prev) => [...prev, creationTask.data!]); + } else { + toast.error( + "Space creation failed: " + creationTask.error ?? + "Unknown error", + ); + } + }} + placeholder="Select or create a new space." + className="bg-[#2F353C] h-min rounded-md mt-4 mb-4" + /> + + <div> + {selectedSpaces.length > 0 && ( + <div className="flex flex-row flex-wrap gap-0.5 h-min"> + {[...new Set(selectedSpaces)].map((x, idx) => ( + <button + key={x} + type="button" + onClick={() => + setSelectedSpaces((prev) => prev.filter((y) => y !== x)) + } + className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${ + idx === selectedSpaces.length - 1 ? "rounded-br-xl" : "" + }`} + > + <p className="line-clamp-1"> + {spaces.find((y) => y.id === x)?.name} + </p> + <div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center"> + <MinusIcon className="w-6 h-6 rounded-full bg-secondary" /> + </div> + </button> + ))} + </div> + )} + </div> + </div> + + <DialogFooter> + <Button + disabled={autoDetectedType === "none"} + variant={"secondary"} + type="submit" + > + Save {autoDetectedType != "none" && autoDetectedType} + </Button> + </DialogFooter> + </form> + </DialogContent> +); +}
\ No newline at end of file diff --git a/apps/web/app/(dash)/dialogTriggerWrapper.tsx b/apps/web/app/(dash)/dialogTriggerWrapper.tsx new file mode 100644 index 00000000..7d21e27e --- /dev/null +++ b/apps/web/app/(dash)/dialogTriggerWrapper.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Dialog, DialogTrigger } from "@repo/ui/shadcn/dialog"; +import { useState } from "react"; +import { DialogContentContainer } from "./dialogContentContainer"; +import { PlusIcon } from "@heroicons/react/24/solid"; + +export function DialogDesktopTrigger() { + return ( + <div className="border-gray-700/50 border-[1px] space-y-4 group relative bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl flex justify-center"> + <button className="cursor-pointer p-2 hover:scale-105 hover:text-[#bfc4c9] active:scale-90"> + <PlusIcon className="h-6 w-6" /> + </button> + <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1 left-[120%] -top-2"> + Add Memories + </div> + </div> + ); +} + +export function DialogMobileTrigger() { + return ( + <div className={`flex flex-col items-center cursor-pointer text-white`}> + <PlusIcon className="h-6 w-6 hover:brightness-125 focus:brightness-125 duration-200 stroke-white" /> + <p className="text-xs text-foreground-menu mt-2">Add</p> + </div> + ); +} +export default function DialogTriggerWrapper({ + children, +}: { + children: React.ReactNode; +}) { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <Dialog open={dialogOpen} onOpenChange={setDialogOpen}> + <DialogTrigger>{children}</DialogTrigger> + <DialogContentContainer + DialogClose={() => { + setDialogOpen(false); + }} + /> + </Dialog> + ); +} |