diff options
| author | Kinfe Michael Tariku <[email protected]> | 2024-06-25 19:56:54 +0300 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-06-25 19:56:54 +0300 |
| commit | f46e42c2dfd1b223d4ad701a86d05fc0bb380e45 (patch) | |
| tree | f17fdfadf3bec08eee7f02da33af952796657254 /packages/ui/components | |
| parent | fix: import using absolute path (diff) | |
| parent | dev and prod databases (diff) | |
| download | supermemory-f46e42c2dfd1b223d4ad701a86d05fc0bb380e45.tar.xz supermemory-f46e42c2dfd1b223d4ad701a86d05fc0bb380e45.zip | |
Merge branch 'main' into feat/landing_revamp
Diffstat (limited to 'packages/ui/components')
| -rw-r--r-- | packages/ui/components/QueryInput.tsx | 60 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/canvas.tsx | 94 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/draggableComponent.tsx | 71 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/dropComponent.tsx | 203 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/enabledComp copy.tsx | 22 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/enabledComp.tsx | 22 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/savesnap.tsx | 43 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/textCard.tsx | 45 | ||||
| -rw-r--r-- | packages/ui/components/canvas/components/twitterCard.tsx | 84 | ||||
| -rw-r--r-- | packages/ui/components/canvas/lib/context.ts | 18 | ||||
| -rw-r--r-- | packages/ui/components/canvas/lib/createAssetUrl.ts | 94 | ||||
| -rw-r--r-- | packages/ui/components/canvas/lib/createEmbeds.ts | 236 | ||||
| -rw-r--r-- | packages/ui/components/canvas/lib/loadSnap.ts | 14 |
13 files changed, 1006 insertions, 0 deletions
diff --git a/packages/ui/components/QueryInput.tsx b/packages/ui/components/QueryInput.tsx new file mode 100644 index 00000000..ba476dda --- /dev/null +++ b/packages/ui/components/QueryInput.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import Divider from "../shadcn/divider"; +import { ArrowRightIcon } from "../icons"; +import Image from "next/image"; + +function QueryInput() { + return ( + <div> + <div className="bg-secondary rounded-[20px] h-[68 px]"> + {/* input and action button */} + <form className="flex gap-4 p-2.5"> + <textarea + name="q" + cols={30} + rows={4} + className="bg-transparent h-12 focus:h-[128px] no-scrollbar pt-3 px-2 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full" + placeholder="Ask your second brain..." + // onKeyDown={(e) => { + // if (e.key === "Enter") { + // e.preventDefault(); + // if (!e.shiftKey) push(parseQ()); + // } + // }} + // onChange={(e) => setQ(e.target.value)} + // value={q} + // disabled={disabled} + /> + + <button + // type="submit" + // onClick={e => e.preventDefault()} + // disabled={disabled} + className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90" + > + <Image src={ArrowRightIcon} alt="Right arrow icon" /> + </button> + </form> + + {/* <Divider /> */} + </div> + {/* selected sources */} + {/* <div className="flex items-center gap-6 p-2 h-auto bg-secondary"> */} + {/* <MultipleSelector + key={options.length} + disabled={disabled} + defaultOptions={options} + onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))} + placeholder="Focus on specific spaces..." + emptyIndicator={ + <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400"> + no results found. + </p> + } + /> */} + {/* </div> */} + </div> + ); +} + +export default QueryInput; diff --git a/packages/ui/components/canvas/components/canvas.tsx b/packages/ui/components/canvas/components/canvas.tsx new file mode 100644 index 00000000..57b63d49 --- /dev/null +++ b/packages/ui/components/canvas/components/canvas.tsx @@ -0,0 +1,94 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw"; +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 { SaveStatus } from "./savesnap"; +import { getAssetUrls } from "@tldraw/assets/selfHosted"; +import { memo } from "react"; +import DragContext from "../lib/context"; +import DropZone from "./dropComponent"; +// import "./canvas.css"; + +export const Canvas = memo(() => { + const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false); + const Dragref = useRef<HTMLDivElement | null>(null); + + const handleDragOver = (event: any) => { + event.preventDefault(); + setIsDraggingOver(true); + console.log("entere"); + }; + + useEffect(() => { + const divElement = Dragref.current; + if (divElement) { + divElement.addEventListener("dragover", handleDragOver); + } + return () => { + if (divElement) { + divElement.removeEventListener("dragover", handleDragOver); + } + }; + }, []); + + return ( + <DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}> + <div ref={Dragref} className="w-full h-full"> + <TldrawComponent /> + </div> + </DragContext.Provider> + ); +}); + +const TldrawComponent = memo(() => { + const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({ + status: "loading", + }); + useEffect(() => { + const fetchStore = async () => { + const store = await loadRemoteSnapshot(); + + setStoreWithStatus({ + store: store, + status: "not-synced", + }); + }; + + fetchStore(); + }, []); + + const handleMount = useCallback((editor: Editor) => { + (window as any).app = editor; + (window as any).editor = editor; + editor.registerExternalAssetHandler("url", createAssetFromUrl); + editor.registerExternalContentHandler("url", ({ url, point, sources }) => { + createEmbedsFromUrl({ url, point, sources, editor }); + }); + }, []); + + setUserPreferences({ id: "supermemory", isDarkMode: true }); + + const assetUrls = getAssetUrls(); + return ( + <div className="w-full h-full"> + <Tldraw + className="relative" + assetUrls={assetUrls} + components={components} + store={storeWithStatus} + shapeUtils={[twitterCardUtil, textCardUtil]} + onMount={handleMount} + > + <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]"> + <SaveStatus /> + </div> + <DropZone /> + </Tldraw> + </div> + ); +}); diff --git a/packages/ui/components/canvas/components/draggableComponent.tsx b/packages/ui/components/canvas/components/draggableComponent.tsx new file mode 100644 index 00000000..d0832e81 --- /dev/null +++ b/packages/ui/components/canvas/components/draggableComponent.tsx @@ -0,0 +1,71 @@ +import Image from "next/image"; +import { useRef, useState } from "react"; + +interface DraggableComponentsProps { + content: string; + extraInfo?: string; + icon: string; + iconAlt: string; +} + +export default function DraggableComponentsContainer({ + content, +}: { + content: DraggableComponentsProps[]; +}) { + 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} + /> + ); + })} + </div> + ); +} + +function DraggableComponents({ + content, + extraInfo, + icon, + iconAlt, +}: DraggableComponentsProps) { + const [isDragging, setIsDragging] = useState(false); + const containerRef = useRef<HTMLDivElement>(null); + + const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => { + setIsDragging(true); + if (containerRef.current) { + // Serialize the children as a string for dataTransfer + const childrenHtml = containerRef.current.innerHTML; + event.dataTransfer.setData("text/html", childrenHtml); + } + }; + + const handleDragEnd = () => { + setIsDragging(false); + }; + + return ( + <div + 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]"}`} + > + <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> + </div> + </div> + ); +} diff --git a/packages/ui/components/canvas/components/dropComponent.tsx b/packages/ui/components/canvas/components/dropComponent.tsx new file mode 100644 index 00000000..0374f367 --- /dev/null +++ b/packages/ui/components/canvas/components/dropComponent.tsx @@ -0,0 +1,203 @@ +import React, { useRef, useCallback, useEffect, useContext } from "react"; +import { useEditor } from "tldraw"; +import DragContext, { DragContextType, useDragContext } from "../lib/context"; +import { handleExternalDroppedContent } from "../lib/createEmbeds"; + +const stripHtmlTags = (html: string): string => { + const div = document.createElement("div"); + div.innerHTML = html; + return div.textContent || div.innerText || ""; +}; + +function formatTextToRatio(text: string) { + const totalWidth = text.length; + const maxLineWidth = Math.floor(totalWidth / 4); + + const words = text.split(" "); + let lines = []; + let currentLine = ""; + + words.forEach((word) => { + // Check if adding the next word exceeds the maximum line width + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + // If the current line is full, push it to new line + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + return lines.join("\n"); +} + +function DropZone() { + const dropRef = useRef<HTMLDivElement | null>(null); + const { isDraggingOver, setIsDraggingOver } = useDragContext(); + + const editor = useEditor(); + + const handleDragLeave = () => { + setIsDraggingOver(false); + console.log("leaver"); + }; + + useEffect(() => { + setInterval(() => { + editor.selectAll(); + const shapes = editor.getSelectedShapes(); + const text = shapes.filter((s) => s.type === "text"); + console.log("hrhh", text); + }, 5000); + }, []); + + const handleDrop = useCallback((event: DragEvent) => { + event.preventDefault(); + setIsDraggingOver(false); + const dt = event.dataTransfer; + if (!dt) { + return; + } + const items = dt.items; + + for (let i = 0; i < items.length; i++) { + if (items[i]!.kind === "file" && items[i]!.type.startsWith("image/")) { + const file = items[i]!.getAsFile(); + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + if (e.target) { + // setDroppedImage(e.target.result as string); + } + }; + reader.readAsDataURL(file); + } + } else if (items[i]!.kind === "string") { + items[i]!.getAsString((data) => { + const cleanText = stripHtmlTags(data); + const onethree = formatTextToRatio(cleanText); + handleExternalDroppedContent({ editor, text: onethree }); + }); + } + } + }, []); + + useEffect(() => { + const divElement = dropRef.current; + if (divElement) { + divElement.addEventListener("drop", handleDrop); + divElement.addEventListener("dragleave", handleDragLeave); + } + return () => { + if (divElement) { + divElement.removeEventListener("drop", handleDrop); + divElement.addEventListener("dragleave", handleDragLeave); + } + }; + }, []); + + return ( + <div + className={`h-full flex justify-center items-center w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2c3439ad] pointer-events-auto"}`} + ref={dropRef} + > + {isDraggingOver && ( + <> + <div className="absolute top-4 left-8"> + <TopRight /> + </div> + <div className="absolute top-4 right-8"> + <TopLeft /> + </div> + <div className="absolute bottom-4 left-8"> + <BottomLeft /> + </div> + <div className="absolute bottom-4 right-8"> + <BottomRight /> + </div> + <h2 className="text-2xl">Drop here to add Content on Canvas</h2> + </> + )} + </div> + ); +} + +function TopRight() { + return ( + <svg + width="48" + height="48" + viewBox="0 0 48 48" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M44 4H12C7.58172 4 4 7.58172 4 12V44" + stroke="white" + stroke-width="8" + stroke-linecap="round" + /> + </svg> + ); +} + +function TopLeft() { + return ( + <svg + width="48" + height="48" + viewBox="0 0 48 48" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4 4H36C40.4183 4 44 7.58172 44 12V44" + stroke="white" + stroke-width="8" + stroke-linecap="round" + /> + </svg> + ); +} + +function BottomLeft() { + return ( + <svg + width="48" + height="48" + viewBox="0 0 48 48" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M44 44H12C7.58172 44 4 40.4183 4 36V4" + stroke="white" + stroke-width="8" + stroke-linecap="round" + /> + </svg> + ); +} + +function BottomRight() { + return ( + <svg + width="48" + height="48" + viewBox="0 0 48 48" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d="M4 44H36C40.4183 44 44 40.4183 44 36V4" + stroke="white" + stroke-width="8" + stroke-linecap="round" + /> + </svg> + ); +} + +export default DropZone; diff --git a/packages/ui/components/canvas/components/enabledComp copy.tsx b/packages/ui/components/canvas/components/enabledComp copy.tsx new file mode 100644 index 00000000..85811b82 --- /dev/null +++ b/packages/ui/components/canvas/components/enabledComp copy.tsx @@ -0,0 +1,22 @@ +import { TLUiComponents } from "tldraw"; + +export const components: Partial<TLUiComponents> = { + ActionsMenu: null, + MainMenu: null, + QuickActions: null, + TopPanel: null, + DebugPanel: null, + DebugMenu: null, + PageMenu: null, + // Minimap: null, + // ContextMenu: null, + // HelpMenu: null, + // ZoomMenu: null, + // StylePanel: null, + // NavigationPanel: null, + // Toolbar: null, + // KeyboardShortcutsDialog: null, + // HelperButtons: null, + // SharePanel: null, + // MenuPanel: null, +};
\ No newline at end of file diff --git a/packages/ui/components/canvas/components/enabledComp.tsx b/packages/ui/components/canvas/components/enabledComp.tsx new file mode 100644 index 00000000..85811b82 --- /dev/null +++ b/packages/ui/components/canvas/components/enabledComp.tsx @@ -0,0 +1,22 @@ +import { TLUiComponents } from "tldraw"; + +export const components: Partial<TLUiComponents> = { + ActionsMenu: null, + MainMenu: null, + QuickActions: null, + TopPanel: null, + DebugPanel: null, + DebugMenu: null, + PageMenu: null, + // Minimap: null, + // ContextMenu: null, + // HelpMenu: null, + // ZoomMenu: null, + // StylePanel: null, + // NavigationPanel: null, + // Toolbar: null, + // KeyboardShortcutsDialog: null, + // HelperButtons: null, + // SharePanel: null, + // MenuPanel: null, +};
\ No newline at end of file diff --git a/packages/ui/components/canvas/components/savesnap.tsx b/packages/ui/components/canvas/components/savesnap.tsx new file mode 100644 index 00000000..f82e97e3 --- /dev/null +++ b/packages/ui/components/canvas/components/savesnap.tsx @@ -0,0 +1,43 @@ +import { useCallback, useEffect, useState } from "react"; +import { debounce, useEditor } from "tldraw"; + +export function SaveStatus() { + 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 res = await fetch( + "https://learning-cf.pruthvirajthinks.workers.dev/post/page3", + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + data: snapshot, + }), + }, + ); + + console.log(await res.json()); + setSave("saved!"); + }, 3000), + [editor], // Dependency array ensures the function is not recreated on every render + ); + + useEffect(() => { + const unsubscribe = editor.store.listen( + () => { + setSave("saving..."); + debouncedSave(); + }, + { scope: "document", source: "user" }, + ); + + return () => unsubscribe(); // Cleanup on unmount + }, [editor, debouncedSave]); + + return <button>{save}</button>; +}
\ No newline at end of file diff --git a/packages/ui/components/canvas/components/textCard.tsx b/packages/ui/components/canvas/components/textCard.tsx new file mode 100644 index 00000000..b24dae52 --- /dev/null +++ b/packages/ui/components/canvas/components/textCard.tsx @@ -0,0 +1,45 @@ +import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from "tldraw"; + +type ITextCardShape = TLBaseShape< + "Textcard", + { w: number; h: number; content: string; extrainfo: string } +>; + +export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> { + static override type = "Textcard" as const; + + getDefaultProps(): ITextCardShape["props"] { + return { + w: 100, + h: 50, + content: "", + extrainfo: "", + }; + } + + component(s: ITextCardShape) { + return ( + <HTMLContainer className="flex h-full w-full items-center justify-center"> + <div + style={{ + height: s.props.h, + width: s.props.w, + pointerEvents: "all", + background: "#2C3439", + borderRadius: "16px", + padding: "8px 14px", + }} + > + <h1 style={{ fontSize: "15px" }}>{s.props.content}</h1> + <p style={{ fontSize: "14px", color: "#369DFD" }}> + {s.props.extrainfo} + </p> + </div> + </HTMLContainer> + ); + } + + indicator(shape: ITextCardShape) { + return <rect width={shape.props.w} height={shape.props.h} />; + } +} diff --git a/packages/ui/components/canvas/components/twitterCard.tsx b/packages/ui/components/canvas/components/twitterCard.tsx new file mode 100644 index 00000000..c5582a98 --- /dev/null +++ b/packages/ui/components/canvas/components/twitterCard.tsx @@ -0,0 +1,84 @@ +import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, toDomPrecision } from "tldraw"; + +type ITwitterCardShape = TLBaseShape< + "Twittercard", + { w: number; h: number; url: string } +>; + +export class twitterCardUtil extends BaseBoxShapeUtil<ITwitterCardShape> { + static override type = "Twittercard" as const; + + getDefaultProps(): ITwitterCardShape["props"] { + return { + w: 500, + h: 550, + url: "", + }; + } + + component(s: ITwitterCardShape) { + return ( + <HTMLContainer className="flex h-full w-full items-center justify-center"> + <TwitterPost + url={s.props.url} + width={s.props.w} + isInteractive={false} + height={s.props.h} + /> + </HTMLContainer> + ); + } + + indicator(shape: ITwitterCardShape) { + return <rect width={shape.props.w} height={shape.props.h} />; + } +} + +function TwitterPost({ + isInteractive, + width, + height, + url, +}: { + isInteractive: boolean; + width: number; + height: number; + url: string; +}) { + const link = (() => { + try { + const urlObj = new URL(url); + const path = urlObj.pathname; + return path; + } catch (error) { + console.error("Invalid URL", error); + return null; + } + })(); + + return ( + <iframe + className="tl-embed" + draggable={false} + width={toDomPrecision(width)} + height={toDomPrecision(height)} + seamless + referrerPolicy="no-referrer-when-downgrade" + style={{ + pointerEvents: isInteractive ? "all" : "none", + zIndex: isInteractive ? "" : "-1", + }} + srcDoc={` + <html lang="en"> + <head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Document</title> + </head> + <body> + <blockquote data-theme="dark" class="twitter-tweet"><p lang="en" dir="ltr"><a href="https://twitter.com${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> + </body> + </html>`} + /> + ); +} diff --git a/packages/ui/components/canvas/lib/context.ts b/packages/ui/components/canvas/lib/context.ts new file mode 100644 index 00000000..4e6ecd1c --- /dev/null +++ b/packages/ui/components/canvas/lib/context.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from 'react'; + +export interface DragContextType { + isDraggingOver: boolean; + setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>; +} + +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'); + } + return context; +}; + +export default DragContext;
\ No newline at end of file diff --git a/packages/ui/components/canvas/lib/createAssetUrl.ts b/packages/ui/components/canvas/lib/createAssetUrl.ts new file mode 100644 index 00000000..05c2baea --- /dev/null +++ b/packages/ui/components/canvas/lib/createAssetUrl.ts @@ -0,0 +1,94 @@ +import { + AssetRecordType, + TLAsset, + getHashForString, + truncateStringWithEllipsis, +} from "tldraw"; +// import { BOOKMARK_ENDPOINT } from './config' + +interface ResponseBody { + title?: string; + description?: string; + image?: string; +} + +export async function createAssetFromUrl({ + url, +}: { + type: "url"; + url: string; +}): Promise<TLAsset> { + // try { + // // First, try to get the meta data from our endpoint + // const meta = (await ( + // await fetch(BOOKMARK_ENDPOINT, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // url, + // }), + // }) + // ).json()) as ResponseBody + + // return { + // id: AssetRecordType.createId(getHashForString(url)), + // typeName: 'asset', + // type: 'bookmark', + // props: { + // src: url, + // description: meta.description ?? '', + // image: meta.image ?? '', + // title: meta.title ?? truncateStringWithEllipsis(url, 32), + // }, + // meta: {}, + // } + // } catch (error) { + // Otherwise, fallback to fetching data from the url + + let meta: { image: string; title: string; description: string }; + + try { + const resp = await fetch(url, { method: "GET", mode: "no-cors" }); + const html = await resp.text(); + const doc = new DOMParser().parseFromString(html, "text/html"); + meta = { + image: + doc.head + .querySelector('meta[property="og:image"]') + ?.getAttribute("content") ?? "", + title: + doc.head + .querySelector('meta[property="og:title"]') + ?.getAttribute("content") ?? truncateStringWithEllipsis(url, 32), + description: + doc.head + .querySelector('meta[property="og:description"]') + ?.getAttribute("content") ?? "", + }; + } catch (error) { + console.error(error); + meta = { + image: "", + title: truncateStringWithEllipsis(url, 32), + description: "", + }; + } + + // Create the bookmark asset from the meta + return { + id: AssetRecordType.createId(getHashForString(url)), + typeName: "asset", + type: "bookmark", + props: { + src: url, + image: meta.image, + title: meta.title, + description: meta.description, + favicon: meta.image, + }, + meta: {}, + }; + // } +} diff --git a/packages/ui/components/canvas/lib/createEmbeds.ts b/packages/ui/components/canvas/lib/createEmbeds.ts new file mode 100644 index 00000000..b3a7fb52 --- /dev/null +++ b/packages/ui/components/canvas/lib/createEmbeds.ts @@ -0,0 +1,236 @@ +// @ts-nocheck TODO: A LOT OF TS ERRORS HERE + +import { + AssetRecordType, + Editor, + TLAsset, + TLAssetId, + TLBookmarkShape, + TLExternalContentSource, + TLShapePartial, + Vec, + VecLike, + createShapeId, + getEmbedInfo, + getHashForString, +} from "tldraw"; + +export default async function createEmbedsFromUrl({ + url, + point, + sources, + editor, +}: { + url: string; + point?: VecLike | undefined; + sources?: TLExternalContentSource[] | undefined; + editor: Editor; +}) { + const position = + point ?? + (editor.inputs.shiftKey + ? editor.inputs.currentPagePoint + : editor.getViewportPageBounds().center); + + if (url?.includes("x.com") || url?.includes("twitter.com")) { + return editor.createShape({ + type: "Twittercard", + x: position.x - 250, + y: position.y - 150, + props: { url: url }, + }); + } + + // try to paste as an embed first + const embedInfo = getEmbedInfo(url); + + if (embedInfo) { + return editor.putExternalContent({ + type: "embed", + url: embedInfo.url, + point, + embed: embedInfo.definition, + }); + } + + const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url)); + const shape = createEmptyBookmarkShape(editor, url, position); + + // Use an existing asset if we have one, or else else create a new one + let asset = editor.getAsset(assetId) as TLAsset; + let shouldAlsoCreateAsset = false; + if (!asset) { + shouldAlsoCreateAsset = true; + try { + const bookmarkAsset = await editor.getAssetForExternalContent({ + type: "url", + url, + }); + const fetchWebsite: { + title?: string; + image?: string; + description?: string; + } = await ( + await fetch(`/api/unfirlsite?website=${url}`, { + method: "POST", + }) + ).json(); + if (bookmarkAsset) { + if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title; + if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image; + if (fetchWebsite.description) + bookmarkAsset.props.description = fetchWebsite.description; + } + if (!bookmarkAsset) throw Error("Could not create an asset"); + asset = bookmarkAsset; + } catch (e) { + console.log(e); + return; + } + } + + editor.batch(() => { + if (shouldAlsoCreateAsset) { + editor.createAssets([asset]); + } + + editor.updateShapes([ + { + id: shape.id, + type: shape.type, + props: { + assetId: asset.id, + }, + }, + ]); + }); +} + +function isURL(str: string) { + try { + new URL(str); + return true; + } catch { + return false; + } +} + +function formatTextToRatio(text: string) { + const totalWidth = text.length; + const maxLineWidth = Math.floor(totalWidth / 10); + + const words = text.split(" "); + let lines = []; + let currentLine = ""; + + words.forEach((word) => { + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + return { height: (lines.length + 1) * 18, width: maxLineWidth * 10 }; +} + +export function handleExternalDroppedContent({ + text, + editor, +}: { + text: string; + editor: Editor; +}) { + const position = editor.inputs.shiftKey + ? editor.inputs.currentPagePoint + : editor.getViewportPageBounds().center; + + if (isURL(text)) { + createEmbedsFromUrl({ editor, url: text }); + } else { + // editor.createShape({ + // type: "text", + // x: position.x - 75, + // y: position.y - 75, + // props: { + // text: text, + // size: "s", + // textAlign: "start", + // }, + // }); + const { height, width } = formatTextToRatio(text); + editor.createShape({ + type: "Textcard", + x: position.x - width / 2, + y: position.y - height / 2, + props: { + content: text, + extrainfo: "https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", + w: width, + h: height, + }, + }); + } +} + +function centerSelectionAroundPoint(editor: Editor, position: VecLike) { + // Re-position shapes so that the center of the group is at the provided point + const viewportPageBounds = editor.getViewportPageBounds(); + let selectionPageBounds = editor.getSelectionPageBounds(); + + if (selectionPageBounds) { + const offset = selectionPageBounds!.center.sub(position); + + editor.updateShapes( + editor.getSelectedShapes().map((shape) => { + const localRotation = editor + .getShapeParentTransform(shape) + .decompose().rotation; + const localDelta = Vec.Rot(offset, -localRotation); + return { + id: shape.id, + type: shape.type, + x: shape.x! - localDelta.x, + y: shape.y! - localDelta.y, + }; + }), + ); + } + + // Zoom out to fit the shapes, if necessary + selectionPageBounds = editor.getSelectionPageBounds(); + if ( + selectionPageBounds && + !viewportPageBounds.contains(selectionPageBounds) + ) { + editor.zoomToSelection(); + } +} + +export function createEmptyBookmarkShape( + editor: Editor, + url: string, + position: VecLike, +): TLBookmarkShape { + const partial: TLShapePartial = { + id: createShapeId(), + type: "bookmark", + x: position.x - 150, + y: position.y - 160, + opacity: 1, + props: { + assetId: null, + url, + }, + }; + + editor.batch(() => { + editor.createShapes([partial]).select(partial.id); + centerSelectionAroundPoint(editor, position); + }); + + return editor.getShape(partial.id) as TLBookmarkShape; +} diff --git a/packages/ui/components/canvas/lib/loadSnap.ts b/packages/ui/components/canvas/lib/loadSnap.ts new file mode 100644 index 00000000..846b1967 --- /dev/null +++ b/packages/ui/components/canvas/lib/loadSnap.ts @@ -0,0 +1,14 @@ +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; +} |