diff options
| -rw-r--r-- | apps/web/app/(canvas)/canvas/[id]/page.tsx | 140 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/layout.tsx | 13 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/page.tsx | 21 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/search&create.tsx | 45 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/thinkPad.tsx | 276 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvas/thinkPads.tsx | 32 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/canvasStyles.css | 32 | ||||
| -rw-r--r-- | apps/web/app/(canvas)/layout.tsx | 30 | ||||
| -rw-r--r-- | apps/web/app/actions/doers.ts | 114 | ||||
| -rw-r--r-- | apps/web/app/actions/fetchers.ts | 69 | ||||
| -rw-r--r-- | apps/web/app/api/canvas/route.ts | 10 | ||||
| -rw-r--r-- | apps/web/app/api/canvasai/route.ts | 31 | ||||
| -rw-r--r-- | apps/web/cf-env.d.ts | 6 | ||||
| -rw-r--r-- | apps/web/components/canvas/canvas.tsx (renamed from packages/ui/components/canvas/components/canvas.tsx) | 16 | ||||
| -rw-r--r-- | apps/web/components/canvas/draggableComponent.tsx (renamed from packages/ui/components/canvas/components/draggableComponent.tsx) | 38 | ||||
| -rw-r--r-- | apps/web/components/canvas/dropComponent.tsx (renamed from packages/ui/components/canvas/components/dropComponent.tsx) | 7 | ||||
| -rw-r--r-- | apps/web/components/canvas/enabledComp copy.tsx (renamed from packages/ui/components/canvas/components/enabledComp copy.tsx) | 2 | ||||
| -rw-r--r-- | apps/web/components/canvas/enabledComp.tsx (renamed from packages/ui/components/canvas/components/enabledComp.tsx) | 2 | ||||
| -rw-r--r-- | apps/web/components/canvas/resizableLayout.tsx | 175 | ||||
| -rw-r--r-- | apps/web/components/canvas/savesnap.tsx (renamed from packages/ui/components/canvas/components/savesnap.tsx) | 24 | ||||
| -rw-r--r-- | apps/web/components/canvas/textCard.tsx (renamed from packages/ui/components/canvas/components/textCard.tsx) | 4 | ||||
| -rw-r--r-- | apps/web/components/canvas/twitterCard.tsx (renamed from packages/ui/components/canvas/components/twitterCard.tsx) | 7 | ||||
| -rw-r--r-- | apps/web/env.d.ts | 1 | ||||
| -rw-r--r-- | apps/web/lib/context.ts (renamed from packages/ui/components/canvas/lib/context.ts) | 6 | ||||
| -rw-r--r-- | apps/web/lib/createAssetUrl.ts (renamed from packages/ui/components/canvas/lib/createAssetUrl.ts) | 0 | ||||
| -rw-r--r-- | apps/web/lib/createEmbeds.ts (renamed from packages/ui/components/canvas/lib/createEmbeds.ts) | 0 | ||||
| -rw-r--r-- | apps/web/lib/loadSnap.ts | 14 | ||||
| -rw-r--r-- | apps/web/migrations/0000_bitter_electro.sql (renamed from apps/web/migrations/000_setup.sql) | 18 | ||||
| -rw-r--r-- | apps/web/migrations/meta/0000_snapshot.json | 75 | ||||
| -rw-r--r-- | apps/web/migrations/meta/_journal.json | 20 | ||||
| -rw-r--r-- | apps/web/server/db/schema.ts | 19 | ||||
| -rw-r--r-- | apps/web/tsconfig.json | 3 | ||||
| -rw-r--r-- | apps/web/wrangler.toml | 4 | ||||
| -rw-r--r-- | packages/ui/components/canvas/lib/loadSnap.ts | 14 |
34 files changed, 1012 insertions, 256 deletions
diff --git a/apps/web/app/(canvas)/canvas/[id]/page.tsx b/apps/web/app/(canvas)/canvas/[id]/page.tsx index 6efb6cf4..cad6143e 100644 --- a/apps/web/app/(canvas)/canvas/[id]/page.tsx +++ b/apps/web/app/(canvas)/canvas/[id]/page.tsx @@ -1,129 +1,17 @@ -"use client"; - -import { Canvas } from "@repo/ui/components/canvas/components/canvas"; -import React, { useState } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import { SettingsIcon, DragIcon } from "@repo/ui/icons"; -import DraggableComponentsContainer from "@repo/ui/components/canvas/components/draggableComponent"; -import { AutocompleteIcon, blockIcon } from "@repo/ui/icons"; -import Image from "next/image"; -import { Switch } from "@repo/ui/shadcn/switch"; -import { Label } from "@repo/ui/shadcn/label"; -import { useRouter } from "next/router"; - -function page() { - const [fullScreen, setFullScreen] = useState(false); - const [visible, setVisible] = useState(true); - - const router = useRouter(); - router.push("/home"); - +import { userHasCanvas } from "@/app/actions/fetchers"; +import { + RectProvider, + ResizaleLayout, +} from "@/components/canvas/resizableLayout"; +import { redirect } from "next/navigation"; +export default async function page({ params }: any) { + const canvasExists = await userHasCanvas(params.id); + if (!canvasExists.success) { + redirect("/canvas"); + } return ( - <div - className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`} - > - <div> - <PanelGroup - onLayout={(l) => { - l[0]! < 20 ? setVisible(false) : setVisible(true); - }} - className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`} - direction="horizontal" - > - <Panel - onExpand={() => { - setTimeout(() => setFullScreen(false), 50); - }} - onCollapse={() => { - setTimeout(() => setFullScreen(true), 50); - }} - defaultSize={30} - collapsible={true} - > - <div - className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`} - > - <div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]"> - Change Filters - <Image src={SettingsIcon} alt="setting-icon" /> - </div> - {visible ? ( - <SidePanel /> - ) : ( - <h1 className="text-center py-10 text-xl"> - Need more space to show! - </h1> - )} - </div> - </Panel> - <PanelResizeHandle - className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`} - > - <div - className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`} - > - <Image src={DragIcon} alt="drag-icon" /> - </div> - </PanelResizeHandle> - <Panel className="relative" defaultSize={70} minSize={60}> - <div - className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`} - > - <Canvas /> - </div> - </Panel> - </PanelGroup> - </div> - </div> + <RectProvider id={params.id}> + <ResizaleLayout /> + </RectProvider> ); } - -function SidePanel() { - const [value, setValue] = useState(""); - const [dragAsText, setDragAsText] = useState(false); - return ( - <> - <div className="px-3 py-5"> - <input - placeholder="search..." - onChange={(e) => { - setValue(e.target.value); - }} - 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> - </div> - <DraggableComponentsContainer content={content} /> - </> - ); -} - -export default page; - -const content = [ - { - content: - "Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.", - icon: AutocompleteIcon, - iconAlt: "Autocomplete", - extraInfo: - "Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", - }, - { - content: - "As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)", - icon: blockIcon, - iconAlt: "Autocomplete", - extraInfo: - "Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia", - }, -]; diff --git a/apps/web/app/(canvas)/canvas/layout.tsx b/apps/web/app/(canvas)/canvas/layout.tsx deleted file mode 100644 index 9bc3b6d7..00000000 --- a/apps/web/app/(canvas)/canvas/layout.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import "../canvasStyles.css"; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - <div lang="en" className="bg-[#151515]"> - <div>{children}</div> - </div> - ); -} diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx index 8b5252af..d0824a20 100644 --- a/apps/web/app/(canvas)/canvas/page.tsx +++ b/apps/web/app/(canvas)/canvas/page.tsx @@ -1,9 +1,22 @@ -import { redirect } from "next/navigation"; import React from "react"; +import { getCanvas } from "@/app/actions/fetchers"; +import SearchandCreate from "./search&create"; +import ThinkPads from "./thinkPads"; -function page() { - redirect("/signin"); - return <div>page</div>; +async function page() { + const canvas = await getCanvas(); + return ( + <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> + <SearchandCreate /> + { + // @ts-ignore + canvas.success && <ThinkPads data={canvas.data} /> + } + </div> + </div> + ); } export default page; diff --git a/apps/web/app/(canvas)/canvas/search&create.tsx b/apps/web/app/(canvas)/canvas/search&create.tsx new file mode 100644 index 00000000..e73ad76f --- /dev/null +++ b/apps/web/app/(canvas)/canvas/search&create.tsx @@ -0,0 +1,45 @@ +"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 ( + <div className="flex w-[90%] max-w-2xl gap-2"> + <div className="flex flex-grow items-center overflow-hidden rounded-xl bg-[#1F2428]"> + <input + placeholder="search here..." + className="flex-grow bg-[#1F2428] px-5 py-3 text-xl focus:border-none focus:outline-none" + /> + <button className="h-full border-l-2 border-[#384149] px-2 pl-2"> + <Image src={SearchIcon} alt="search" /> + </button> + </div> + + <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(); + return ( + <button className="rounded-xl bg-[#1F2428] px-5 py-3 text-xl text-[#B8C4C6]"> + {pending ? "Creating.." : "Create New"} + </button> + ); +} diff --git a/apps/web/app/(canvas)/canvas/thinkPad.tsx b/apps/web/app/(canvas)/canvas/thinkPad.tsx new file mode 100644 index 00000000..fff30f26 --- /dev/null +++ b/apps/web/app/(canvas)/canvas/thinkPad.tsx @@ -0,0 +1,276 @@ +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)" }, + visible: { opacity: 1, y: 0, filter: "blur(0px)" }, +}; + +export default function ThinkPad({ + title, + description, + image, + id, +}: { + title: string; + description: string; + image: string; + id: string; +}) { + const [deleted, setDeleted] = useState(false); + const [info, setInfo] = useState({ title, description }); + return ( + <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> + ); +} + +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)/canvas/thinkPads.tsx b/apps/web/app/(canvas)/canvas/thinkPads.tsx new file mode 100644 index 00000000..3e8d7550 --- /dev/null +++ b/apps/web/app/(canvas)/canvas/thinkPads.tsx @@ -0,0 +1,32 @@ +"use client"; +import { motion } from "framer-motion"; +import ThinkPad from "./thinkPad"; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +export default function ThinkPads({ + data, +}: { + data: { image: string; title: string; description: string; id: string }[]; +}) { + return ( + <motion.div + variants={containerVariants} + initial="hidden" + animate="visible" + className="w-[90%] max-w-2xl space-y-6" + > + {data.map((item) => { + return <ThinkPad {...item} />; + })} + </motion.div> + ); +} diff --git a/apps/web/app/(canvas)/canvasStyles.css b/apps/web/app/(canvas)/canvasStyles.css index 04da2054..af4740e1 100644 --- a/apps/web/app/(canvas)/canvasStyles.css +++ b/apps/web/app/(canvas)/canvasStyles.css @@ -1,28 +1,36 @@ .tl-background { - background: #1F2428 !important; + background: #1f2428 !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 { - background: #2C3439 !important; - border-top: #2C3439 !important; - border-right: #2C3439 !important; - border-bottom: #2C3439 !important; - border-left: #2C3439 !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 { + background: #2c3840 !important; + border-top: #2c3840 !important; + border-right: #2c3840 !important; + border-bottom: #2c3840 !important; + border-left: #2c3840 !important; } .tlui-navigation-panel::before { - border-top: #2C3439 !important; - border-right: #2C3439 !important; + border-top: #2c3840 !important; + border-right: #2c3840 !important; } .tlui-minimap { - background: #2C3439 !important; + background: #2c3840 !important; } .tlui-minimap__canvas { - background: #1F2428 !important; + background: #1f2428 !important; } .tlui-dialog__overlay { position: fixed; -}
\ No newline at end of file +} diff --git a/apps/web/app/(canvas)/layout.tsx b/apps/web/app/(canvas)/layout.tsx new file mode 100644 index 00000000..33e0adfb --- /dev/null +++ b/apps/web/app/(canvas)/layout.tsx @@ -0,0 +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 8acc5679..98104ebd 100644 --- a/apps/web/app/actions/doers.ts +++ b/apps/web/app/actions/doers.ts @@ -3,6 +3,7 @@ import { revalidatePath } from "next/cache"; import { db } from "../../server/db"; import { + canvas, chatHistory, chatThreads, contentToSpace, @@ -435,3 +436,116 @@ export const linkTelegramToUser = async ( data: true, }; }; + +export const createCanvas = async () => { + const data = await auth(); + + if (!data || !data.user || !data.user.id) { + redirect("/signin"); + 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 }); + redirect(`/canvas/${resp[0]!.id}`); + // TODO INVESTIGATE: NO REDIRECT INSIDE TRY CATCH BLOCK + // try { + // const resp = await db + // .insert(canvas) + // .values({ userId: data.user.id }).returning({id: canvas.id}); + // return redirect(`/canvas/${resp[0]!.id}`); + // } catch (e: unknown) { + // const error = e as Error; + // if ( + // error.message.includes("D1_ERROR: UNIQUE constraint failed: space.name") + // ) { + // return { success: false, data: 0, error: "Space already exists" }; + // } else { + // return { + // success: false, + // data: 0, + // error: "Failed to create space with error: " + error.message, + // }; + // } + // } +}; + +export const SaveCanvas = async ({ + id, + data, +}: { + id: string; + data: string; +}) => { + console.log({ id, data }); + try { + await process.env.CANVAS_SNAPS.put(id, data); + return { + success: true, + message: "in-sync", + }; + } 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 :/", + }; + } +} diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts index b9f6dc2a..a4cc9e95 100644 --- a/apps/web/app/actions/fetchers.ts +++ b/apps/web/app/actions/fetchers.ts @@ -3,6 +3,7 @@ import { and, asc, eq, inArray, not, sql } from "drizzle-orm"; import { db } from "../../server/db"; import { + canvas, chatHistory, ChatThread, chatThreads, @@ -223,3 +224,71 @@ export const getNoteFromId = async ( data: note, }; }; +export const getCanvas = async () => { + const data = await auth(); + + if (!data || !data.user || !data.user.id) { + redirect("/signin"); + return { error: "Not authenticated", success: false }; + } + + try { + const canvases = await db + .select() + .from(canvas) + .where(eq(canvas.userId, data.user.id)); + + return { + success: true, + data: canvases.map(({ userId, ...rest }) => rest), + }; + } catch (e) { + return { + success: false, + error: (e as Error).message, + }; + } +}; + +export const userHasCanvas = async (canvasId: string) => { + const data = await auth(); + + if (!data || !data.user || !data.user.id) { + redirect("/signin"); + return { error: "Not authenticated", success: false }; + } + + try { + const canvases = await db + .select() + .from(canvas) + .where(eq(canvas.userId, data.user.id)); + const exists = !!canvases.find((canvas) => canvas.id === canvasId); + return { + success: exists, + }; + } catch (e) { + return { + success: false, + error: (e as Error).message, + }; + } +}; + +export const getCanvasData = async (canvasId: string) => { + const data = await auth(); + + if (!data || !data.user || !data.user.id) { + redirect("/signin"); + return { error: "Not authenticated", success: false }; + } + + const canvas = await process.env.CANVAS_SNAPS.get(canvasId); + + console.log({ canvas, canvasId }); + if (canvas) { + return JSON.parse(canvas); + } else { + return { snapshot: {} }; + } +}; diff --git a/apps/web/app/api/canvas/route.ts b/apps/web/app/api/canvas/route.ts new file mode 100644 index 00000000..70abace4 --- /dev/null +++ b/apps/web/app/api/canvas/route.ts @@ -0,0 +1,10 @@ +export function GET(req: Request) { + const id = new URL(req.url).searchParams.get("id"); + return new Response(JSON.stringify(id)); +} + +export async function POST(req: Request) { + const body = await req.json(); + const id = new URL(req.url).searchParams.get("id"); + return new Response(JSON.stringify({ body, id })); +} diff --git a/apps/web/app/api/canvasai/route.ts b/apps/web/app/api/canvasai/route.ts new file mode 100644 index 00000000..7e31f5b3 --- /dev/null +++ b/apps/web/app/api/canvasai/route.ts @@ -0,0 +1,31 @@ +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}`); + } +} diff --git a/apps/web/cf-env.d.ts b/apps/web/cf-env.d.ts index be5c991a..72bd946b 100644 --- a/apps/web/cf-env.d.ts +++ b/apps/web/cf-env.d.ts @@ -4,13 +4,19 @@ declare global { GOOGLE_CLIENT_ID: string; GOOGLE_CLIENT_SECRET: string; AUTH_SECRET: string; + R2_ENDPOINT: string; R2_ACCESS_KEY_ID: string; R2_SECRET_ACCESS_KEY: string; R2_PUBLIC_BUCKET_ADDRESS: string; R2_BUCKET_NAME: string; + BACKEND_SECURITY_KEY: string; BACKEND_BASE_URL: string; + + CLOUDFLARE_ACCOUNT_ID: string; + CLOUDFLARE_DATABASE_ID: string; + CLOUDFLARE_D1_TOKEN: string; } } } diff --git a/packages/ui/components/canvas/components/canvas.tsx b/apps/web/components/canvas/canvas.tsx index f3e4c090..1fbff4b8 100644 --- a/packages/ui/components/canvas/components/canvas.tsx +++ b/apps/web/components/canvas/canvas.tsx @@ -1,17 +1,18 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw"; -import { createAssetFromUrl } from "../lib/createAssetUrl"; +import { createAssetFromUrl } from "../../lib/createAssetUrl"; import "tldraw/tldraw.css"; import { components } from "./enabledComp"; import { twitterCardUtil } from "./twitterCard"; import { textCardUtil } from "./textCard"; -import createEmbedsFromUrl from "../lib/createEmbeds"; -import { loadRemoteSnapshot } from "../lib/loadSnap"; +import createEmbedsFromUrl from "../../lib/createEmbeds"; +import { loadRemoteSnapshot } from "../../lib/loadSnap"; import { SaveStatus } from "./savesnap"; import { getAssetUrls } from "@tldraw/assets/selfHosted"; import { memo } from "react"; -import DragContext from "../lib/context"; +import DragContext from "../../lib/context"; import DropZone from "./dropComponent"; +import { useRect } from "./resizableLayout"; // import "./canvas.css"; export const Canvas = memo(() => { @@ -46,12 +47,13 @@ export const Canvas = memo(() => { }); const TldrawComponent = memo(() => { + const { id } = useRect(); const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ status: "loading", }); useEffect(() => { const fetchStore = async () => { - const store = await loadRemoteSnapshot(); + const store = await loadRemoteSnapshot(id); setStoreWithStatus({ store: store, @@ -71,7 +73,7 @@ const TldrawComponent = memo(() => { }); }, []); - setUserPreferences({ id: "supermemory" }); + setUserPreferences({ id: "supermemory", colorScheme: "dark" }); const assetUrls = getAssetUrls(); return ( @@ -85,7 +87,7 @@ const TldrawComponent = memo(() => { onMount={handleMount} > <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]"> - <SaveStatus /> + <SaveStatus id={id} /> </div> <DropZone /> </Tldraw> diff --git a/packages/ui/components/canvas/components/draggableComponent.tsx b/apps/web/components/canvas/draggableComponent.tsx index d0832e81..da087156 100644 --- a/packages/ui/components/canvas/components/draggableComponent.tsx +++ b/apps/web/components/canvas/draggableComponent.tsx @@ -1,40 +1,23 @@ import Image from "next/image"; import { useRef, useState } from "react"; - -interface DraggableComponentsProps { - content: string; - extraInfo?: string; - icon: 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} - icon={i.icon} - iconAlt={i.iconAlt} - extraInfo={i.extraInfo} - /> - ); + return <DraggableComponents content={i.context} />; })} </div> ); } -function DraggableComponents({ - content, - extraInfo, - icon, - iconAlt, -}: DraggableComponentsProps) { +function DraggableComponents({ content }: { content: string }) { const [isDragging, setIsDragging] = useState(false); const containerRef = useRef<HTMLDivElement>(null); @@ -52,20 +35,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]"}`} > - <Image className="select-none" src={icon} alt={iconAlt} /> <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/packages/ui/components/canvas/components/dropComponent.tsx b/apps/web/components/canvas/dropComponent.tsx index 0374f367..5ea383a1 100644 --- a/packages/ui/components/canvas/components/dropComponent.tsx +++ b/apps/web/components/canvas/dropComponent.tsx @@ -1,7 +1,10 @@ import React, { useRef, useCallback, useEffect, useContext } from "react"; import { useEditor } from "tldraw"; -import DragContext, { DragContextType, useDragContext } from "../lib/context"; -import { handleExternalDroppedContent } from "../lib/createEmbeds"; +import DragContext, { + DragContextType, + useDragContext, +} from "../../lib/context"; +import { handleExternalDroppedContent } from "../../lib/createEmbeds"; const stripHtmlTags = (html: string): string => { const div = document.createElement("div"); diff --git a/packages/ui/components/canvas/components/enabledComp copy.tsx b/apps/web/components/canvas/enabledComp copy.tsx index 85811b82..b87ef227 100644 --- a/packages/ui/components/canvas/components/enabledComp copy.tsx +++ b/apps/web/components/canvas/enabledComp copy.tsx @@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = { // HelperButtons: null, // SharePanel: null, // MenuPanel: null, -};
\ No newline at end of file +}; diff --git a/packages/ui/components/canvas/components/enabledComp.tsx b/apps/web/components/canvas/enabledComp.tsx index 85811b82..b87ef227 100644 --- a/packages/ui/components/canvas/components/enabledComp.tsx +++ b/apps/web/components/canvas/enabledComp.tsx @@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = { // HelperButtons: null, // SharePanel: null, // MenuPanel: null, -};
\ No newline at end of file +}; diff --git a/apps/web/components/canvas/resizableLayout.tsx b/apps/web/components/canvas/resizableLayout.tsx new file mode 100644 index 00000000..2ff27083 --- /dev/null +++ b/apps/web/components/canvas/resizableLayout.tsx @@ -0,0 +1,175 @@ +"use client"; + +import { Canvas } from "./canvas"; +import React, { createContext, useContext, useState } from "react"; +import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; +import { SettingsIcon, DragIcon } from "@repo/ui/icons"; +import DraggableComponentsContainer from "./draggableComponent"; +import Image from "next/image"; +import { Label } from "@repo/ui/shadcn/label"; + +interface RectContextType { + fullScreen: boolean; + setFullScreen: React.Dispatch<React.SetStateAction<boolean>>; + visible: boolean; + setVisible: React.Dispatch<React.SetStateAction<boolean>>; + id: string; +} + +const RectContext = createContext<RectContextType | undefined>(undefined); + +export const RectProvider = ({ + id, + children, +}: { + id: string; + children: React.ReactNode; +}) => { + const [fullScreen, setFullScreen] = useState(false); + const [visible, setVisible] = useState(true); + + const value = { + id, + fullScreen, + setFullScreen, + visible, + setVisible, + }; + + return <RectContext.Provider value={value}>{children}</RectContext.Provider>; +}; + +export const useRect = () => { + const context = useContext(RectContext); + if (context === undefined) { + throw new Error("useRect must be used within a RectProvider"); + } + return context; +}; + +export function ResizaleLayout() { + const { setVisible, fullScreen, setFullScreen } = useRect(); + + return ( + <div + className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`} + > + <PanelGroup + onLayout={(l) => { + l[0]! < 20 ? setVisible(false) : setVisible(true); + }} + className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`} + direction="horizontal" + > + <Panel + onExpand={() => { + setTimeout(() => setFullScreen(false), 50); + }} + onCollapse={() => { + setTimeout(() => setFullScreen(true), 50); + }} + defaultSize={30} + collapsible={true} + > + <SidePanelContainer /> + </Panel> + <PanelResizeHandle + className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`} + > + <DragIconContainer /> + </PanelResizeHandle> + <Panel className="relative" defaultSize={70} minSize={60}> + <CanvasContainer /> + </Panel> + </PanelGroup> + </div> + ); +} + +function DragIconContainer() { + const { fullScreen } = useRect(); + return ( + <div + className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`} + > + <Image src={DragIcon} alt="drag-icon" /> + </div> + ); +} + +function CanvasContainer() { + const { fullScreen } = useRect(); + return ( + <div + className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`} + > + <Canvas /> + </div> + ); +} + +function SidePanelContainer() { + 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]`} + > + <div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]"> + Change Filters + <Image src={SettingsIcon} alt="setting-icon" /> + </div> + {visible ? ( + <SidePanel /> + ) : ( + <h1 className="text-center py-10 text-xl">Need more space to show!</h1> + )} + </div> + ); +} + +function SidePanel() { + const [content, setContent] = useState<{ context: string }[]>(); + return ( + <> + <div className="px-3 py-5"> + <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); + }} + > + <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: + "Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.", + iconAlt: "Autocomplete", + extraInfo: + "Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", + }, + { + content: + "As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)", + iconAlt: "Autocomplete", + extraInfo: + "Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia", + }, +]; diff --git a/packages/ui/components/canvas/components/savesnap.tsx b/apps/web/components/canvas/savesnap.tsx index f82e97e3..a8cacd3e 100644 --- a/packages/ui/components/canvas/components/savesnap.tsx +++ b/apps/web/components/canvas/savesnap.tsx @@ -1,27 +1,19 @@ import { useCallback, useEffect, useState } from "react"; -import { debounce, useEditor } from "tldraw"; +import { debounce, getSnapshot, useEditor } from "tldraw"; +import { SaveCanvas } from "@/app/actions/doers"; -export function SaveStatus() { +export function SaveStatus({ id }: { id: string }) { const [save, setSave] = useState("saved!"); const editor = useEditor(); const debouncedSave = useCallback( debounce(async () => { - const snapshot = editor.store.getSnapshot(); - localStorage.setItem("saved", JSON.stringify(snapshot)); + const snapshot = getSnapshot(editor.store); + const bounds = editor.getViewportPageBounds(); + console.log(bounds); - const res = await fetch( - "https://learning-cf.pruthvirajthinks.workers.dev/post/page3", - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - data: snapshot, - }), - }, - ); + SaveCanvas({ id, data: JSON.stringify({ snapshot, bounds }) }); - console.log(await res.json()); setSave("saved!"); }, 3000), [editor], // Dependency array ensures the function is not recreated on every render @@ -40,4 +32,4 @@ export function SaveStatus() { }, [editor, debouncedSave]); return <button>{save}</button>; -}
\ No newline at end of file +} diff --git a/packages/ui/components/canvas/components/textCard.tsx b/apps/web/components/canvas/textCard.tsx index b24dae52..600dc1a5 100644 --- a/packages/ui/components/canvas/components/textCard.tsx +++ b/apps/web/components/canvas/textCard.tsx @@ -25,9 +25,11 @@ export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> { height: s.props.h, width: s.props.w, pointerEvents: "all", - background: "#2C3439", + background: "#2E3C4C", borderRadius: "16px", + border: "2px solid #3e4449", padding: "8px 14px", + overflow: "auto", }} > <h1 style={{ fontSize: "15px" }}>{s.props.content}</h1> diff --git a/packages/ui/components/canvas/components/twitterCard.tsx b/apps/web/components/canvas/twitterCard.tsx index c5582a98..8cf8e576 100644 --- a/packages/ui/components/canvas/components/twitterCard.tsx +++ b/apps/web/components/canvas/twitterCard.tsx @@ -1,4 +1,9 @@ -import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, toDomPrecision } from "tldraw"; +import { + BaseBoxShapeUtil, + HTMLContainer, + TLBaseShape, + toDomPrecision, +} from "tldraw"; type ITwitterCardShape = TLBaseShape< "Twittercard", diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts index ee41239f..d52ab7b5 100644 --- a/apps/web/env.d.ts +++ b/apps/web/env.d.ts @@ -5,4 +5,5 @@ interface CloudflareEnv { STORAGE: R2Bucket; DATABASE: D1Database; DEV_IMAGES: R2Bucket; + CANVAS_SNAPS: KVNamespace; } diff --git a/packages/ui/components/canvas/lib/context.ts b/apps/web/lib/context.ts index 4e6ecd1c..840c0d31 100644 --- a/packages/ui/components/canvas/lib/context.ts +++ b/apps/web/lib/context.ts @@ -1,4 +1,4 @@ -import { createContext, useContext } from 'react'; +import { createContext, useContext } from "react"; export interface DragContextType { isDraggingOver: boolean; @@ -10,9 +10,9 @@ const DragContext = createContext<DragContextType | undefined>(undefined); export const useDragContext = () => { const context = useContext(DragContext); if (context === undefined) { - throw new Error('useAppContext must be used within an AppProvider'); + throw new Error("useAppContext must be used within an AppProvider"); } return context; }; -export default DragContext;
\ No newline at end of file +export default DragContext; diff --git a/packages/ui/components/canvas/lib/createAssetUrl.ts b/apps/web/lib/createAssetUrl.ts index 05c2baea..05c2baea 100644 --- a/packages/ui/components/canvas/lib/createAssetUrl.ts +++ b/apps/web/lib/createAssetUrl.ts diff --git a/packages/ui/components/canvas/lib/createEmbeds.ts b/apps/web/lib/createEmbeds.ts index b3a7fb52..b3a7fb52 100644 --- a/packages/ui/components/canvas/lib/createEmbeds.ts +++ b/apps/web/lib/createEmbeds.ts diff --git a/apps/web/lib/loadSnap.ts b/apps/web/lib/loadSnap.ts new file mode 100644 index 00000000..083603eb --- /dev/null +++ b/apps/web/lib/loadSnap.ts @@ -0,0 +1,14 @@ +import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw"; +import { getCanvasData } from "../app/actions/fetchers"; +import { twitterCardUtil } from "../components/canvas/twitterCard"; +import { textCardUtil } from "../components/canvas/textCard"; + +export async function loadRemoteSnapshot(id: string) { + const snapshot = await getCanvasData(id); + + const newStore = createTLStore({ + shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil], + }); + loadSnapshot(newStore, snapshot.snapshot); + return newStore; +} diff --git a/apps/web/migrations/000_setup.sql b/apps/web/migrations/0000_bitter_electro.sql index a4855ec9..3ce840e7 100644 --- a/apps/web/migrations/000_setup.sql +++ b/apps/web/migrations/0000_bitter_electro.sql @@ -27,6 +27,15 @@ CREATE TABLE `authenticator` ( FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade ); --> statement-breakpoint +CREATE TABLE `canvas` ( + `id` text PRIMARY KEY NOT NULL, + `title` text DEFAULT 'Untitled' NOT NULL, + `description` text DEFAULT 'Untitled' NOT NULL, + `url` text DEFAULT '' NOT NULL, + `userId` text NOT NULL, + FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint CREATE TABLE `chatHistory` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, `threadId` text NOT NULL, @@ -86,7 +95,8 @@ CREATE TABLE `user` ( `name` text, `email` text NOT NULL, `emailVerified` integer, - `image` text + `image` text, + `telegramId` text ); --> statement-breakpoint CREATE TABLE `verificationToken` ( @@ -97,6 +107,7 @@ CREATE TABLE `verificationToken` ( ); --> statement-breakpoint CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint +CREATE INDEX `canvas_user_userId` ON `canvas` (`userId`);--> statement-breakpoint CREATE INDEX `chatHistory_thread_idx` ON `chatHistory` (`threadId`);--> statement-breakpoint CREATE INDEX `chatThread_user_idx` ON `chatThread` (`userId`);--> statement-breakpoint CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint @@ -105,4 +116,7 @@ CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint CREATE INDEX `storedContent_url_idx` ON `storedContent` (`url`);--> statement-breakpoint CREATE INDEX `storedContent_savedAt_idx` ON `storedContent` (`savedAt`);--> statement-breakpoint CREATE INDEX `storedContent_title_idx` ON `storedContent` (`title`);--> statement-breakpoint -CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`); +CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`);--> statement-breakpoint +CREATE INDEX `users_email_idx` ON `user` (`email`);--> statement-breakpoint +CREATE INDEX `users_telegram_idx` ON `user` (`telegramId`);--> statement-breakpoint +CREATE INDEX `users_id_idx` ON `user` (`id`);
\ No newline at end of file diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json index 291848ad..92a05c00 100644 --- a/apps/web/migrations/meta/0000_snapshot.json +++ b/apps/web/migrations/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "6988522d-8117-484d-b52a-94c0fbd75140", + "id": "58ec8bd8-5aad-4ade-a718-74eaf6056d36", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "account": { @@ -191,6 +191,69 @@ }, "uniqueConstraints": {} }, + "canvas": { + "name": "canvas", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Untitled'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'Untitled'" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "canvas_user_userId": { + "name": "canvas_user_userId", + "columns": ["userId"], + "isUnique": false + } + }, + "foreignKeys": { + "canvas_userId_user_id_fk": { + "name": "canvas_userId_user_id_fk", + "tableFrom": "canvas", + "tableTo": "user", + "columnsFrom": ["userId"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "chatHistory": { "name": "chatHistory", "columns": { @@ -415,13 +478,6 @@ "primaryKey": false, "notNull": false, "autoincrement": false - }, - "createdAt": { - "name": "createdAt", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false } }, "indexes": { @@ -681,5 +737,8 @@ "schemas": {}, "tables": {}, "columns": {} + }, + "internal": { + "indexes": {} } } diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json index 6231765b..b628749f 100644 --- a/apps/web/migrations/meta/_journal.json +++ b/apps/web/migrations/meta/_journal.json @@ -1,26 +1,12 @@ { - "version": "6", + "version": "7", "dialect": "sqlite", "entries": [ { "idx": 0, "version": "6", - "when": 1719671614406, - "tag": "0000_classy_speed_demon", - "breakpoints": true - }, - { - "idx": 1, - "version": "6", - "when": 1719772835765, - "tag": "0001_chubby_vulture", - "breakpoints": true - }, - { - "idx": 2, - "version": "6", - "when": 1719789157348, - "tag": "0002_old_titanium_man", + "when": 1719619409551, + "tag": "0000_bitter_electro", "breakpoints": true } ] diff --git a/apps/web/server/db/schema.ts b/apps/web/server/db/schema.ts index 2f4c0c41..01cf202d 100644 --- a/apps/web/server/db/schema.ts +++ b/apps/web/server/db/schema.ts @@ -201,5 +201,24 @@ export const chatHistory = createTable( }), ); +export const canvas = createTable( + "canvas", + { + id: text("id") + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + title: text("title").default("Untitled").notNull(), + description: text("description").default("Untitled").notNull(), + imageUrl: text("url").default("").notNull(), + userId: text("userId") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + }, + (canvas) => ({ + userIdx: index("canvas_user_userId").on(canvas.userId), + }), +); + export type ChatThread = typeof chatThreads.$inferSelect; export type ChatHistory = typeof chatHistory.$inferSelect; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 63996482..35e6a20c 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -18,7 +18,8 @@ "**/*.tsx", ".next/types/**/*.ts", "../../packages/ui/", - "../../packages/shared-types/utils.ts" + "../../packages/shared-types/utils.ts", + "./components" ], "exclude": ["node_modules/"] } diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml index 594614c7..a5f9b461 100644 --- a/apps/web/wrangler.toml +++ b/apps/web/wrangler.toml @@ -16,6 +16,10 @@ binding = "DATABASE" database_name = "dev-d1-anycontext" database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c" +[[kv_namespaces]] +binding = "CANVAS_SNAPS" +id = "c6446f7190dd4afebe1c318df3400518" + [[env.production.d1_databases]] binding = "DATABASE" database_name = "prod-d1-supermemory" diff --git a/packages/ui/components/canvas/lib/loadSnap.ts b/packages/ui/components/canvas/lib/loadSnap.ts deleted file mode 100644 index 846b1967..00000000 --- a/packages/ui/components/canvas/lib/loadSnap.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw"; -import { twitterCardUtil } from "../components/twitterCard"; -import { textCardUtil } from "../components/textCard"; -export async function loadRemoteSnapshot() { - const res = await fetch( - "https://learning-cf.pruthvirajthinks.workers.dev/get/page3", - ); - const snapshot = JSON.parse(await res.json()); - const newStore = createTLStore({ - shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil], - }); - loadSnapshot(newStore, snapshot); - return newStore; -} |