From f012632cde716eb20bb7491e191f402a6265d7b8 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:01:02 -0500 Subject: [PATCH] prettify and cleanup --- .prettierrc.json | 4 + build/markdownPlugin.js | 17 - build/markdownToHtml.js | 42 --- src/hooks/useCameraControls.ts | 204 ++++++------ src/hooks/useCanvas.ts | 98 ------ .../causal-islands-integration-domain.pdf | 3 - .../artifact/tft-rocks-integration-domain.pdf | 3 - src/public/canvas-button.svg | 5 - src/public/favicon.ico | Bin 85886 -> 0 bytes src/public/gravity-button.svg | 8 - src/public/website-embed.png | 3 - src/routes/Board.tsx | 160 +++++----- src/routes/Contact.tsx | 30 +- src/routes/Default.tsx | 104 +++++-- src/routes/Inbox.tsx | 72 +++-- src/shapes/ChatBoxShapeUtil.tsx | 294 ++++++++++-------- src/shapes/ContainerShapeUtil.tsx | 0 src/shapes/ElementShapeUtil.tsx | 0 src/shapes/EmbedShapeUtil.tsx | 275 ++++++++-------- src/shapes/HTMLShapeUtil.tsx | 51 +-- src/shapes/VideoChatShapeUtil.tsx | 213 ++++++------- src/tools/ChatBoxTool.ts | 10 +- src/tools/EmbedTool.ts | 12 +- src/tools/VideoChatTool.ts | 12 +- src/utils/card-shape-migrations.ts | 25 -- src/utils/card-shape-props.ts | 11 - src/utils/card-shape-types.ts | 11 - src/utils/getBookmarkPreview.tsx | 37 --- src/utils/my-interactive-shape-util.tsx | 120 ------- src/utils/readingTime.ts | 9 - vite.config.ts | 39 +-- 31 files changed, 807 insertions(+), 1065 deletions(-) create mode 100644 .prettierrc.json delete mode 100644 build/markdownPlugin.js delete mode 100644 build/markdownToHtml.js delete mode 100644 src/hooks/useCanvas.ts delete mode 100644 src/public/artifact/causal-islands-integration-domain.pdf delete mode 100644 src/public/artifact/tft-rocks-integration-domain.pdf delete mode 100644 src/public/canvas-button.svg delete mode 100644 src/public/favicon.ico delete mode 100644 src/public/gravity-button.svg delete mode 100644 src/public/website-embed.png delete mode 100644 src/shapes/ContainerShapeUtil.tsx delete mode 100644 src/shapes/ElementShapeUtil.tsx delete mode 100644 src/utils/card-shape-migrations.ts delete mode 100644 src/utils/card-shape-props.ts delete mode 100644 src/utils/card-shape-types.ts delete mode 100644 src/utils/getBookmarkPreview.tsx delete mode 100644 src/utils/my-interactive-shape-util.tsx delete mode 100644 src/utils/readingTime.ts 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 7032ae5821da0c6cfa24be01eed73aed0f4904ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85886 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 - - - - - - - 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 ( -
-
-