diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..e225180 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "semi": false, + "trailingComma": "all" + } \ No newline at end of file diff --git a/build/markdownPlugin.js b/build/markdownPlugin.js deleted file mode 100644 index 4db9f11..0000000 --- a/build/markdownPlugin.js +++ /dev/null @@ -1,17 +0,0 @@ -import matter from "gray-matter"; -import { markdownToHtml } from "./markdownToHtml"; -import path from "path"; - -export const markdownPlugin = { - name: "markdown-plugin", - enforce: "pre", - transform(code, id) { - if (id.endsWith(".md")) { - const { data, content } = matter(code); - const filename = path.basename(id, ".md"); - const html = markdownToHtml(filename, content); - return `export const html = ${JSON.stringify(html)}; - export const data = ${JSON.stringify(data)};`; - } - }, -}; diff --git a/build/markdownToHtml.js b/build/markdownToHtml.js deleted file mode 100644 index a7e92fd..0000000 --- a/build/markdownToHtml.js +++ /dev/null @@ -1,42 +0,0 @@ -import MarkdownIt from "markdown-it"; -// import markdownItLatex from "markdown-it-latex"; -import markdownLatex from "markdown-it-latex2img"; - -const md = new MarkdownIt({ - html: true, - breaks: true, - linkify: true, -}); - -md.use( - markdownLatex, - // {style: "width: 200%; height: 200%;",} -); - -// const mediaSrc = (folderName, fileName) => { -// return `/posts/${folderName}/${fileName}`; -// }; - -md.renderer.rules.code_block = (tokens, idx, options, env, self) => { - console.log("tokens", tokens); - return `${tokens[idx].content}`; -}; -md.renderer.rules.image = (tokens, idx, options, env, self) => { - const token = tokens[idx]; - const src = token.attrGet("src"); - const alt = token.content; - const postName = env.postName; - const formattedSrc = `/posts/${postName}/${src}`; - - if (src.endsWith(".mp4") || src.endsWith(".mov")) { - return ``; - } - - return `${alt}`; -}; - -export function markdownToHtml(postName, content) { - return md.render(content, { postName: postName }); -} diff --git a/src/hooks/useCameraControls.ts b/src/hooks/useCameraControls.ts index 6a0a688..22b879c 100644 --- a/src/hooks/useCameraControls.ts +++ b/src/hooks/useCameraControls.ts @@ -1,129 +1,127 @@ -import { useEffect } from 'react'; -import { Editor, TLEventMap, TLFrameShape, TLParentId } from 'tldraw'; -import { useSearchParams } from 'react-router-dom'; +import { useEffect } from "react" +import { Editor, TLEventMap, TLFrameShape, TLParentId } from "tldraw" +import { useSearchParams } from "react-router-dom" // Define camera state interface interface CameraState { - x: number; - y: number; - z: number; + x: number + y: number + z: number } -const MAX_HISTORY = 10; -let cameraHistory: CameraState[] = []; +const MAX_HISTORY = 10 +let cameraHistory: CameraState[] = [] + +// TODO: use this // Improved camera change tracking with debouncing const trackCameraChange = (editor: Editor) => { - const currentCamera = editor.getCamera(); - const lastPosition = cameraHistory[cameraHistory.length - 1]; + const currentCamera = editor.getCamera() + const lastPosition = cameraHistory[cameraHistory.length - 1] - // Store any viewport change that's not from a revert operation - if (!lastPosition || - currentCamera.x !== lastPosition.x || - currentCamera.y !== lastPosition.y || - currentCamera.z !== lastPosition.z) { - cameraHistory.push({ ...currentCamera }); - if (cameraHistory.length > MAX_HISTORY) { - cameraHistory.shift(); - } + // Store any viewport change that's not from a revert operation + if ( + !lastPosition || + currentCamera.x !== lastPosition.x || + currentCamera.y !== lastPosition.y || + currentCamera.z !== lastPosition.z + ) { + cameraHistory.push({ ...currentCamera }) + if (cameraHistory.length > MAX_HISTORY) { + cameraHistory.shift() } -}; + } +} export function useCameraControls(editor: Editor | null) { - const [searchParams] = useSearchParams(); + const [searchParams] = useSearchParams() - // Handle URL-based camera positioning - useEffect(() => { - if (!editor) return; + // Handle URL-based camera positioning + useEffect(() => { + if (!editor) return - const frameId = searchParams.get('frameId'); - const x = searchParams.get('x'); - const y = searchParams.get('y'); - const zoom = searchParams.get('zoom'); + const frameId = searchParams.get("frameId") + const x = searchParams.get("x") + const y = searchParams.get("y") + const zoom = searchParams.get("zoom") - if (x && y && zoom) { - editor.setCamera({ - x: parseFloat(x), - y: parseFloat(y), - z: parseFloat(zoom) - }); - return; - } + if (x && y && zoom) { + editor.setCamera({ + x: parseFloat(x), + y: parseFloat(y), + z: parseFloat(zoom), + }) + return + } - if (frameId) { - const frame = editor.getShape(frameId as TLParentId) as TLFrameShape; - if (!frame) { - console.warn('Frame not found:', frameId); - return; - } + if (frameId) { + const frame = editor.getShape(frameId as TLParentId) as TLFrameShape + if (!frame) { + console.warn("Frame not found:", frameId) + return + } - // Use editor's built-in zoomToBounds with animation - editor.zoomToBounds( - editor.getShapePageBounds(frame)!, - { - inset: 32, - animation: { duration: 500 } - } - ); - } - }, [editor, searchParams]); + // Use editor's built-in zoomToBounds with animation + editor.zoomToBounds(editor.getShapePageBounds(frame)!, { + inset: 32, + animation: { duration: 500 }, + }) + } + }, [editor, searchParams]) - // Track camera changes - useEffect(() => { - if (!editor) return; + // Track camera changes + useEffect(() => { + if (!editor) return - const handler = () => { - trackCameraChange(editor); - }; + const handler = () => { + trackCameraChange(editor) + } - // Track both viewport changes and user interaction end - editor.on('viewportChange' as keyof TLEventMap, handler); - editor.on('userChangeEnd' as keyof TLEventMap, handler); + // Track both viewport changes and user interaction end + editor.on("viewportChange" as keyof TLEventMap, handler) + editor.on("userChangeEnd" as keyof TLEventMap, handler) - return () => { - editor.off('viewportChange' as keyof TLEventMap, handler); - editor.off('userChangeEnd' as keyof TLEventMap, handler); - }; - }, [editor]); + return () => { + editor.off("viewportChange" as keyof TLEventMap, handler) + editor.off("userChangeEnd" as keyof TLEventMap, handler) + } + }, [editor]) - // Enhanced camera control functions - return { - zoomToFrame: (frameId: string) => { - if (!editor) return; - const frame = editor.getShape(frameId as TLParentId) as TLFrameShape; - if (!frame) return; + // Enhanced camera control functions + return { + zoomToFrame: (frameId: string) => { + if (!editor) return + const frame = editor.getShape(frameId as TLParentId) as TLFrameShape + if (!frame) return - editor.zoomToBounds( - editor.getShapePageBounds(frame)!, - { - inset: 32, - animation: { duration: 500 } - } - ); - }, + editor.zoomToBounds(editor.getShapePageBounds(frame)!, { + inset: 32, + animation: { duration: 500 }, + }) + }, - copyFrameLink: (frameId: string) => { - const url = new URL(window.location.href); - url.searchParams.set('frameId', frameId); - navigator.clipboard.writeText(url.toString()); - }, + copyFrameLink: (frameId: string) => { + const url = new URL(window.location.href) + url.searchParams.set("frameId", frameId) + navigator.clipboard.writeText(url.toString()) + }, - copyLocationLink: () => { - if (!editor) return; - const camera = editor.getCamera(); - const url = new URL(window.location.href); - url.searchParams.set('x', camera.x.toString()); - url.searchParams.set('y', camera.y.toString()); - url.searchParams.set('zoom', camera.z.toString()); - navigator.clipboard.writeText(url.toString()); - }, + copyLocationLink: () => { + if (!editor) return + const camera = editor.getCamera() + const url = new URL(window.location.href) + url.searchParams.set("x", camera.x.toString()) + url.searchParams.set("y", camera.y.toString()) + url.searchParams.set("zoom", camera.z.toString()) + navigator.clipboard.writeText(url.toString()) + }, - revertCamera: () => { - if (!editor || cameraHistory.length === 0) return; - const previousCamera = cameraHistory.pop(); - if (previousCamera) { - editor.setCamera(previousCamera, { animation: { duration: 200 } }); - } - } - }; -} \ No newline at end of file + revertCamera: () => { + if (!editor || cameraHistory.length === 0) return + const previousCamera = cameraHistory.pop() + if (previousCamera) { + editor.setCamera(previousCamera, { animation: { duration: 200 } }) + } + }, + } +} diff --git a/src/hooks/useCanvas.ts b/src/hooks/useCanvas.ts deleted file mode 100644 index 6807c3e..0000000 --- a/src/hooks/useCanvas.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { useState, useEffect } from 'react'; - -interface ElementInfo { - tagName: string; - x: number; - y: number; - w: number; - h: number; - html: string; -} - -export function useCanvas() { - const [isCanvasEnabled, setIsCanvasEnabled] = useState(false); - const [elementsInfo, setElementsInfo] = useState([]); - - useEffect(() => { - const toggleCanvas = async () => { - if (!isCanvasEnabled) { - const info = await gatherElementsInfo(); - setElementsInfo(info); - setIsCanvasEnabled(true); - document.body.classList.add('canvas-mode'); - } else { - setElementsInfo([]); - setIsCanvasEnabled(false); - document.body.classList.remove('canvas-mode'); - } - }; - - window.addEventListener('toggleCanvasEvent', toggleCanvas); - - return () => { - window.removeEventListener('toggleCanvasEvent', toggleCanvas); - }; - }, [isCanvasEnabled]); - - return { isCanvasEnabled, elementsInfo }; -} - -async function gatherElementsInfo() { - const rootElement = document.getElementsByTagName('main')[0]; - const info: any[] = []; - if (rootElement) { - for (const child of rootElement.children) { - if (['BUTTON'].includes(child.tagName)) continue; - const rect = child.getBoundingClientRect(); - let w = rect.width; - if (!['P', 'UL', 'OL'].includes(child.tagName)) { - w = measureElementTextWidth(child as HTMLElement); - } - // Check if the element is centered - const computedStyle = window.getComputedStyle(child); - let x = rect.left; // Default x position - if (computedStyle.display === 'block' && computedStyle.textAlign === 'center') { - // Adjust x position for centered elements - const parentWidth = child.parentElement ? child.parentElement.getBoundingClientRect().width : 0; - x = (parentWidth - w) / 2 + window.scrollX + (child.parentElement ? child.parentElement.getBoundingClientRect().left : 0); - } - - info.push({ - tagName: child.tagName, - x: x, - y: rect.top, - w: w, - h: rect.height, - html: child.outerHTML - }); - }; - } - return info; -} - -function measureElementTextWidth(element: HTMLElement) { - // Create a temporary span element - const tempElement = document.createElement('span'); - // Get the text content from the passed element - tempElement.textContent = element.textContent || element.innerText; - // Get the computed style of the passed element - const computedStyle = window.getComputedStyle(element); - // Apply relevant styles to the temporary element - tempElement.style.font = computedStyle.font; - tempElement.style.fontWeight = computedStyle.fontWeight; - tempElement.style.fontSize = computedStyle.fontSize; - tempElement.style.fontFamily = computedStyle.fontFamily; - tempElement.style.letterSpacing = computedStyle.letterSpacing; - // Ensure the temporary element is not visible in the viewport - tempElement.style.position = 'absolute'; - tempElement.style.visibility = 'hidden'; - tempElement.style.whiteSpace = 'nowrap'; // Prevent text from wrapping - // Append to the body to make measurements possible - document.body.appendChild(tempElement); - // Measure the width - const width = tempElement.getBoundingClientRect().width; - // Remove the temporary element from the document - document.body.removeChild(tempElement); - // Return the measured width - return width === 0 ? 10 : width; -} \ No newline at end of file diff --git a/src/public/artifact/causal-islands-integration-domain.pdf b/src/public/artifact/causal-islands-integration-domain.pdf deleted file mode 100644 index 8a97a6b..0000000 --- a/src/public/artifact/causal-islands-integration-domain.pdf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c8140e8b76c29fa3c828553e8a6a87651af6e7ee18d1ccb82e79125eb532194b -size 19738445 diff --git a/src/public/artifact/tft-rocks-integration-domain.pdf b/src/public/artifact/tft-rocks-integration-domain.pdf deleted file mode 100644 index 3447d8f..0000000 --- a/src/public/artifact/tft-rocks-integration-domain.pdf +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d63dd9f37a74b85680a7c1df823f69936619ecab5e8d4d06eeeedea4d908f1de -size 18268955 diff --git a/src/public/canvas-button.svg b/src/public/canvas-button.svg deleted file mode 100644 index 37fe119..0000000 --- a/src/public/canvas-button.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/public/favicon.ico b/src/public/favicon.ico deleted file mode 100644 index 7032ae5..0000000 Binary files a/src/public/favicon.ico and /dev/null differ diff --git a/src/public/gravity-button.svg b/src/public/gravity-button.svg deleted file mode 100644 index df7e2d1..0000000 --- a/src/public/gravity-button.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/src/public/website-embed.png b/src/public/website-embed.png deleted file mode 100644 index da6b9bd..0000000 --- a/src/public/website-embed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4b55a408375712bc169bc5c987d85c71c353028e611f216817f13ca0fb284604 -size 28423 diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 276a84c..bf6c8c1 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -1,91 +1,103 @@ -import { useSync } from '@tldraw/sync' -import { useMemo } from 'react' +import { useSync } from "@tldraw/sync" +import { useMemo } from "react" import { - AssetRecordType, - getHashForString, - TLBookmarkAsset, - Tldraw, - Editor, -} from 'tldraw' -import { useParams } from 'react-router-dom' -import { ChatBoxTool } from '@/tools/ChatBoxTool' -import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil' -import { VideoChatTool } from '@/tools/VideoChatTool' -import { VideoChatShape } from '@/shapes/VideoChatShapeUtil' -import { multiplayerAssetStore } from '../utils/multiplayerAssetStore' -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' + AssetRecordType, + getHashForString, + TLBookmarkAsset, + Tldraw, + Editor, +} from "tldraw" +import { useParams } from "react-router-dom" +import { ChatBoxTool } from "@/tools/ChatBoxTool" +import { ChatBoxShape } from "@/shapes/ChatBoxShapeUtil" +import { VideoChatTool } from "@/tools/VideoChatTool" +import { VideoChatShape } from "@/shapes/VideoChatShapeUtil" +import { multiplayerAssetStore } from "../utils/multiplayerAssetStore" +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" // Default to production URL if env var isn't available -export const WORKER_URL = 'https://jeffemmett-canvas.jeffemmett.workers.dev'; +export const WORKER_URL = "https://jeffemmett-canvas.jeffemmett.workers.dev" const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] -const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools - +const tools = [ChatBoxTool, VideoChatTool, EmbedTool] // Array of tools export function Board() { - const { slug } = useParams<{ slug: string }>(); - const roomId = slug || 'default-room'; + const { slug } = useParams<{ slug: string }>() + const roomId = slug || "default-room" - const storeConfig = useMemo(() => ({ - uri: `${WORKER_URL}/connect/${roomId}`, - assets: multiplayerAssetStore, - shapeUtils: [...shapeUtils, ...defaultShapeUtils], - bindingUtils: [...defaultBindingUtils], - }), [roomId]); + const storeConfig = useMemo( + () => ({ + uri: `${WORKER_URL}/connect/${roomId}`, + assets: multiplayerAssetStore, + shapeUtils: [...shapeUtils, ...defaultShapeUtils], + bindingUtils: [...defaultBindingUtils], + }), + [roomId], + ) - const store = useSync(storeConfig); - const [editor, setEditor] = useState(null) + const store = useSync(storeConfig) + const [editor, setEditor] = useState(null) - return ( -
- { - setEditor(editor) - editor.registerExternalAssetHandler('url', unfurlBookmarkUrl) - editor.setCurrentTool('hand') - }} - /> -
- ) + return ( +
+ { + setEditor(editor) + editor.registerExternalAssetHandler("url", unfurlBookmarkUrl) + editor.setCurrentTool("hand") + }} + /> +
+ ) } // 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: '', - }, - } +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 } + 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) - } + 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 + return asset } diff --git a/src/routes/Contact.tsx b/src/routes/Contact.tsx index b362662..f4da458 100644 --- a/src/routes/Contact.tsx +++ b/src/routes/Contact.tsx @@ -2,16 +2,28 @@ export function Contact() { return (
- - Jeff Emmett - + Jeff Emmett

Contact

-

Twitter: @jeffemmett

-

BlueSky: @jeffemnmett.bsky.social

-

Mastodon: @jeffemmett@social.coop

-

Email: jeffemmett@gmail.com

-

GitHub: Jeff-Emmett

+

+ Twitter: @jeffemmett +

+

+ BlueSky:{" "} + + @jeffemnmett.bsky.social + +

+

+ Mastodon:{" "} + @jeffemmett@social.coop +

+

+ Email: jeffemmett@gmail.com +

+

+ GitHub: Jeff-Emmett +

- ); + ) } diff --git a/src/routes/Default.tsx b/src/routes/Default.tsx index 229e69a..810fd0d 100644 --- a/src/routes/Default.tsx +++ b/src/routes/Default.tsx @@ -1,68 +1,106 @@ export function Default() { return (
-
- Jeff Emmett -
+
Jeff Emmett

Hello! 👋🍄

- My research investigates the intersection of mycelium and emancipatory technologies. - I am interested in the potential of new convivial tooling as a medium for group - consensus building and collective action, in order to empower communities of practice to address their own challenges. + My research investigates the intersection of mycelium and emancipatory + technologies. I am interested in the potential of new convivial tooling + as a medium for group consensus building and collective action, in order + to empower communities of practice to address their own challenges.

My current focus is basic research into the nature of digital organisation, developing prototype toolkits to improve shared - infrastructure, and applying this research to the design of new - systems and protocols which support the self-organisation of knowledge - and emergent response to local needs. + infrastructure, and applying this research to the design of new systems + and protocols which support the self-organisation of knowledge and + emergent response to local needs.

My work

- Alongside my independent work, I am a researcher and engineering communicator at Block Science, an advisor to the Active Inference Lab, Commons Stack, and the Trusted Seed. I am also an occasional collaborator with ECSA. + Alongside my independent work, I am a researcher and engineering + communicator at Block Science, an + advisor to the Active Inference Lab, Commons Stack, and the Trusted + Seed. I am also an occasional collaborator with{" "} + ECSA.

Get in touch

- I am on Twitter @jeffemmett, - Mastodon @jeffemmett@social.coop and GitHub @Jeff-Emmett. + I am on Twitter @jeffemmett + , Mastodon{" "} + @jeffemmett@social.coop{" "} + and GitHub @Jeff-Emmett.

***

Talks

    -
  1. MycoPunk Futures on Team Human with Douglas Rushkoff (slides) +
  2. + + MycoPunk Futures on Team Human with Douglas Rushkoff + {" "} + (slides)
  3. -
  4. Exploring MycoFi on the Greenpill Network with Kevin Owocki (slides) +
  5. + + Exploring MycoFi on the Greenpill Network with Kevin Owocki + {" "} + (slides)
  6. -
  7. Re-imagining Human Value on the Telos Podcast with Rieki & Brandonfrom SEEDS (slides) +
  8. + + Re-imagining Human Value on the Telos Podcast with Rieki & + Brandonfrom SEEDS + {" "} + (slides)
  9. -
  10. Move Slow & Fix Things: Design Patterns from Nature (slides) +
  11. + + Move Slow & Fix Things: Design Patterns from Nature + {" "} + (slides)
  12. -
  13. Localized Democracy and Public Goods with Token Engineering on the Ownership Economy (slides) +
  14. + + Localized Democracy and Public Goods with Token Engineering on the + Ownership Economy + {" "} + (slides) +
  15. +
  16. + + A Discussion on Warm Data with Nora Bateson on Systems Innovation +
  17. -
  18. A Discussion on Warm Data with Nora Bateson on Systems Innovation

Writing

    -
  1. Exploring MycoFi: Mycelial Design Patterns for Web3 & Beyond
  2. -
  3. Challenges & Approaches to Scaling the Global Commons
  4. -
  5. From Monoculture to Permaculture Currencies: A Glimpse of the Myco-Economic Future
  6. -
  7. Rewriting the Story of Human Collaboration
  8. +
  9. + + Exploring MycoFi: Mycelial Design Patterns for Web3 & Beyond + +
  10. +
  11. + + Challenges & Approaches to Scaling the Global Commons + +
  12. +
  13. + + From Monoculture to Permaculture Currencies: A Glimpse of the + Myco-Economic Future + +
  14. +
  15. + + Rewriting the Story of Human Collaboration + +
- ); + ) } diff --git a/src/routes/Inbox.tsx b/src/routes/Inbox.tsx index 75db423..43a9eed 100644 --- a/src/routes/Inbox.tsx +++ b/src/routes/Inbox.tsx @@ -1,45 +1,59 @@ -import { createShapeId, Editor, Tldraw, TLGeoShape, TLShapePartial } from "tldraw"; -import { useEffect, useRef } from "react"; +import { + createShapeId, + Editor, + Tldraw, + TLGeoShape, + TLShapePartial, +} from "tldraw" +import { useEffect, useRef } from "react" export function Inbox() { - const editorRef = useRef(null); + const editorRef = useRef(null) const updateEmails = async (editor: Editor) => { try { - const response = await fetch('https://jeffemmett-canvas.web.val.run', { - method: 'GET', - }); - const messages = await response.json() as { id: string, from: string, subject: string, text: string }[]; + const response = await fetch("https://jeffemmett-canvas.web.val.run", { + method: "GET", + }) + const messages = (await response.json()) as { + id: string + from: string + subject: string + text: string + }[] - for (let i = 0; i < messages.length; i++) { - const message = messages[i]; - const messageId = message.id; - const parsedEmailName = message.from.match(/^([^<]+)/)?.[1]?.trim() || message.from.match(/[^<@]+(?=@)/)?.[0] || message.from; + for (let i = 0; i < messages.length; i++) { + const message = messages[i] + const messageId = message.id + const parsedEmailName = + message.from.match(/^([^<]+)/)?.[1]?.trim() || + message.from.match(/[^<@]+(?=@)/)?.[0] || + message.from const messageText = `from: ${parsedEmailName}\nsubject: ${message.subject}\n\n${message.text}` const shapeWidth = 500 const shapeHeight = 300 const spacing = 50 const shape: TLShapePartial = { id: createShapeId(), - type: 'geo', + type: "geo", x: shapeWidth * (i % 5) + spacing * (i % 5), y: shapeHeight * Math.floor(i / 5) + spacing * Math.floor(i / 5), props: { w: shapeWidth, h: shapeHeight, text: messageText, - align:'start', - verticalAlign:'start' + align: "start", + verticalAlign: "start", }, meta: { - id: messageId - } + id: messageId, + }, } - let found = false; + let found = false for (const s of editor.getCurrentPageShapes()) { if (s.meta.id === messageId) { - found = true; - break; + found = true + break } } if (!found) { @@ -47,28 +61,28 @@ export function Inbox() { } } } catch (error) { - console.error('Error fetching data:', error); + console.error("Error fetching data:", error) } - }; + } useEffect(() => { const intervalId = setInterval(() => { if (editorRef.current) { - updateEmails(editorRef.current); + updateEmails(editorRef.current) } - }, 5*1000); + }, 5 * 1000) - return () => clearInterval(intervalId); - }, []); + return () => clearInterval(intervalId) + }, []) return (
{ - editorRef.current = editor; - updateEmails(editor); + editorRef.current = editor + updateEmails(editor) }} />
- ); -} \ No newline at end of file + ) +} diff --git a/src/shapes/ChatBoxShapeUtil.tsx b/src/shapes/ChatBoxShapeUtil.tsx index 8d11f90..8aabec5 100644 --- a/src/shapes/ChatBoxShapeUtil.tsx +++ b/src/shapes/ChatBoxShapeUtil.tsx @@ -1,155 +1,191 @@ -import { useEffect, useRef, useState } from "react"; -import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; +import { useEffect, useRef, useState } from "react" +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw" export type IChatBoxShape = TLBaseShape< - 'ChatBox', - { - w: number - h: number - roomId: string - userName: string - } + "ChatBox", + { + w: number + h: number + roomId: string + userName: string + } > export class ChatBoxShape extends BaseBoxShapeUtil { - static override type = 'ChatBox' + static override type = "ChatBox" - getDefaultProps(): IChatBoxShape['props'] { - return { - roomId: 'default-room', - w: 100, - h: 100, - userName: '', - } + getDefaultProps(): IChatBoxShape["props"] { + return { + roomId: "default-room", + w: 100, + h: 100, + userName: "", } + } - indicator(shape: IChatBoxShape) { - return - } + indicator(shape: IChatBoxShape) { + return + } - component(shape: IChatBoxShape) { - return ( - - ) - } + component(shape: IChatBoxShape) { + return ( + + ) + } } interface Message { - id: string; - username: string; - content: string; - timestamp: Date; + id: string + username: string + content: string + timestamp: Date } - - - // Update the ChatBox component to accept userName -export const ChatBox: React.FC = ({ roomId, w, h, userName }) => { - const [messages, setMessages] = useState([]); - const [inputMessage, setInputMessage] = useState(""); - const [username, setUsername] = useState(userName); - const messagesEndRef = useRef(null); +export const ChatBox: React.FC = ({ + roomId, + w, + h, + userName, +}) => { + const [messages, setMessages] = useState([]) + const [inputMessage, setInputMessage] = useState("") + const [username, setUsername] = useState(userName) + const messagesEndRef = useRef(null) - useEffect(() => { - const storedUsername = localStorage.getItem("chatUsername"); - if (storedUsername) { - setUsername(storedUsername); - } else { - const newUsername = `User${Math.floor(Math.random() * 1000)}`; - setUsername(newUsername); - localStorage.setItem("chatUsername", newUsername); - } - fetchMessages(roomId); - const interval = setInterval(() => fetchMessages(roomId), 2000); + useEffect(() => { + const storedUsername = localStorage.getItem("chatUsername") + if (storedUsername) { + setUsername(storedUsername) + } else { + const newUsername = `User${Math.floor(Math.random() * 1000)}` + setUsername(newUsername) + localStorage.setItem("chatUsername", newUsername) + } + fetchMessages(roomId) + const interval = setInterval(() => fetchMessages(roomId), 2000) - return () => clearInterval(interval); - }, [roomId]); + return () => clearInterval(interval) + }, [roomId]) - useEffect(() => { - if (messagesEndRef.current) { - (messagesEndRef.current as HTMLElement).scrollIntoView({ behavior: "smooth" }); - } - }, [messages]); + useEffect(() => { + if (messagesEndRef.current) { + ;(messagesEndRef.current as HTMLElement).scrollIntoView({ + behavior: "smooth", + }) + } + }, [messages]) - const fetchMessages = async (roomId: string) => { - try { - const response = await fetch(`https://jeffemmett-realtimechatappwithpolling.web.val.run?action=getMessages&roomId=${roomId}`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const newMessages = await response.json() as Message[]; - setMessages(newMessages.map(msg => ({ ...msg, timestamp: new Date(msg.timestamp) }))); - } catch (error) { - console.error('Error fetching messages:', error); - } - }; + const fetchMessages = async (roomId: string) => { + try { + const response = await fetch( + `https://jeffemmett-realtimechatappwithpolling.web.val.run?action=getMessages&roomId=${roomId}`, + ) + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + const newMessages = (await response.json()) as Message[] + setMessages( + newMessages.map((msg) => ({ + ...msg, + timestamp: new Date(msg.timestamp), + })), + ) + } catch (error) { + console.error("Error fetching messages:", error) + } + } - const sendMessage = async (e: React.FormEvent) => { - e.preventDefault(); - if (!inputMessage.trim()) return; - await sendMessageToChat(roomId, username, inputMessage); - setInputMessage(""); - fetchMessages(roomId); - }; + const sendMessage = async (e: React.FormEvent) => { + e.preventDefault() + if (!inputMessage.trim()) return + await sendMessageToChat(roomId, username, inputMessage) + setInputMessage("") + fetchMessages(roomId) + } - return ( -
-
- {messages.map((msg) => ( -
-
- {msg.username} - {new Date(msg.timestamp).toLocaleTimeString()} -
-
{msg.content}
-
- ))} -
+ return ( +
+
+ {messages.map((msg) => ( +
+
+ {msg.username} + + {new Date(msg.timestamp).toLocaleTimeString()} +
-
- setInputMessage(e.target.value)} - placeholder="Type a message..." - className="message-input" - style={{ touchAction: 'manipulation' }} - /> - -
-
- ); +
{msg.content}
+
+ ))} +
+
+
+ setInputMessage(e.target.value)} + placeholder="Type a message..." + className="message-input" + style={{ touchAction: "manipulation" }} + /> + +
+
+ ) } -async function sendMessageToChat(roomId: string, username: string, content: string): Promise { - const apiUrl = 'https://jeffemmett-realtimechatappwithpolling.web.val.run'; // Replace with your actual Val Town URL +async function sendMessageToChat( + roomId: string, + username: string, + content: string, +): Promise { + const apiUrl = "https://jeffemmett-realtimechatappwithpolling.web.val.run" // Replace with your actual Val Town URL - try { - const response = await fetch(`${apiUrl}?action=sendMessage`, { - method: 'POST', - mode: 'no-cors', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - roomId, - username, - content, - }), - }); + try { + const response = await fetch(`${apiUrl}?action=sendMessage`, { + method: "POST", + mode: "no-cors", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + roomId, + username, + content, + }), + }) - const result = await response.text(); - console.log('Message sent successfully:', result); - } catch (error) { - console.error('Error sending message:', error); - } -} \ No newline at end of file + const result = await response.text() + console.log("Message sent successfully:", result) + } catch (error) { + console.error("Error sending message:", error) + } +} diff --git a/src/shapes/ContainerShapeUtil.tsx b/src/shapes/ContainerShapeUtil.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/shapes/ElementShapeUtil.tsx b/src/shapes/ElementShapeUtil.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx index ae404d3..85efa63 100644 --- a/src/shapes/EmbedShapeUtil.tsx +++ b/src/shapes/EmbedShapeUtil.tsx @@ -1,138 +1,165 @@ -import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; -import { useCallback, useState } from "react"; +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw" +import { useCallback, useState } from "react" export type IEmbedShape = TLBaseShape< - 'Embed', - { - w: number; - h: number; - url: string | null; - } ->; + "Embed", + { + w: number + h: number + url: string | null + } +> export class EmbedShape extends BaseBoxShapeUtil { - static override type = 'Embed'; + static override type = "Embed" - getDefaultProps(): IEmbedShape['props'] { - return { - url: null, - w: 640, - h: 480, - }; + getDefaultProps(): IEmbedShape["props"] { + return { + url: null, + w: 640, + h: 480, } + } - indicator(shape: IEmbedShape) { - return ( - - - - ); - } + indicator(shape: IEmbedShape) { + return ( + + + + ) + } - component(shape: IEmbedShape) { - const [inputUrl, setInputUrl] = useState(shape.props.url || ''); - const [error, setError] = useState(''); + component(shape: IEmbedShape) { + const [inputUrl, setInputUrl] = useState(shape.props.url || "") + const [error, setError] = useState("") - const handleSubmit = useCallback((e: React.FormEvent) => { - e.preventDefault(); - let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`; + const handleSubmit = useCallback( + (e: React.FormEvent) => { + e.preventDefault() + let completedUrl = + inputUrl.startsWith("http://") || inputUrl.startsWith("https://") + ? inputUrl + : `https://${inputUrl}` - // Handle YouTube links - if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) { - const videoId = extractYouTubeVideoId(completedUrl); - if (videoId) { - completedUrl = `https://www.youtube.com/embed/${videoId}`; - } else { - setError('Invalid YouTube URL'); - return; - } - } - - // Handle Google Docs links - if (completedUrl.includes('docs.google.com')) { - const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1]; - if (docId) { - completedUrl = `https://docs.google.com/document/d/${docId}/preview`; - } else { - setError('Invalid Google Docs URL'); - return; - } - } - - this.editor.updateShape({ id: shape.id, type: 'Embed', props: { ...shape.props, url: completedUrl } }); - - // Check if the URL is valid - const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//); - if (!isValidUrl) { - setError('Invalid website URL'); - } else { - setError(''); - } - }, [inputUrl]); - - const extractYouTubeVideoId = (url: string): string | null => { - const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; - const match = url.match(regExp); - return (match && match[2].length === 11) ? match[2] : null; - }; - - const wrapperStyle = { - width: `${shape.props.w}px`, - height: `${shape.props.h}px`, - padding: '15px', - boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', - backgroundColor: '#F0F0F0', - borderRadius: '4px', - }; - - const contentStyle = { - pointerEvents: 'all' as const, - width: '100%', - height: '100%', - border: '1px solid #D3D3D3', - backgroundColor: '#FFFFFF', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - overflow: 'hidden', - }; - - if (!shape.props.url) { - return ( -
-
document.querySelector('input')?.focus()}> -
- setInputUrl(e.target.value)} - placeholder="Enter URL" - style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - handleSubmit(e); - } - }} - /> - {error &&
{error}
} -
-
-
- ); + // Handle YouTube links + if ( + completedUrl.includes("youtube.com") || + completedUrl.includes("youtu.be") + ) { + const videoId = extractYouTubeVideoId(completedUrl) + if (videoId) { + completedUrl = `https://www.youtube.com/embed/${videoId}` + } else { + setError("Invalid YouTube URL") + return + } } - return ( -
-
-