aboutsummaryrefslogtreecommitdiff
path: root/apps/web/app/(canvas)
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2024-06-18 17:58:46 -0500
committerDhravya Shah <[email protected]>2024-06-18 17:58:46 -0500
commitf4bb71e8f7e07bb2e919b7f222d5acb2905eb8f2 (patch)
tree7310dc521ef3559055bbe71f50c3861be2fa0503 /apps/web/app/(canvas)
parentdarkmode by default - so that the colors don't f up on lightmode devices (diff)
parentCreate Embeddings for Canvas (diff)
downloadsupermemory-default-darkmode.tar.xz
supermemory-default-darkmode.zip
Diffstat (limited to 'apps/web/app/(canvas)')
-rw-r--r--apps/web/app/(canvas)/canvas.tsx55
-rw-r--r--apps/web/app/(canvas)/canvas/layout.tsx13
-rw-r--r--apps/web/app/(canvas)/canvas/page.tsx99
-rw-r--r--apps/web/app/(canvas)/canvasStyles.css24
-rw-r--r--apps/web/app/(canvas)/enabledComp.tsx22
-rw-r--r--apps/web/app/(canvas)/lib/createAssetUrl.ts94
-rw-r--r--apps/web/app/(canvas)/lib/createEmbeds.ts142
-rw-r--r--apps/web/app/(canvas)/lib/loadSnap.ts13
-rw-r--r--apps/web/app/(canvas)/savesnap.tsx43
-rw-r--r--apps/web/app/(canvas)/svg.tsx97
-rw-r--r--apps/web/app/(canvas)/twitterCard.tsx84
11 files changed, 686 insertions, 0 deletions
diff --git a/apps/web/app/(canvas)/canvas.tsx b/apps/web/app/(canvas)/canvas.tsx
new file mode 100644
index 00000000..9ec57d6d
--- /dev/null
+++ b/apps/web/app/(canvas)/canvas.tsx
@@ -0,0 +1,55 @@
+import { useCallback, useEffect, useMemo, 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 createEmbedsFromUrl from "./lib/createEmbeds";
+import { loadRemoteSnapshot } from "./lib/loadSnap";
+import { SaveStatus } from "./savesnap";
+import { getAssetUrls } from '@tldraw/assets/selfHosted'
+import { memo } from 'react';
+
+export const Canvas = 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 (
+ <Tldraw
+ assetUrls={assetUrls}
+ components={components}
+ store={storeWithStatus}
+ shapeUtils={[twitterCardUtil]}
+ 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>
+ </Tldraw>
+ );
+})
diff --git a/apps/web/app/(canvas)/canvas/layout.tsx b/apps/web/app/(canvas)/canvas/layout.tsx
new file mode 100644
index 00000000..9bc3b6d7
--- /dev/null
+++ b/apps/web/app/(canvas)/canvas/layout.tsx
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 00000000..366a4481
--- /dev/null
+++ b/apps/web/app/(canvas)/canvas/page.tsx
@@ -0,0 +1,99 @@
+"use client";
+
+// import Canvas from "./_components/canvas";
+import {Canvas} from "../canvas";
+import React, { useState } from "react";
+// import ReactTextareaAutosize from "react-textarea-autosize";
+import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
+import {
+ DragSvg,
+ SettingsSvg,
+ LinkSvg,
+ ThreeDBlock,
+ TextLoadingSvg,
+} from "../svg";
+
+function page() {
+ const [value, setValue] = useState("");
+ const [fullScreen, setFullScreen] = useState(false);
+
+ return (
+ <div className={`h-screen w-full ${ !fullScreen ? "px-4 py-6": "bg-[#1F2428]"} transition-all`}>
+ <div>
+ <PanelGroup 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} minSize={22}>
+ <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
+ <SettingsSvg />
+ </div>
+ <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 flex-col gap-10">
+ <div className="flex gap-4 px-3 text-[#989EA4]">
+ <TextLoadingSvg />
+ <h1>
+ Nvidia will most likely create monopoly in software industry
+ as they are already largest player in GPU hardware by 20...
+ </h1>
+ </div>
+ <div className="flex gap-4 px-3 text-[#989EA4]">
+ <ThreeDBlock />
+ <div className="flex flex-col gap-2">
+ <div>
+ <h1 className="line-clamp-3">
+ Nvidia currently dominates the GPU hardware market, with
+ a market share over 97%. This has led some to argue...
+ </h1>
+ </div>
+ <p className="line-clamp-1 text-[#369DFD]">
+ From space: GPU GOATS
+ </p>
+ </div>
+ </div>
+ <div className="flex gap-4 px-3 text-[#989EA4]">
+ <LinkSvg />
+ <div className="flex flex-col gap-2">
+ <div>
+ <h1 className="line-clamp-3">
+ Nvidia currently dominates the GPU hardware market, with
+ a market share over 97%. This has led some to argue...
+ </h1>
+ </div>
+ <p className="line-clamp-1 text-[#369DFD]">
+ Page url:
+ https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </Panel>
+ <PanelResizeHandle className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`}>
+ {/* <div className="absolute z-[1000000] top-1/2 -translate-y-1/2"> */}
+ <div className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}>
+ <DragSvg />
+ </div>
+ {/* </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>
+ );
+}
+
+export default page;
diff --git a/apps/web/app/(canvas)/canvasStyles.css b/apps/web/app/(canvas)/canvasStyles.css
new file mode 100644
index 00000000..a53d8c96
--- /dev/null
+++ b/apps/web/app/(canvas)/canvasStyles.css
@@ -0,0 +1,24 @@
+.tl-background {
+ 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-navigation-panel::before {
+ border-top: #2C3439 !important;
+ border-right: #2C3439 !important;
+}
+
+.tlui-minimap {
+ background: #2C3439 !important;
+}
+
+.tlui-minimap__canvas {
+ background: #1F2428 !important;
+} \ No newline at end of file
diff --git a/apps/web/app/(canvas)/enabledComp.tsx b/apps/web/app/(canvas)/enabledComp.tsx
new file mode 100644
index 00000000..5dbe6ee7
--- /dev/null
+++ b/apps/web/app/(canvas)/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,
+ // Minimap: null,
+ // ContextMenu: null,
+ // HelpMenu: null,
+ // ZoomMenu: null,
+ // StylePanel: null,
+ // PageMenu: null,
+ // NavigationPanel: null,
+ // Toolbar: null,
+ // KeyboardShortcutsDialog: null,
+ // HelperButtons: null,
+ // SharePanel: null,
+ // MenuPanel: null,
+}; \ No newline at end of file
diff --git a/apps/web/app/(canvas)/lib/createAssetUrl.ts b/apps/web/app/(canvas)/lib/createAssetUrl.ts
new file mode 100644
index 00000000..05c2baea
--- /dev/null
+++ b/apps/web/app/(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/apps/web/app/(canvas)/lib/createEmbeds.ts b/apps/web/app/(canvas)/lib/createEmbeds.ts
new file mode 100644
index 00000000..53d81533
--- /dev/null
+++ b/apps/web/app/(canvas)/lib/createEmbeds.ts
@@ -0,0 +1,142 @@
+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 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
+} \ No newline at end of file
diff --git a/apps/web/app/(canvas)/lib/loadSnap.ts b/apps/web/app/(canvas)/lib/loadSnap.ts
new file mode 100644
index 00000000..15aad998
--- /dev/null
+++ b/apps/web/app/(canvas)/lib/loadSnap.ts
@@ -0,0 +1,13 @@
+import { createTLStore, defaultShapeUtils } from "tldraw";
+import { twitterCardUtil } from "../twitterCard";
+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],
+ });
+ newStore.loadSnapshot(snapshot);
+ return newStore;
+} \ No newline at end of file
diff --git a/apps/web/app/(canvas)/savesnap.tsx b/apps/web/app/(canvas)/savesnap.tsx
new file mode 100644
index 00000000..f82e97e3
--- /dev/null
+++ b/apps/web/app/(canvas)/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/apps/web/app/(canvas)/svg.tsx b/apps/web/app/(canvas)/svg.tsx
new file mode 100644
index 00000000..bae4e614
--- /dev/null
+++ b/apps/web/app/(canvas)/svg.tsx
@@ -0,0 +1,97 @@
+export function SettingsSvg() {
+ return (
+ <svg
+ width="16"
+ height="18"
+ viewBox="0 0 16 18"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M7.2321 0.875C6.46793 0.875 5.81627 1.4275 5.69043 2.18083L5.5421 3.07417C5.52543 3.17417 5.44627 3.29083 5.2946 3.36417C5.00906 3.50143 4.73438 3.66022 4.47293 3.83917C4.3346 3.935 4.1946 3.94417 4.09793 3.90833L3.25043 3.59C2.90397 3.4602 2.52268 3.45755 2.17445 3.58253C1.82621 3.70751 1.53362 3.95201 1.34877 4.2725L0.580434 5.60333C0.395508 5.92363 0.330196 6.29915 0.396115 6.66307C0.462034 7.027 0.65491 7.35575 0.940434 7.59083L1.64043 8.1675C1.7196 8.2325 1.7821 8.35833 1.76877 8.52583C1.74502 8.84178 1.74502 9.15906 1.76877 9.475C1.78127 9.64167 1.7196 9.76833 1.64127 9.83333L0.940434 10.41C0.65491 10.6451 0.462034 10.9738 0.396115 11.3378C0.330196 11.7017 0.395508 12.0772 0.580434 12.3975L1.34877 13.7283C1.53376 14.0487 1.8264 14.293 2.17462 14.4178C2.52285 14.5426 2.90406 14.5399 3.25043 14.41L4.0996 14.0917C4.19543 14.0558 4.33543 14.0658 4.4746 14.16C4.7346 14.3383 5.00877 14.4975 5.29543 14.635C5.4471 14.7083 5.52627 14.825 5.54293 14.9267L5.69127 15.8192C5.8171 16.5725 6.46877 17.125 7.23293 17.125H8.7696C9.53293 17.125 10.1854 16.5725 10.3113 15.8192L10.4596 14.9258C10.4763 14.8258 10.5546 14.7092 10.7071 14.635C10.9938 14.4975 11.2679 14.3383 11.5279 14.16C11.6671 14.065 11.8071 14.0558 11.9029 14.0917L12.7529 14.41C13.0992 14.5394 13.4801 14.5418 13.828 14.4168C14.1758 14.2919 14.4681 14.0476 14.6529 13.7275L15.4221 12.3967C15.607 12.0764 15.6723 11.7009 15.6064 11.3369C15.5405 10.973 15.3476 10.6443 15.0621 10.4092L14.3621 9.8325C14.2829 9.7675 14.2204 9.64167 14.2338 9.47417C14.2575 9.15822 14.2575 8.84095 14.2338 8.525C14.2204 8.35833 14.2829 8.23167 14.3613 8.16667L15.0613 7.59C15.6513 7.105 15.8038 6.265 15.4221 5.6025L14.6538 4.27167C14.4688 3.95132 14.1761 3.707 13.8279 3.58218C13.4797 3.45735 13.0985 3.46013 12.7521 3.59L11.9021 3.90833C11.8071 3.94417 11.6671 3.93417 11.5279 3.83917C11.2668 3.66025 10.9924 3.50145 10.7071 3.36417C10.5546 3.29167 10.4763 3.175 10.4596 3.07417L10.3104 2.18083C10.2497 1.81589 10.0614 1.48435 9.77905 1.24522C9.49674 1.0061 9.13874 0.874907 8.76877 0.875H7.23293H7.2321ZM8.00043 12.125C8.82923 12.125 9.62409 11.7958 10.2101 11.2097C10.7962 10.6237 11.1254 9.8288 11.1254 9C11.1254 8.1712 10.7962 7.37634 10.2101 6.79029C9.62409 6.20424 8.82923 5.875 8.00043 5.875C7.17163 5.875 6.37678 6.20424 5.79072 6.79029C5.20467 7.37634 4.87543 8.1712 4.87543 9C4.87543 9.8288 5.20467 10.6237 5.79072 11.2097C6.37678 11.7958 7.17163 12.125 8.00043 12.125Z"
+ fill="#989EA4"
+ />
+ </svg>
+ );
+}
+
+export function DragSvg() {
+ return (
+ <svg
+ width="6"
+ height="9"
+ viewBox="0 0 6 9"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ fill-rule="evenodd"
+ clip-rule="evenodd"
+ d="M4.78829 0.916134C4.78348 0.920945 4.77696 0.923648 4.77015 0.923648C4.76335 0.923648 4.75682 0.920945 4.75201 0.916134C4.7472 0.911322 4.74449 0.904797 4.74449 0.897991C4.74449 0.891186 4.7472 0.884661 4.75201 0.879849C4.75682 0.875038 4.76335 0.872335 4.77015 0.872335C4.77696 0.872335 4.78348 0.875038 4.78829 0.879849C4.79311 0.884661 4.79581 0.891186 4.79581 0.897991C4.79581 0.904797 4.79311 0.911322 4.78829 0.916134ZM4.77015 0C4.53199 0 4.30358 0.0946096 4.13518 0.263016C3.96677 0.431421 3.87216 0.659829 3.87216 0.897991C3.87216 1.13615 3.96677 1.36456 4.13518 1.53297C4.30358 1.70137 4.53199 1.79598 4.77015 1.79598C5.00831 1.79598 5.23672 1.70137 5.40513 1.53297C5.57353 1.36456 5.66814 1.13615 5.66814 0.897991C5.66814 0.659829 5.57353 0.431421 5.40513 0.263016C5.23672 0.0946096 5.00831 0 4.77015 0ZM4.78829 4.40547C4.78348 4.41028 4.77696 4.41299 4.77015 4.41299C4.76335 4.41299 4.75682 4.41028 4.75201 4.40547C4.7472 4.40066 4.74449 4.39414 4.74449 4.38733C4.74449 4.38052 4.7472 4.374 4.75201 4.36919C4.75682 4.36438 4.76335 4.36167 4.77015 4.36167C4.77696 4.36167 4.78348 4.36438 4.78829 4.36919C4.79311 4.374 4.79581 4.38052 4.79581 4.38733C4.79581 4.39414 4.79311 4.40066 4.78829 4.40547ZM4.77015 3.48934C4.53199 3.48934 4.30358 3.58395 4.13518 3.75235C3.96677 3.92076 3.87216 4.14917 3.87216 4.38733C3.87216 4.62549 3.96677 4.8539 4.13518 5.02231C4.30358 5.19071 4.53199 5.28532 4.77015 5.28532C5.00831 5.28532 5.23672 5.19071 5.40513 5.02231C5.57353 4.8539 5.66814 4.62549 5.66814 4.38733C5.66814 4.14917 5.57353 3.92076 5.40513 3.75235C5.23672 3.58395 5.00831 3.48934 4.77015 3.48934ZM4.78829 7.89481C4.78348 7.89962 4.77696 7.90232 4.77015 7.90232C4.76335 7.90232 4.75682 7.89962 4.75201 7.89481C4.7472 7.89 4.74449 7.88347 4.74449 7.87667C4.74449 7.86986 4.7472 7.86334 4.75201 7.85853C4.75682 7.85371 4.76335 7.85101 4.77015 7.85101C4.77696 7.85101 4.78348 7.85371 4.78829 7.85853C4.79311 7.86334 4.79581 7.86986 4.79581 7.87667C4.79581 7.88347 4.79311 7.89 4.78829 7.89481ZM4.77015 6.97868C4.53199 6.97868 4.30358 7.07329 4.13518 7.24169C3.96677 7.4101 3.87216 7.63851 3.87216 7.87667C3.87216 8.11483 3.96677 8.34324 4.13518 8.51164C4.30358 8.68005 4.53199 8.77466 4.77015 8.77466C5.00831 8.77466 5.23672 8.68005 5.40513 8.51164C5.57353 8.34324 5.66814 8.11483 5.66814 7.87667C5.66814 7.63851 5.57353 7.4101 5.40513 7.24169C5.23672 7.07329 5.00831 6.97868 4.77015 6.97868ZM0.916134 0.91702C0.911322 0.921832 0.904796 0.924535 0.897991 0.924535C0.891187 0.924535 0.884661 0.921832 0.879849 0.91702C0.875038 0.912209 0.872335 0.905683 0.872335 0.898878C0.872335 0.892073 0.875038 0.885547 0.879849 0.880736C0.884661 0.875924 0.891187 0.873221 0.897991 0.873221C0.904796 0.873221 0.911322 0.875924 0.916134 0.880736C0.920945 0.885547 0.923648 0.892073 0.923648 0.898878C0.923648 0.905683 0.920945 0.912209 0.916134 0.91702ZM0.897991 0.000886679C0.659829 0.000886679 0.431422 0.0954962 0.263016 0.263902C0.0946102 0.432308 0 0.660715 0 0.898878C0 1.13704 0.0946102 1.36545 0.263016 1.53385C0.431422 1.70226 0.659829 1.79687 0.897991 1.79687C1.13615 1.79687 1.36456 1.70226 1.53297 1.53385C1.70137 1.36545 1.79598 1.13704 1.79598 0.898878C1.79598 0.660715 1.70137 0.432308 1.53297 0.263902C1.36456 0.0954962 1.13615 0.000886679 0.897991 0.000886679ZM0.916134 4.40636C0.911323 4.41117 0.904797 4.41387 0.897991 4.41387C0.891186 4.41387 0.88466 4.41117 0.879849 4.40636C0.875038 4.40155 0.872335 4.39502 0.872335 4.38822C0.872335 4.38141 0.875038 4.37489 0.879849 4.37007C0.88466 4.36526 0.891186 4.36256 0.897991 4.36256C0.904797 4.36256 0.911323 4.36526 0.916134 4.37007C0.920945 4.37489 0.923648 4.38141 0.923648 4.38822C0.923648 4.39502 0.920945 4.40155 0.916134 4.40636ZM0.897991 3.49022C0.659828 3.49022 0.431421 3.58484 0.263016 3.75324C0.0946104 3.92165 0 4.15005 0 4.38822C0 4.62638 0.0946104 4.85479 0.263016 5.02319C0.431421 5.1916 0.659828 5.28621 0.897991 5.28621C1.13615 5.28621 1.36456 5.1916 1.53297 5.02319C1.70137 4.85479 1.79598 4.62638 1.79598 4.38822C1.79598 4.15005 1.70137 3.92165 1.53297 3.75324C1.36456 3.58484 1.13615 3.49022 0.897991 3.49022ZM0.916134 7.8957C0.911322 7.90051 0.904795 7.90321 0.897991 7.90321C0.891187 7.90321 0.884661 7.90051 0.879849 7.8957C0.875038 7.89089 0.872335 7.88436 0.872335 7.87755C0.872335 7.87075 0.875038 7.86422 0.879849 7.85941C0.884661 7.8546 0.891187 7.8519 0.897991 7.8519C0.904795 7.8519 0.911322 7.8546 0.916134 7.85941C0.920945 7.86422 0.923648 7.87075 0.923648 7.87755C0.923648 7.88436 0.920945 7.89089 0.916134 7.8957ZM0.897991 6.97956C0.65983 6.97956 0.431422 7.07417 0.263016 7.24258C0.0946099 7.41098 0 7.63939 0 7.87755C0 8.11572 0.0946099 8.34412 0.263016 8.51253C0.431422 8.68094 0.65983 8.77555 0.897991 8.77555C1.13615 8.77555 1.36456 8.68094 1.53297 8.51253C1.70137 8.34412 1.79598 8.11572 1.79598 7.87755C1.79598 7.63939 1.70137 7.41098 1.53297 7.24258C1.36456 7.07417 1.13615 6.97956 0.897991 6.97956Z"
+ fill="#989EA4"
+ />
+ </svg>
+ );
+}
+
+export function TextLoadingSvg() {
+ return (
+ <svg
+ width="34"
+ height="24"
+ viewBox="0 0 14 10"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M0.8125 1.0625H13.1875M0.8125 5H13.1875M0.8125 8.9375H7"
+ stroke="#989EA4"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ );
+}
+
+export function ThreeDBlock() {
+ return (
+ <svg
+ width="32"
+ height="36"
+ viewBox="0 0 16 18"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M14.75 5.625L8 1.6875L1.25 5.625M14.75 5.625L8 9.5625M14.75 5.625V12.375L8 16.3125M1.25 5.625L8 9.5625M1.25 5.625V12.375L8 16.3125M8 9.5625V16.3125"
+ stroke="#989EA4"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ );
+}
+
+export function LinkSvg() {
+ return (
+ <svg
+ width="36"
+ height="36"
+ viewBox="0 0 18 18"
+ fill="none"
+ xmlns="http://www.w3.org/2000/svg"
+ >
+ <path
+ d="M9.89252 6.51602C10.3799 6.74871 10.8043 7.09497 11.1301 7.5257C11.4559 7.95643 11.6736 8.45906 11.7648 8.99136C11.8561 9.52366 11.8183 10.0701 11.6546 10.5848C11.4909 11.0994 11.206 11.5673 10.824 11.949L7.44902 15.324C6.81608 15.957 5.95763 16.3125 5.06252 16.3125C4.16741 16.3125 3.30896 15.957 2.67602 15.324C2.04308 14.6911 1.6875 13.8326 1.6875 12.9375C1.6875 12.0424 2.04308 11.184 2.67602 10.551L3.99377 9.23327M14.0063 8.76677L15.324 7.44902C15.957 6.81608 16.3125 5.95763 16.3125 5.06252C16.3125 4.16741 15.957 3.30896 15.324 2.67602C14.6911 2.04308 13.8326 1.6875 12.9375 1.6875C12.0424 1.6875 11.184 2.04308 10.551 2.67602L7.17602 6.05102C6.794 6.43277 6.50917 6.90063 6.34546 7.41529C6.18175 7.92995 6.14393 8.47638 6.2352 9.00868C6.32646 9.54098 6.54414 10.0436 6.86994 10.4743C7.19574 10.9051 7.62015 11.2513 8.10752 11.484"
+ stroke="#989EA4"
+ stroke-width="1.5"
+ stroke-linecap="round"
+ stroke-linejoin="round"
+ />
+ </svg>
+ );
+}
diff --git a/apps/web/app/(canvas)/twitterCard.tsx b/apps/web/app/(canvas)/twitterCard.tsx
new file mode 100644
index 00000000..c5582a98
--- /dev/null
+++ b/apps/web/app/(canvas)/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>`}
+ />
+ );
+}