From e04efac1ccbb682f524c1729edb1fe379fea7e66 Mon Sep 17 00:00:00 2001 From: Jeff Emmett <46964190+Jeff-Emmett@users.noreply.github.com> Date: Sat, 23 Nov 2024 15:27:17 +0700 Subject: [PATCH] pretty bad and unworking code --- .env.production | 3 + index.html | 88 ++++++---- instructions.md | 13 ++ package.json | 15 +- src/App.tsx | 51 +++--- src/components/AuthCallback.tsx | 1 + src/components/Board.tsx | 29 ++- src/context/GoogleAuthContext.tsx | 50 ++++++ src/pages/auth/callback.tsx | 40 +++++ src/shapes/EmbedShapeUtil.tsx | 283 ++++++++++++++++++++---------- src/test/EmbedTest.tsx | 51 ++++++ src/test/EnvCheck.tsx | 8 + src/types/google.accounts.d.ts | 52 ++++++ src/utils/websocket-manager.ts | 53 ++++++ tsconfig.json | 33 ++-- vite.config.ts | 70 +++++--- worker/TldrawDurableObject.ts | 30 ++-- worker/worker.ts | 41 ++++- wrangler.toml | 6 +- 19 files changed, 702 insertions(+), 215 deletions(-) create mode 100644 .env.production create mode 100644 instructions.md create mode 100644 src/components/AuthCallback.tsx create mode 100644 src/context/GoogleAuthContext.tsx create mode 100644 src/pages/auth/callback.tsx create mode 100644 src/test/EmbedTest.tsx create mode 100644 src/test/EnvCheck.tsx create mode 100644 src/types/google.accounts.d.ts create mode 100644 src/utils/websocket-manager.ts diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..24beea0 --- /dev/null +++ b/.env.production @@ -0,0 +1,3 @@ +# Production environment variables +VITE_GOOGLE_CLIENT_ID=your_production_client_id_here +VITE_GOOGLE_API_KEY=your_production_api_key_here \ No newline at end of file diff --git a/index.html b/index.html index 063435a..16de180 100644 --- a/index.html +++ b/index.html @@ -1,42 +1,58 @@ - - - Jeff Emmett - - - - - - - + - - + + Jeff Emmett + + + + + + + - - - - - + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
+ + - - - - -
- - \ No newline at end of file diff --git a/instructions.md b/instructions.md new file mode 100644 index 0000000..5e70c90 --- /dev/null +++ b/instructions.md @@ -0,0 +1,13 @@ +https://docs.google.com/presentation/d/11tdbD5vw5y0IPhP3ygb8pXoVMaSzew-650Zr2Zvw2Yw/edit?pli=1#slide=id.p + + + +https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy + + + +https://github.com/vercel/next.js/discussions/51135 + diff --git a/package.json b/package.json index 70423e7..1686a92 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,15 @@ "dev:worker": "wrangler dev", "build": "tsc && vite build", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "watch": "tsc --noEmit --watch" }, "keywords": [], "author": "Jeff Emmett", "license": "ISC", "dependencies": { "@dimforge/rapier2d": "^0.11.2", + "@react-oauth/google": "^0.12.1", "@tldraw/sync": "^2.4.6", "@tldraw/sync-core": "^2.4.6", "@tldraw/tlschema": "^2.4.6", @@ -25,13 +27,19 @@ "cloudflare-workers-unfurl": "^0.0.7", "gray-matter": "^4.0.3", "itty-router": "^5.0.17", + "jwt-decode": "^4.0.0", "lodash.throttle": "^4.1.1", "markdown-it": "^14.1.0", "markdown-it-latex2img": "^0.0.6", + "prop-types": "^15.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-google-drive-picker": "^1.2.2", + "react-google-picker": "^0.1.0", "react-router-dom": "^6.22.3", - "tldraw": "^2.4.6" + "socket.io-client": "^4.8.1", + "tldraw": "^2.4.6", + "use-local-storage-state": "^19.5.0" }, "devDependencies": { "@biomejs/biome": "1.4.1", @@ -40,6 +48,7 @@ "@types/lodash.throttle": "^4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", + "@types/react-google-picker": "^0.1.4", "@typescript-eslint/eslint-plugin": "^5.59.0", "@typescript-eslint/parser": "^5.59.0", "@vitejs/plugin-react": "^4.0.3", @@ -55,4 +64,4 @@ "vite-plugin-wasm": "^3.2.2", "wrangler": "^3.88.0" } -} +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 832e943..d0f6f4c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,16 +1,16 @@ import { inject } from '@vercel/analytics'; import "tldraw/tldraw.css"; -import "@/css/style.css" +import "./css/style.css" import { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; -import { Default } from "@/components/Default"; -import { Canvas } from "@/components/Canvas"; -import { Toggle } from "@/components/Toggle"; -import { useCanvas } from "@/hooks/useCanvas" -import { createShapes } from "@/utils/utils"; +import { Default } from "./components/Default"; +import { Canvas } from "./components/Canvas"; +import { Toggle } from "./components/Toggle"; +import { useCanvas } from "./hooks/useCanvas" +import { createShapes } from "./utils/utils"; import { BrowserRouter, Route, Routes } from 'react-router-dom'; -import { Contact } from "@/components/Contact"; -import { Post } from '@/components/Post'; +import { Contact } from "./components/Contact"; +import { Post } from "./components/Post"; import { Board } from './components/Board'; import { Inbox } from './components/Inbox'; import { Books } from './components/Books'; @@ -26,6 +26,11 @@ import { ChatBoxShape } from './shapes/ChatBoxShapeUtil'; import { VideoChatShape } from './shapes/VideoChatShapeUtil'; import { ChatBoxTool } from './tools/ChatBoxTool'; import { VideoChatTool } from './tools/VideoChatTool'; +import { GoogleAuthProvider } from './context/GoogleAuthContext'; +import EnvCheck from './test/EnvCheck'; +import { EmbedTest } from './test/EmbedTest'; +//import { Callback } from './components/callback'; +import AuthCallback from './pages/auth/callback'; inject(); @@ -76,22 +81,24 @@ export default function InteractiveShapeExample() { ReactDOM.createRoot(document.getElementById("root")!).render(); function App() { - return ( - // - - - } /> - } /> - } /> - } /> - } /> - } /> - - - // + + + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); -}; +} function Home() { const { isCanvasEnabled, elementsInfo } = useCanvas(); diff --git a/src/components/AuthCallback.tsx b/src/components/AuthCallback.tsx new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/src/components/AuthCallback.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Board.tsx b/src/components/Board.tsx index 4fe8fba..1291a71 100644 --- a/src/components/Board.tsx +++ b/src/components/Board.tsx @@ -14,12 +14,15 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore' import { customSchema } from '../../worker/TldrawDurableObject' import { EmbedShape } from '@/shapes/EmbedShapeUtil' import { EmbedTool } from '@/tools/EmbedTool' +import { useGoogleAuth } from '@/context/GoogleAuthContext'; import React, { useState } from 'react'; import { ChatBox } from '@/shapes/ChatBoxShapeUtil'; import { components, uiOverrides } from '@/ui-overrides' -const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev` +const WORKER_URL = process.env.NODE_ENV === 'development' + ? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.hostname}:5172` + : 'wss://jeffemmett-canvas.jeffemmett.workers.dev' const shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape] const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools @@ -38,6 +41,7 @@ export function Board() { const [isChatBoxVisible, setChatBoxVisible] = useState(false); const [userName, setUserName] = useState(''); const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility + const { isAuthenticated, user } = useGoogleAuth(); const handleNameChange = (event: React.ChangeEvent) => { setUserName(event.target.value); @@ -45,6 +49,29 @@ export function Board() { return (
+ {isAuthenticated && user?.picture && ( +
+ User Profile +
+ )} void; + user: any; + setUser: (user: any) => void; + accessToken: string | null; + setAccessToken: (token: string | null) => void; + logout: () => void; +} + +const GoogleAuthContext = createContext(undefined); + +export function GoogleAuthProvider({ children }: { children: ReactNode }) { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(null); + const [accessToken, setAccessToken] = useState(null); + + const logout = () => { + setIsAuthenticated(false); + setUser(null); + setAccessToken(null); + }; + + return ( + + + {children} + + + ); +} + +export function useGoogleAuth() { + const context = useContext(GoogleAuthContext); + if (context === undefined) { + throw new Error('useGoogleAuth must be used within a GoogleAuthProvider'); + } + return context; +} \ No newline at end of file diff --git a/src/pages/auth/callback.tsx b/src/pages/auth/callback.tsx new file mode 100644 index 0000000..3966e73 --- /dev/null +++ b/src/pages/auth/callback.tsx @@ -0,0 +1,40 @@ +import { useEffect } from 'react'; + +export default function AuthCallback() { + useEffect(() => { + console.log('🎯 Callback page loaded'); + + const hashParams = new URLSearchParams(window.location.hash.replace('#', '')); + const queryParams = new URLSearchParams(window.location.search); + + console.log('📝 URL params:', { + hash: window.location.hash, + search: window.location.search + }); + + const state = hashParams.get('state') || queryParams.get('state'); + const accessToken = hashParams.get('access_token') || queryParams.get('access_token'); + const docId = state ? decodeURIComponent(state) : null; + + console.log('🔑 Extracted values:', { + state, + accessToken: accessToken ? 'present' : 'missing', + docId + }); + + if (window.opener && docId) { + console.log('📤 Sending message to opener'); + window.opener.postMessage({ + type: 'auth-complete', + docId, + accessToken + }, window.location.origin); + console.log('🚪 Closing callback window'); + window.close(); + } else { + console.warn('⚠️ Missing window.opener or docId'); + } + }, []); + + return
Processing authentication...
; +} \ No newline at end of file diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx index ae404d3..d21966e 100644 --- a/src/shapes/EmbedShapeUtil.tsx +++ b/src/shapes/EmbedShapeUtil.tsx @@ -1,5 +1,8 @@ +/// + import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"; -import { useCallback, useState } from "react"; +import { useState, useCallback } from "react"; +import { useGoogleAuth } from '@/context/GoogleAuthContext'; export type IEmbedShape = TLBaseShape< 'Embed', @@ -15,124 +18,214 @@ export class EmbedShape extends BaseBoxShapeUtil { getDefaultProps(): IEmbedShape['props'] { return { + w: 400, + h: 300, url: null, - w: 640, - h: 480, - }; + } } indicator(shape: IEmbedShape) { - return ( - - - - ); + return } component(shape: IEmbedShape) { - const [inputUrl, setInputUrl] = useState(shape.props.url || ''); + const [inputUrl, setInputUrl] = useState(''); const [error, setError] = useState(''); + const { isAuthenticated, setIsAuthenticated, accessToken, setAccessToken } = useGoogleAuth(); + + const handleGoogleAuth = useCallback((docId: string) => { + // Create message handler before opening window + const messageHandler = (event: MessageEvent) => { + if (event.origin !== window.location.origin) return; + if (event.data.type === 'auth-complete') { + console.log('Auth complete received', event.data); + setAccessToken(event.data.accessToken); + setIsAuthenticated(true); + const embedUrl = `https://docs.google.com/document/d/${docId}/preview?embedded=true&access_token=${event.data.accessToken}`; + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { ...shape.props, url: embedUrl } + }); + // Clean up + window.removeEventListener('message', messageHandler); + } + }; + + // Add message listener + window.addEventListener('message', messageHandler); + + // Open auth window with additional parameters + const authWindow = window.open( + `https://accounts.google.com/o/oauth2/v2/auth?` + + `client_id=${import.meta.env.VITE_GOOGLE_CLIENT_ID}` + + `&redirect_uri=${window.location.origin}/auth/callback` + + `&response_type=token` + + `&scope=https://www.googleapis.com/auth/drive.readonly` + + `&prompt=consent` + + `&access_type=online` + + `&state=${encodeURIComponent(docId)}`, + 'googleAuth', + 'width=500,height=600' + ); + + if (!authWindow) { + setError('Popup blocked. Please allow popups and try again.'); + return; + } + + // Simplified window check + const checkWindow = setInterval(() => { + try { + if (!authWindow || authWindow.closed) { + clearInterval(checkWindow); + window.removeEventListener('message', messageHandler); + } + } catch (e) { + // Ignore COOP errors + } + }, 500); + + // Cleanup after 5 minutes + setTimeout(() => { + clearInterval(checkWindow); + window.removeEventListener('message', messageHandler); + }, 300000); + + }, []); const handleSubmit = useCallback((e: React.FormEvent) => { e.preventDefault(); - let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`; + setError(''); - // 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}`; + try { + // Check if it's a Google Docs URL + if (inputUrl.includes('docs.google.com')) { + const docId = inputUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1]; + if (!docId) { + setError('Invalid Google Docs URL'); + return; + } + + if (!isAuthenticated) { + handleGoogleAuth(docId); + return; + } + + // If already authenticated, use preview URL + const embedUrl = `https://docs.google.com/document/d/${docId}/preview`; + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { ...shape.props, url: embedUrl } + }); } else { - setError('Invalid YouTube URL'); - return; + // For non-Google URLs + this.editor.updateShape({ + id: shape.id, + type: 'Embed', + props: { ...shape.props, url: inputUrl } + }); } + } catch (err) { + setError('Error processing URL'); + console.error(err); } - - // 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', - }; + }, [inputUrl, isAuthenticated]); 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}
} -
-
+
e.stopPropagation()} + > +
e.stopPropagation()} + > +
{ + e.stopPropagation(); + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e as any); + } + }} + onInput={(e) => { + setInputUrl(e.currentTarget.textContent || ''); + }} + onPaste={(e) => { + e.preventDefault(); + const text = e.clipboardData.getData('text/plain'); + document.execCommand('insertText', false, text); + }} + onPointerDown={(e) => { + e.stopPropagation(); + }} + onClick={(e) => { + e.stopPropagation(); + }} + /> + + + {error &&

{error}

}
); } return ( -
-
-