diff options
| author | CodeTorso <[email protected]> | 2024-07-01 06:10:13 +0530 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-07-01 06:10:13 +0530 |
| commit | 38565f2ec45390c79101b752d366656bc70fd4d7 (patch) | |
| tree | 20b3a9c9c43a109028b06b6cf3b3bc199a7c2771 | |
| parent | fix typescript errors (diff) | |
| parent | canvas (3/3) (diff) | |
| download | supermemory-38565f2ec45390c79101b752d366656bc70fd4d7.tar.xz supermemory-38565f2ec45390c79101b752d366656bc70fd4d7.zip | |
Merge pull request #90 from Dhravya/editor
canvas (3/3)
| -rw-r--r-- | apps/web/app/(canvas)/canvas/page.tsx | 3 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/search&create.tsx | 22 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/thinkPad.tsx | 274 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvasStyles.css | 2 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/layout.tsx | 13 | ||||
| -rw-r--r-- | apps/web/app/actions/doers.ts | 35 | ||||
| -rw-r--r-- | apps/web/app/actions/fetchers.ts | 2 | ||||
| -rw-r--r-- | apps/web/app/api/canvasai/route.ts | 27 | ||||
| -rw-r--r-- | apps/web/components/canvas/draggableComponent.tsx | 28 | ||||
| -rw-r--r-- | apps/web/components/canvas/resizableLayout.tsx | 57 | ||||
| -rw-r--r-- | apps/web/components/canvas/savesnap.tsx | 5 | ||||
| -rw-r--r-- | apps/web/lib/loadSnap.ts | 2 |
12 files changed, 397 insertions, 73 deletions
diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx index 815d0b93..d0824a20 100644 --- a/apps/web/app/(canvas)/canvas/page.tsx +++ b/apps/web/app/(canvas)/canvas/page.tsx @@ -6,10 +6,9 @@ import ThinkPads from "./thinkPads"; async function page() { const canvas = await getCanvas(); return ( - <div className="h-screen w-full bg-[#171B1F] py-32 text-[#FFFFFF] "> + <div className="h-screen w-full py-32 text-[#FFFFFF] "> <div className="flex w-full flex-col items-center gap-8"> <h1 className="text-4xl font-medium">Your thinkpads</h1> - <p>{JSON.stringify(canvas)}</p> <SearchandCreate /> { // @ts-ignore diff --git a/apps/web/app/(canvas)/canvas/search&create.tsx b/apps/web/app/(canvas)/canvas/search&create.tsx index 3998dde7..a52a4cbe 100644 --- a/apps/web/app/(canvas)/canvas/search&create.tsx +++ b/apps/web/app/(canvas)/canvas/search&create.tsx @@ -1,9 +1,10 @@ -"use client" +"use client"; import { useFormStatus } from "react-dom"; import Image from "next/image"; import { SearchIcon } from "@repo/ui/icons"; import { createCanvas } from "@/app/actions/doers"; +import { toast } from "sonner"; export default function SearchandCreate() { return ( @@ -18,18 +19,27 @@ export default function SearchandCreate() { </button> </div> - <form action={createCanvas}> - <Button /> + <form + action={async () => { + const res = await createCanvas(); + if (!res.success){ + toast.warning(res.message, { + style: {backgroundColor: "rgb(22 31 42 / 0.3)"} + }); + } + }} + > + <Button /> </form> </div> ); } function Button() { - const {pending} = useFormStatus() + const { pending } = useFormStatus(); return ( <button className="rounded-xl bg-[#1F2428] px-5 py-3 text-xl text-[#B8C4C6]"> - {pending? "Creating.." : "Create New"} + {pending ? "Creating.." : "Create New"} </button> ); -}
\ No newline at end of file +} diff --git a/apps/web/app/(canvas)/canvas/thinkPad.tsx b/apps/web/app/(canvas)/canvas/thinkPad.tsx index dad2de48..00a83fda 100644 --- a/apps/web/app/(canvas)/canvas/thinkPad.tsx +++ b/apps/web/app/(canvas)/canvas/thinkPad.tsx @@ -1,5 +1,13 @@ -import {motion} from "framer-motion" +import { getCanvasData } from "@/app/actions/fetchers"; +import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; +import { + EllipsisHorizontalCircleIcon, + TrashIcon, + PencilSquareIcon, +} from "@heroicons/react/24/outline"; +import { toast } from "sonner"; +import { Label } from "@repo/ui/shadcn/label"; const childVariants = { hidden: { opacity: 0, y: 10, filter: "blur(2px)" }, @@ -10,27 +18,259 @@ export default function ThinkPad({ title, description, image, - id + id, }: { title: string; description: string; image: string; id: string; }) { + const [deleted, setDeleted] = useState(false); + const [info, setInfo] = useState({ title, description }); return ( - <motion.div - variants={childVariants} - className="flex h-48 gap-4 rounded-2xl bg-[#1F2428] p-2" - > - <Link className="h-full min-w-[40%] rounded-xl bg-[#363f46]" href={`/canvas/${id}`}> - <div></div> - </Link> - <div className="flex flex-col gap-2"> - <div>{title}</div> - <div className="overflow-hidden text-ellipsis text-[#B8C4C6]"> - {description} - </div> - </div> - </motion.div> + <AnimatePresence mode="sync"> + {!deleted && ( + <motion.div + layout + exit={{ opacity: 0, scaleY: 0 }} + variants={childVariants} + className="flex h-48 origin-top relative gap-4 rounded-2xl bg-[#1F2428] p-2" + > + <Link + className="h-full select-none min-w-[40%] bg-[#363f46] rounded-xl overflow-hidden" + href={`/canvas/${id}`} + > + <Suspense + fallback={ + <div className=" h-full w-full flex justify-center items-center"> + Loading... + </div> + } + > + <ImageComponent id={id} /> + </Suspense> + </Link> + <div className="flex flex-col gap-2"> + <motion.h2 + initial={{ opacity: 0, filter: "blur(3px)" }} + animate={{ opacity: 1, filter: "blur(0px)" }} + key={info.title} + > + {info.title} + </motion.h2> + <motion.h3 + key={info.description} + initial={{ opacity: 0, filter: "blur(3px)" }} + animate={{ opacity: 1, filter: "blur(0px)" }} + className="overflow-hidden text-ellipsis text-[#B8C4C6]" + > + {info.description} + </motion.h3> + </div> + <Menu + info={info} + id={id} + setDeleted={() => setDeleted(true)} + setInfo={(e) => setInfo(e)} + /> + </motion.div> + )} + </AnimatePresence> ); -}
\ No newline at end of file +} + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@repo/ui/shadcn/popover"; + +function Menu({ + info, + id, + setDeleted, + setInfo, +}: { + info: { title: string; description: string }; + id: string; + setDeleted: () => void; + setInfo: ({ + title, + description, + }: { + title: string; + description: string; + }) => void; +}) { + return ( + <Popover> + <PopoverTrigger className="absolute z-20 top-0 right-0" asChild> + <Button variant="secondary"> + <EllipsisHorizontalCircleIcon className="size-5 stroke-2 stroke-[#B8C4C6]" /> + </Button> + </PopoverTrigger> + <PopoverContent + align="start" + className="w-32 px-2 py-2 bg-[#161f2a]/30 text-[#B8C4C6] border-border flex flex-col gap-3" + > + <EditToolbar info={info} id={id} setInfo={setInfo} /> + <Button + onClick={async () => { + const res = await deleteCanvas(id); + if (res.success) { + toast.success("Thinkpad removed.", { + style: { backgroundColor: "rgb(22 31 42 / 0.3)" }, + }); + setDeleted(); + } else { + toast.warning("Something went wrong.", { + style: { backgroundColor: "rgb(22 31 42 / 0.3)" }, + }); + } + }} + className="flex gap-2 border-border" + variant="outline" + > + <TrashIcon className="size-8 stroke-1" /> Delete + </Button> + </PopoverContent> + </Popover> + ); +} + +function EditToolbar({ + id, + setInfo, + info +}: { + id: string; + setInfo: ({ + title, + description, + }: { + title: string; + description: string; + }) => void; + info: { + title: string; + description: string; + } +}) { + const [open, setOpen] = useState(false); + return ( + <Dialog open={open} onOpenChange={setOpen}> + <DialogTrigger asChild> + <Button className="flex gap-2 border-border" variant="outline"> + <PencilSquareIcon className="size-8 stroke-1" /> Edit + </Button> + </DialogTrigger> + <DialogContent className="sm:max-w-[425px] bg-[#161f2a]/30 border-0"> + <form + action={async (FormData) => { + const data = { + title: FormData.get("title") as string, + description: FormData.get("description") as string, + }; + const res = await AddCanvasInfo({ id, ...data }); + if (res.success) { + setOpen(false); + setInfo(data); + } else { + setOpen(false); + toast.error("Something went wrong.", { + style: { backgroundColor: "rgb(22 31 42 / 0.3)" }, + }); + } + }} + > + <DialogHeader> + <DialogTitle>Edit Canvas</DialogTitle> + <DialogDescription> + Add Description to your canvas. Pro tip: Let AI do the job, as you + add your content into canvas, we will autogenerate your + description. + </DialogDescription> + </DialogHeader> + <div className="grid gap-4 py-4"> + <div className="grid grid-cols-4 items-center gap-4"> + <Label htmlFor="title" className="text-right"> + Title + </Label> + <Input + defaultValue={info.title} + name="title" + id="title" + placeholder="life planning..." + className="col-span-3 border-0" + /> + </div> + <div className="grid grid-cols-4 items-center gap-4"> + <Label htmlFor="description" className="text-right"> + Description + </Label> + <Textarea + defaultValue={info.description} + rows={6} + id="description" + name="description" + placeholder="contains information about..." + className="col-span-3 border-0 resize-none" + /> + </div> + </div> + <DialogFooter> + <Button type="submit">Save changes</Button> + </DialogFooter> + </form> + </DialogContent> + </Dialog> + ); +} + +import { Suspense, memo, use, useState } from "react"; +import { Box, TldrawImage } from "tldraw"; +import { Button } from "@repo/ui/shadcn/button"; +import { AddCanvasInfo, deleteCanvas } from "@/app/actions/doers"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@repo/ui/shadcn/dialog"; +import { Input } from "@repo/ui/shadcn/input"; +import { Textarea } from "@repo/ui/shadcn/textarea"; +import { textCardUtil } from "@/components/canvas/textCard"; +import { twitterCardUtil } from "@/components/canvas/twitterCard"; + +const ImageComponent = memo(({ id }: { id: string }) => { + const snapshot = use(getCanvasData(id)); + if (snapshot.bounds) { + const pageBounds = new Box( + snapshot.bounds.x, + snapshot.bounds.y, + snapshot.bounds.w, + snapshot.bounds.h + ); + + return ( + <TldrawImage + shapeUtils={[twitterCardUtil, textCardUtil]} + snapshot={snapshot.snapshot} + background={false} + darkMode={true} + bounds={pageBounds} + padding={0} + scale={1} + format="png" + /> + ); + } + return ( + <div className=" h-full w-full flex justify-center items-center"> + Drew things to seee here + </div> + ); +}); diff --git a/apps/web/app/(canvas)/canvasStyles.css b/apps/web/app/(canvas)/canvasStyles.css index 04da2054..a9029680 100644 --- a/apps/web/app/(canvas)/canvasStyles.css +++ b/apps/web/app/(canvas)/canvasStyles.css @@ -1,5 +1,5 @@ .tl-background { - background: #1F2428 !important; + background: #181E23 !important; } .tlui-style-panel.tlui-style-panel__wrapper, .tlui-navigation-panel::before ,.tlui-menu-zone, .tlui-toolbar__tools, .tlui-popover__content, .tlui-menu, .tlui-button__help, .tlui-help-menu, .tlui-dialog__content { diff --git a/apps/web/app/(canvas)/layout.tsx b/apps/web/app/(canvas)/layout.tsx index 5c925573..33e0adfb 100644 --- a/apps/web/app/(canvas)/layout.tsx +++ b/apps/web/app/(canvas)/layout.tsx @@ -1,19 +1,30 @@ import { auth } from "@/server/auth"; import "./canvasStyles.css"; import { redirect } from "next/navigation"; +import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid"; +import { Toaster } from "@repo/ui/shadcn/sonner"; export default async function RootLayout({ children, }: { children: React.ReactNode; }) { - const info = await auth(); if (!info) { return redirect("/signin"); } return ( + <> + <div className="relative flex justify-center z-40 pointer-events-none"> + <div + className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]" + style={{ transform: "rotate(-30deg)" }} + /> + </div> + <BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" /> <div>{children}</div> + <Toaster /> + </> ); } diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts index 00feaa17..da0e49b5 100644 --- a/apps/web/app/actions/doers.ts +++ b/apps/web/app/actions/doers.ts @@ -391,6 +391,12 @@ export const createCanvas = async () => { return { error: "Not authenticated", success: false }; } + const canvases = await db.select().from(canvas).where(eq(canvas.userId, data.user.id)) + + if (canvases.length >= 5){ + return {success: false, message: "A user currently can only have 5 canvases"} + } + const resp = await db .insert(canvas) .values({ userId: data.user.id }).returning({id: canvas.id}); @@ -428,4 +434,33 @@ export const SaveCanvas = async ({id, data}: {id: string, data: string}) => { } catch (error) { return {success: false, error, message:"An error occured while saving your canvas"} } +} + +export const deleteCanvas = async (id: string) => { + try { + await process.env.CANVAS_SNAPS.delete(id) + await db.delete(canvas).where(eq(canvas.id,id)) + return { + success: true, + message: "in-sync" + } + } catch (error) { + return {success: false, error, message:"An error occured while saving your canvas"} + } +} + +export async function AddCanvasInfo({id, title, description}: {id: string, title: string, description: string}){ + try { + await db.update(canvas).set({description, title}).where(eq(canvas.id, id)) + return { + success: true, + message: "info updated successfully" + } + } catch (error) { + return { + success: false, + message: "something went wrong :/" + } + + } }
\ No newline at end of file diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts index d93cd4d0..4cb5d1dc 100644 --- a/apps/web/app/actions/fetchers.ts +++ b/apps/web/app/actions/fetchers.ts @@ -251,7 +251,7 @@ export const getCanvasData = async (canvasId: string) => { if (canvas){ return JSON.parse(canvas); } else { - return {} + return {snapshot: {}} } } diff --git a/apps/web/app/api/canvasai/route.ts b/apps/web/app/api/canvasai/route.ts new file mode 100644 index 00000000..07538bdf --- /dev/null +++ b/apps/web/app/api/canvasai/route.ts @@ -0,0 +1,27 @@ +import type { NextRequest } from "next/server"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function POST(request: NextRequest) { + const session = await ensureAuth(request); + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + const res : {query: string} = await request.json() + + try { + const resp = await fetch(`${process.env.BACKEND_BASE_URL}/api/search?query=${res.query}&user=${session.user.id}`); + if (resp.status !== 200 || !resp.ok) { + const errorData = await resp.text(); + console.log(errorData); + return new Response( + JSON.stringify({ message: "Error in CF function", error: errorData }), + { status: resp.status }, + ); + } + return new Response(JSON.stringify({response:await resp.json(), status: 200 })); + } catch (error) { + return new Response(`Error, ${error}`) + } +}
\ No newline at end of file diff --git a/apps/web/components/canvas/draggableComponent.tsx b/apps/web/components/canvas/draggableComponent.tsx index 8c39c732..cc31246e 100644 --- a/apps/web/components/canvas/draggableComponent.tsx +++ b/apps/web/components/canvas/draggableComponent.tsx @@ -1,25 +1,19 @@ import Image from "next/image"; import { useRef, useState } from "react"; - -interface DraggableComponentsProps { - content: string; - extraInfo?: string; - iconAlt: string; -} +import {motion} from "framer-motion" export default function DraggableComponentsContainer({ content, }: { - content: DraggableComponentsProps[]; + content: {context:string}[] | undefined; }) { + if (content === undefined) return null; return ( <div className="flex flex-col gap-10"> {content.map((i) => { return ( <DraggableComponents - content={i.content} - iconAlt={i.iconAlt} - extraInfo={i.extraInfo} + content={i.context} /> ); })} @@ -29,9 +23,7 @@ export default function DraggableComponentsContainer({ function DraggableComponents({ content, - extraInfo, - iconAlt, -}: DraggableComponentsProps) { +}: {content: string}) { const [isDragging, setIsDragging] = useState(false); const containerRef = useRef<HTMLDivElement>(null); @@ -49,19 +41,21 @@ function DraggableComponents({ }; return ( - <div + <motion.div + initial={{opacity: 0, y: 5}} + animate={{opacity: 1, y: 0}} ref={containerRef} onDragEnd={handleDragEnd} onDragStart={handleDragStart} draggable - className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`} + className={`flex gap-4 px-3 overflow-hidden rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`} > <div className="flex flex-col gap-2"> <div> <h1 className="line-clamp-3">{content}</h1> </div> - <p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p> + {/* <p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p> */} </div> - </div> + </motion.div> ); } diff --git a/apps/web/components/canvas/resizableLayout.tsx b/apps/web/components/canvas/resizableLayout.tsx index 5ba6780b..5f88de55 100644 --- a/apps/web/components/canvas/resizableLayout.tsx +++ b/apps/web/components/canvas/resizableLayout.tsx @@ -13,12 +13,18 @@ interface RectContextType { setFullScreen: React.Dispatch<React.SetStateAction<boolean>>; visible: boolean; setVisible: React.Dispatch<React.SetStateAction<boolean>>; - id: string + id: string; } const RectContext = createContext<RectContextType | undefined>(undefined); -export const RectProvider = ({ id, children }: {id: string, children: React.ReactNode}) => { +export const RectProvider = ({ + id, + children, +}: { + id: string; + children: React.ReactNode; +}) => { const [fullScreen, setFullScreen] = useState(false); const [visible, setVisible] = useState(true); @@ -36,12 +42,11 @@ export const RectProvider = ({ id, children }: {id: string, children: React.Reac export const useRect = () => { const context = useContext(RectContext); if (context === undefined) { - throw new Error('useRect must be used within a RectProvider'); + throw new Error("useRect must be used within a RectProvider"); } return context; }; - export function ResizaleLayout() { const { setVisible, fullScreen, setFullScreen } = useRect(); @@ -82,7 +87,7 @@ export function ResizaleLayout() { } function DragIconContainer() { - const { fullScreen} = useRect(); + const { fullScreen } = useRect(); return ( <div className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`} @@ -93,7 +98,7 @@ function DragIconContainer() { } function CanvasContainer() { - const { fullScreen} = useRect(); + const { fullScreen } = useRect(); return ( <div className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`} @@ -104,7 +109,7 @@ function CanvasContainer() { } function SidePanelContainer() { - const { fullScreen, visible} = useRect(); + const { fullScreen, visible } = useRect(); return ( <div className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`} @@ -123,35 +128,35 @@ function SidePanelContainer() { } function SidePanel() { - const [value, setValue] = useState(""); - // const [dragAsText, setDragAsText] = useState(false); + const [content, setContent] = useState<{context: string}[]>() return ( <> <div className="px-3 py-5"> - <input - placeholder="search..." - onChange={(e) => { - setValue(e.target.value); + <form + action={async (FormData) => { + const search = FormData.get("search"); + console.log(search) + const res = await fetch("/api/canvasai", { + method: "POST", + body: JSON.stringify({ query: search }), + }); + const t = await res.json() + console.log(t.response.response); + setContent(t.response.response) }} - value={value} - // rows={1} - className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52" - /> - </div> - <div className="flex items-center justify-end px-3 py-4"> - {/* <Switch - className="bg-[#151515] data-[state=unchecked]:bg-red-400 data-[state=checked]:bg-blue-400" - onCheckedChange={(e) => setDragAsText(e)} - id="drag-text-mode" - /> */} - <Label htmlFor="drag-text-mode">Drag as Text</Label> + > + <input + placeholder="search..." + name="search" + className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52" + /> + </form> </div> <DraggableComponentsContainer content={content} /> </> ); } - const content = [ { content: diff --git a/apps/web/components/canvas/savesnap.tsx b/apps/web/components/canvas/savesnap.tsx index 45fc7e9d..52654bd1 100644 --- a/apps/web/components/canvas/savesnap.tsx +++ b/apps/web/components/canvas/savesnap.tsx @@ -10,7 +10,10 @@ export function SaveStatus({id}: {id:string}) { const debouncedSave = useCallback( debounce(async () => { const snapshot = getSnapshot(editor.store) - SaveCanvas({id, data: JSON.stringify(snapshot)}) + const bounds = editor.getViewportPageBounds() + console.log(bounds) + + SaveCanvas({id, data: JSON.stringify({snapshot, bounds})}) setSave("saved!"); }, 3000), diff --git a/apps/web/lib/loadSnap.ts b/apps/web/lib/loadSnap.ts index fb4647f6..0d5bb593 100644 --- a/apps/web/lib/loadSnap.ts +++ b/apps/web/lib/loadSnap.ts @@ -9,6 +9,6 @@ export async function loadRemoteSnapshot(id:string) { const newStore = createTLStore({ shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil], }); - loadSnapshot(newStore, snapshot); + loadSnapshot(newStore, snapshot.snapshot); return newStore; } |