diff --git a/src/App.tsx b/src/App.tsx index 7f55f75..21c8679 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,8 @@ import { Contact } from "@/routes/Contact" import { Board } from "./routes/Board" import { Inbox } from "./routes/Inbox" import { Editor, Tldraw, TLShapeId } from "tldraw" -import { components, overrides } from "./ui-overrides" +import { components } from "./ui/components" +import { overrides } from "./ui/overrides" import { ChatBoxShape } from "./shapes/ChatBoxShapeUtil" import { VideoChatShape } from "./shapes/VideoChatShapeUtil" import { ChatBoxTool } from "./tools/ChatBoxTool" diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index bf6c8c1..882dd7a 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -1,12 +1,6 @@ import { useSync } from "@tldraw/sync" import { useMemo } from "react" -import { - AssetRecordType, - getHashForString, - TLBookmarkAsset, - Tldraw, - Editor, -} from "tldraw" +import { Tldraw, Editor } from "tldraw" import { useParams } from "react-router-dom" import { ChatBoxTool } from "@/tools/ChatBoxTool" import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil" @@ -17,7 +11,9 @@ import { EmbedShape } from "@/shapes/EmbedShapeUtil" import { EmbedTool } from "@/tools/EmbedTool" import { defaultShapeUtils, defaultBindingUtils } from "tldraw" import { useState } from "react" -import { components, overrides } from "@/ui-overrides" +import { components } from "@/ui/components" +import { overrides } from "@/ui/overrides" +import { unfurlBookmarkUrl } from "../utils/unfurlBookmarkUrl" // Default to production URL if env var isn't available export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev" @@ -59,45 +55,3 @@ export function Board() { ) } - -// How does our server handle bookmark unfurling? -async function unfurlBookmarkUrl({ - url, -}: { - url: string -}): Promise { - const asset: TLBookmarkAsset = { - id: AssetRecordType.createId(getHashForString(url)), - typeName: "asset", - type: "bookmark", - meta: {}, - props: { - src: url, - description: "", - image: "", - favicon: "", - title: "", - }, - } - - try { - const response = await fetch( - `${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`, - ) - const data = (await response.json()) as { - description: string - image: string - favicon: string - title: string - } - - asset.props.description = data?.description ?? "" - asset.props.image = data?.image ?? "" - asset.props.favicon = data?.favicon ?? "" - asset.props.title = data?.title ?? "" - } catch (e) { - console.error(e) - } - - return asset -} diff --git a/src/ui-overrides.tsx b/src/ui-overrides.tsx deleted file mode 100644 index 3184ea5..0000000 --- a/src/ui-overrides.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import { - DefaultToolbar, - DefaultToolbarContent, - TLComponents, - TLUiOverrides, - TldrawUiMenuItem, - useEditor, - useTools, - DefaultContextMenu, - DefaultContextMenuContent, - TLUiContextMenuProps, - TldrawUiMenuGroup, -} from 'tldraw' -import { CustomMainMenu } from './components/CustomMainMenu' -import { Editor } from 'tldraw' - -let cameraHistory: { x: number; y: number; z: number }[] = []; -const MAX_HISTORY = 10; // Keep last 10 camera positions - -// Helper function to store camera position -const storeCameraPosition = (editor: Editor) => { - const currentCamera = editor.getCamera(); - // Only store if there's a meaningful change from the last position - const lastPosition = cameraHistory[cameraHistory.length - 1]; - if (!lastPosition || - Math.abs(lastPosition.x - currentCamera.x) > 1 || - Math.abs(lastPosition.y - currentCamera.y) > 1 || - Math.abs(lastPosition.z - currentCamera.z) > 0.1) { - - cameraHistory.push({ ...currentCamera }); - if (cameraHistory.length > MAX_HISTORY) { - cameraHistory.shift(); - } - console.log('Stored camera position:', currentCamera); - } -}; - - -export const zoomToSelection = (editor: Editor) => { - // Store camera position before zooming - storeCameraPosition(editor); - - // Get all selected shape IDs - const selectedIds = editor.getSelectedShapeIds(); - if (selectedIds.length === 0) return; - - // Get the common bounds that encompass all selected shapes - const commonBounds = editor.getSelectionPageBounds(); - if (!commonBounds) return; - - // Calculate viewport dimensions - const viewportPageBounds = editor.getViewportPageBounds(); - - // Calculate the ratio of selection size to viewport size - const widthRatio = commonBounds.width / viewportPageBounds.width; - const heightRatio = commonBounds.height / viewportPageBounds.height; - - // Calculate target zoom based on selection size - let targetZoom; - if (widthRatio < 0.1 || heightRatio < 0.1) { - // For very small selections, zoom in up to 8x - targetZoom = Math.min( - (viewportPageBounds.width * 0.8) / commonBounds.width, - (viewportPageBounds.height * 0.8) / commonBounds.height, - 8 // Max zoom of 8x for small selections - ); - } else if (widthRatio > 1 || heightRatio > 1) { - // For selections larger than viewport, zoom out more - targetZoom = Math.min( - (viewportPageBounds.width * 0.7) / commonBounds.width, - (viewportPageBounds.height * 0.7) / commonBounds.height, - 0.125 // Min zoom of 1/8x for large selections (reciprocal of 8) - ); - } else { - // For medium-sized selections, allow up to 4x zoom - targetZoom = Math.min( - (viewportPageBounds.width * 0.8) / commonBounds.width, - (viewportPageBounds.height * 0.8) / commonBounds.height, - 4 // Medium zoom level - ); - } - - // Zoom to the common bounds - editor.zoomToBounds(commonBounds, { - targetZoom, - inset: widthRatio > 1 || heightRatio > 1 ? 20 : 50, // Less padding for large selections - animation: { - duration: 400, - easing: (t) => t * (2 - t) - } - }); - - // Update URL with new camera position and first selected shape ID - const newCamera = editor.getCamera(); - const url = new URL(window.location.href); - url.searchParams.set('shapeId', selectedIds[0].toString()); - url.searchParams.set('x', newCamera.x.toString()); - url.searchParams.set('y', newCamera.y.toString()); - url.searchParams.set('zoom', newCamera.z.toString()); - window.history.replaceState(null, '', url.toString()); -}; - -const copyLinkToCurrentView = async (editor: Editor) => { - console.log('Starting copyLinkToCurrentView'); - - if (!editor.store.serialize()) { - console.warn('Store not ready'); - return; - } - - try { - const baseUrl = `${window.location.origin}${window.location.pathname}`; - console.log('Base URL:', baseUrl); - - const url = new URL(baseUrl); - const camera = editor.getCamera(); - console.log('Current camera position:', { x: camera.x, y: camera.y, zoom: camera.z }); - - // Set camera parameters - url.searchParams.set('x', camera.x.toString()); - url.searchParams.set('y', camera.y.toString()); - url.searchParams.set('zoom', camera.z.toString()); - - const finalUrl = url.toString(); - console.log('Final URL to copy:', finalUrl); - - if (navigator.clipboard && window.isSecureContext) { - console.log('Using modern clipboard API...'); - await navigator.clipboard.writeText(finalUrl); - console.log('URL copied successfully using clipboard API'); - } else { - console.log('Falling back to legacy clipboard method...'); - const textArea = document.createElement('textarea'); - textArea.value = finalUrl; - document.body.appendChild(textArea); - try { - await navigator.clipboard.writeText(textArea.value); - console.log('URL copied successfully'); - } catch (err) { - // Fallback for older browsers - textArea.select(); - document.execCommand('copy'); - console.log('URL copied using fallback method'); - } - document.body.removeChild(textArea); - } - } catch (error) { - console.error('Failed to copy to clipboard:', error); - alert('Failed to copy link. Please check clipboard permissions.'); - } -}; - -const revertCamera = (editor: Editor) => { - if (cameraHistory.length > 0) { - const previousCamera = cameraHistory.pop(); - if (previousCamera) { - // Get current viewport bounds - const viewportPageBounds = editor.getViewportPageBounds(); - - // Create bounds that center on the previous camera position - const targetBounds = { - x: previousCamera.x - (viewportPageBounds.width / 2) / previousCamera.z, - y: previousCamera.y - (viewportPageBounds.height / 2) / previousCamera.z, - w: viewportPageBounds.width / previousCamera.z, - h: viewportPageBounds.height / previousCamera.z, - }; - - // Use the same zoom animation as zoomToShape - editor.zoomToBounds(targetBounds, { - targetZoom: previousCamera.z, - animation: { - duration: 400, - easing: (t) => t * (2 - t) - } - }); - - console.log('Reverted to camera position:', previousCamera); - } - } else { - console.log('No camera history available'); - } -}; - -// Export a function that creates the uiOverrides -export const overrides: TLUiOverrides = ({ - tools(editor, tools) { - return { - ...tools, - VideoChat: { - id: 'VideoChat', - icon: 'video', - label: 'Video Chat', - kbd: 'v', - readonlyOk: true, - onSelect: () => editor.setCurrentTool('VideoChat'), - }, - ChatBox: { - id: 'ChatBox', - icon: 'chat', - label: 'Chat', - kbd: 'c', - readonlyOk: true, - onSelect: () => editor.setCurrentTool('ChatBox'), - }, - Embed: { - id: 'Embed', - icon: 'embed', - label: 'Embed', - kbd: 'e', - readonlyOk: true, - onSelect: () => editor.setCurrentTool('Embed'), - }, - } - }, - actions(editor, actions) { - return { - ...actions, - 'zoomToSelection': { - id: 'zoom-to-selection', - label: 'Zoom to Selection', - kbd: 'z', - onSelect: () => { - if (editor.getSelectedShapeIds().length > 0) { - zoomToSelection(editor); - } - }, - readonlyOk: true, - }, - 'copyLinkToCurrentView': { - id: 'copy-link-to-current-view', - label: 'Copy Link to Current View', - kbd: 's', - onSelect: () => { - copyLinkToCurrentView(editor); - }, - readonlyOk: true, - }, - 'revertCamera': { - id: 'revert-camera', - label: 'Revert Camera', - kbd: 'b', - onSelect: () => { - if (cameraHistory.length > 0) { - revertCamera(editor); - } - }, - readonlyOk: true, - }, - 'lockToFrame': { - id: 'lock-to-frame', - label: 'Lock to Frame', - kbd: 'l', - onSelect: () => { - const selectedShapes = editor.getSelectedShapes() - if (selectedShapes.length === 0) return - const selectedShape = selectedShapes[0] - const isFrame = selectedShape.type === 'frame' - const bounds = editor.getShapePageBounds(selectedShape) - if (!isFrame || !bounds) return - - editor.zoomToBounds(bounds, { - animation: { duration: 300 }, - targetZoom: 1 - }) - editor.updateInstanceState({ - meta: { ...editor.getInstanceState().meta, lockedFrameId: selectedShape.id } - }) - } - } - } - }, -}) - -export const components: TLComponents = { - Toolbar: function Toolbar() { - const editor = useEditor() - const tools = useTools() - - return ( - - - {tools['VideoChat'] && ( - - )} - {tools['ChatBox'] && ( - - )} - {tools['Embed'] && ( - - )} - - ) - }, - MainMenu: CustomMainMenu, - ContextMenu: function CustomContextMenu(props: TLUiContextMenuProps) { - const editor = useEditor() - const hasSelection = editor.getSelectedShapeIds().length > 0 - const hasCameraHistory = cameraHistory.length > 0 - const selectedShape = editor.getSelectedShapes()[0] - const isFrame = selectedShape?.type === 'frame' - - return ( - - - - {/* Camera Controls Group */} - - zoomToSelection(editor)} - /> - copyLinkToCurrentView(editor)} - /> - revertCamera(editor)} - /> - - - {/* Creation Tools Group */} - - { editor.setCurrentTool('VideoChat'); }} - /> - { editor.setCurrentTool('ChatBox'); }} - /> - { editor.setCurrentTool('Embed'); }} - /> - - - {/* Frame Controls */} - {isFrame && ( - - { - console.warn('lock to frame NOT IMPLEMENTED') - }} - /> - - )} - - ) - } -} \ No newline at end of file diff --git a/src/ui/CustomContextMenu.tsx b/src/ui/CustomContextMenu.tsx new file mode 100644 index 0000000..0cc24cb --- /dev/null +++ b/src/ui/CustomContextMenu.tsx @@ -0,0 +1,102 @@ +import { TldrawUiMenuItem } from "tldraw" + +import { TldrawUiMenuGroup } from "tldraw" + +import { DefaultContextMenuContent } from "tldraw" + +import { DefaultContextMenu } from "tldraw" + +import { TLUiContextMenuProps, useEditor } from "tldraw" +import { + cameraHistory, + copyLinkToCurrentView, + revertCamera, + zoomToSelection, +} from "./cameraUtils" + +export function CustomContextMenu(props: TLUiContextMenuProps) { + const editor = useEditor() + const hasSelection = editor.getSelectedShapeIds().length > 0 + const hasCameraHistory = cameraHistory.length > 0 + const selectedShape = editor.getSelectedShapes()[0] + const isFrame = selectedShape?.type === "frame" + + return ( + + + + {/* Camera Controls Group */} + + zoomToSelection(editor)} + /> + copyLinkToCurrentView(editor)} + /> + revertCamera(editor)} + /> + + + {/* Creation Tools Group */} + + { + editor.setCurrentTool("VideoChat") + }} + /> + { + editor.setCurrentTool("ChatBox") + }} + /> + { + editor.setCurrentTool("Embed") + }} + /> + + + {/* Frame Controls */} + {isFrame && ( + + { + console.warn("lock to frame NOT IMPLEMENTED") + }} + /> + + )} + + ) +} diff --git a/src/components/CustomMainMenu.tsx b/src/ui/CustomMainMenu.tsx similarity index 100% rename from src/components/CustomMainMenu.tsx rename to src/ui/CustomMainMenu.tsx diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx new file mode 100644 index 0000000..e297e48 --- /dev/null +++ b/src/ui/CustomToolbar.tsx @@ -0,0 +1,39 @@ +import { TldrawUiMenuItem } from "tldraw" +import { DefaultToolbar, DefaultToolbarContent } from "tldraw" +import { useTools } from "tldraw" +import { useEditor } from "tldraw" + +export function CustomToolbar() { + const editor = useEditor() + const tools = useTools() + + return ( + + + {tools["VideoChat"] && ( + + )} + {tools["ChatBox"] && ( + + )} + {tools["Embed"] && ( + + )} + + ) +} diff --git a/src/ui/cameraUtils.ts b/src/ui/cameraUtils.ts new file mode 100644 index 0000000..c1ecf75 --- /dev/null +++ b/src/ui/cameraUtils.ts @@ -0,0 +1,193 @@ +import { Editor } from "tldraw" + +export const cameraHistory: { x: number; y: number; z: number }[] = [] +const MAX_HISTORY = 10 // Keep last 10 camera positions + +// Helper function to store camera position +const storeCameraPosition = (editor: Editor) => { + const currentCamera = editor.getCamera() + // Only store if there's a meaningful change from the last position + const lastPosition = cameraHistory[cameraHistory.length - 1] + if ( + !lastPosition || + Math.abs(lastPosition.x - currentCamera.x) > 1 || + Math.abs(lastPosition.y - currentCamera.y) > 1 || + Math.abs(lastPosition.z - currentCamera.z) > 0.1 + ) { + cameraHistory.push({ ...currentCamera }) + if (cameraHistory.length > MAX_HISTORY) { + cameraHistory.shift() + } + console.log("Stored camera position:", currentCamera) + } +} + +export const zoomToSelection = (editor: Editor) => { + // Store camera position before zooming + storeCameraPosition(editor) + + // Get all selected shape IDs + const selectedIds = editor.getSelectedShapeIds() + if (selectedIds.length === 0) return + + // Get the common bounds that encompass all selected shapes + const commonBounds = editor.getSelectionPageBounds() + if (!commonBounds) return + + // Calculate viewport dimensions + const viewportPageBounds = editor.getViewportPageBounds() + + // Calculate the ratio of selection size to viewport size + const widthRatio = commonBounds.width / viewportPageBounds.width + const heightRatio = commonBounds.height / viewportPageBounds.height + + // Calculate target zoom based on selection size + let targetZoom + if (widthRatio < 0.1 || heightRatio < 0.1) { + // For very small selections, zoom in up to 8x + targetZoom = Math.min( + (viewportPageBounds.width * 0.8) / commonBounds.width, + (viewportPageBounds.height * 0.8) / commonBounds.height, + 8, // Max zoom of 8x for small selections + ) + } else if (widthRatio > 1 || heightRatio > 1) { + // For selections larger than viewport, zoom out more + targetZoom = Math.min( + (viewportPageBounds.width * 0.7) / commonBounds.width, + (viewportPageBounds.height * 0.7) / commonBounds.height, + 0.125, // Min zoom of 1/8x for large selections (reciprocal of 8) + ) + } else { + // For medium-sized selections, allow up to 4x zoom + targetZoom = Math.min( + (viewportPageBounds.width * 0.8) / commonBounds.width, + (viewportPageBounds.height * 0.8) / commonBounds.height, + 4, // Medium zoom level + ) + } + + // Zoom to the common bounds + editor.zoomToBounds(commonBounds, { + targetZoom, + inset: widthRatio > 1 || heightRatio > 1 ? 20 : 50, // Less padding for large selections + animation: { + duration: 400, + easing: (t) => t * (2 - t), + }, + }) + + // Update URL with new camera position and first selected shape ID + const newCamera = editor.getCamera() + const url = new URL(window.location.href) + url.searchParams.set("shapeId", selectedIds[0].toString()) + url.searchParams.set("x", newCamera.x.toString()) + url.searchParams.set("y", newCamera.y.toString()) + url.searchParams.set("zoom", newCamera.z.toString()) + window.history.replaceState(null, "", url.toString()) +} + +export const revertCamera = (editor: Editor) => { + if (cameraHistory.length > 0) { + const previousCamera = cameraHistory.pop() + if (previousCamera) { + // Get current viewport bounds + const viewportPageBounds = editor.getViewportPageBounds() + + // Create bounds that center on the previous camera position + const targetBounds = { + x: previousCamera.x - viewportPageBounds.width / 2 / previousCamera.z, + y: previousCamera.y - viewportPageBounds.height / 2 / previousCamera.z, + w: viewportPageBounds.width / previousCamera.z, + h: viewportPageBounds.height / previousCamera.z, + } + + // Use the same zoom animation as zoomToShape + editor.zoomToBounds(targetBounds, { + targetZoom: previousCamera.z, + animation: { + duration: 400, + easing: (t) => t * (2 - t), + }, + }) + + console.log("Reverted to camera position:", previousCamera) + } + } else { + console.log("No camera history available") + } +} + +export const copyLinkToCurrentView = async (editor: Editor) => { + console.log("Starting copyLinkToCurrentView") + + if (!editor.store.serialize()) { + console.warn("Store not ready") + return + } + + try { + const baseUrl = `${window.location.origin}${window.location.pathname}` + console.log("Base URL:", baseUrl) + + const url = new URL(baseUrl) + const camera = editor.getCamera() + console.log("Current camera position:", { + x: camera.x, + y: camera.y, + zoom: camera.z, + }) + + // Set camera parameters + url.searchParams.set("x", camera.x.toString()) + url.searchParams.set("y", camera.y.toString()) + url.searchParams.set("zoom", camera.z.toString()) + + const finalUrl = url.toString() + console.log("Final URL to copy:", finalUrl) + + if (navigator.clipboard && window.isSecureContext) { + console.log("Using modern clipboard API...") + await navigator.clipboard.writeText(finalUrl) + console.log("URL copied successfully using clipboard API") + } else { + console.log("Falling back to legacy clipboard method...") + const textArea = document.createElement("textarea") + textArea.value = finalUrl + document.body.appendChild(textArea) + try { + await navigator.clipboard.writeText(textArea.value) + console.log("URL copied successfully") + } catch (err) { + // Fallback for older browsers + textArea.select() + document.execCommand("copy") + console.log("URL copied using fallback method") + } + document.body.removeChild(textArea) + } + } catch (error) { + console.error("Failed to copy to clipboard:", error) + alert("Failed to copy link. Please check clipboard permissions.") + } +} + +// TODO: doesnt lock permanently +export const lockCameraToFrame = (editor: Editor) => { + const selectedShapes = editor.getSelectedShapes() + if (selectedShapes.length === 0) return + const selectedShape = selectedShapes[0] + const isFrame = selectedShape.type === "frame" + const bounds = editor.getShapePageBounds(selectedShape) + if (!isFrame || !bounds) return + + editor.zoomToBounds(bounds, { + animation: { duration: 300 }, + targetZoom: 1, + }) + editor.updateInstanceState({ + meta: { + ...editor.getInstanceState().meta, + lockedFrameId: selectedShape.id, + }, + }) +} diff --git a/src/ui/components.tsx b/src/ui/components.tsx new file mode 100644 index 0000000..ed302a7 --- /dev/null +++ b/src/ui/components.tsx @@ -0,0 +1,10 @@ +import { CustomMainMenu } from "./CustomMainMenu" +import { CustomToolbar } from "./CustomToolbar" +import { CustomContextMenu } from "./CustomContextMenu" +import { TLComponents } from "tldraw" + +export const components: TLComponents = { + Toolbar: CustomToolbar, + MainMenu: CustomMainMenu, + ContextMenu: CustomContextMenu, +} diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx new file mode 100644 index 0000000..745171e --- /dev/null +++ b/src/ui/overrides.tsx @@ -0,0 +1,82 @@ +import { TLUiOverrides } from "tldraw" +import { + cameraHistory, + copyLinkToCurrentView, + lockCameraToFrame, + revertCamera, + zoomToSelection, +} from "./cameraUtils" + +export const overrides: TLUiOverrides = { + tools(editor, tools) { + return { + ...tools, + VideoChat: { + id: "VideoChat", + icon: "video", + label: "Video Chat", + kbd: "v", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("VideoChat"), + }, + ChatBox: { + id: "ChatBox", + icon: "chat", + label: "Chat", + kbd: "c", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("ChatBox"), + }, + Embed: { + id: "Embed", + icon: "embed", + label: "Embed", + kbd: "e", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("Embed"), + }, + } + }, + actions(editor, actions) { + return { + ...actions, + zoomToSelection: { + id: "zoom-to-selection", + label: "Zoom to Selection", + kbd: "z", + onSelect: () => { + if (editor.getSelectedShapeIds().length > 0) { + zoomToSelection(editor) + } + }, + readonlyOk: true, + }, + copyLinkToCurrentView: { + id: "copy-link-to-current-view", + label: "Copy Link to Current View", + kbd: "s", + onSelect: () => { + copyLinkToCurrentView(editor) + }, + readonlyOk: true, + }, + revertCamera: { + id: "revert-camera", + label: "Revert Camera", + kbd: "b", + onSelect: () => { + if (cameraHistory.length > 0) { + revertCamera(editor) + } + }, + readonlyOk: true, + }, + lockToFrame: { + id: "lock-to-frame", + label: "Lock to Frame", + kbd: "l", + onSelect: () => lockCameraToFrame(editor), + }, + } + }, +} diff --git a/src/utils/unfurlBookmarkUrl.tsx b/src/utils/unfurlBookmarkUrl.tsx new file mode 100644 index 0000000..275552c --- /dev/null +++ b/src/utils/unfurlBookmarkUrl.tsx @@ -0,0 +1,43 @@ +import { TLBookmarkAsset, AssetRecordType, getHashForString } from "tldraw" +import { WORKER_URL } from "../routes/Board" + +export async function unfurlBookmarkUrl({ + url, +}: { + url: string +}): Promise { + const asset: TLBookmarkAsset = { + id: AssetRecordType.createId(getHashForString(url)), + typeName: "asset", + type: "bookmark", + meta: {}, + props: { + src: url, + description: "", + image: "", + favicon: "", + title: "", + }, + } + + try { + const response = await fetch( + `${WORKER_URL}/unfurl?url=${encodeURIComponent(url)}`, + ) + const data = (await response.json()) as { + description: string + image: string + favicon: string + title: string + } + + asset.props.description = data?.description ?? "" + asset.props.image = data?.image ?? "" + asset.props.favicon = data?.favicon ?? "" + asset.props.title = data?.title ?? "" + } catch (e) { + console.error(e) + } + + return asset +}