pretty bad and unworking code
This commit is contained in:
parent
202971f343
commit
e04efac1cc
|
|
@ -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
|
||||
88
index.html
88
index.html
|
|
@ -1,42 +1,58 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Jeff Emmett</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=4" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap"
|
||||
rel="stylesheet">
|
||||
<html lang="en">
|
||||
|
||||
<!-- Social Meta Tags -->
|
||||
<meta name="description"
|
||||
content="My research investigates the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
<head>
|
||||
<title>Jeff Emmett</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico?v=4" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico?v=4" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Recursive:slnt,wght,CASL,CRSV,MONO@-15..0,300..1000,0..1,0..1,0..1&display=swap"
|
||||
rel="stylesheet">
|
||||
|
||||
<meta property="og:url" content="https://jeffemmett.com">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Jeff Emmett">
|
||||
<meta property="og:description"
|
||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
<meta property="og:image" content="/website-embed.png">
|
||||
<!-- Social Meta Tags -->
|
||||
<meta name="description"
|
||||
content="My research investigates the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="jeffemmett.com">
|
||||
<meta property="twitter:url" content="https://jeffemmett.com">
|
||||
<meta name="twitter:title" content="Jeff Emmett">
|
||||
<meta name="twitter:description"
|
||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
<meta name="twitter:image" content="/website-embed.png">
|
||||
<meta property="og:url" content="https://jeffemmett.com">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Jeff Emmett">
|
||||
<meta property="og:description"
|
||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
<meta property="og:image" content="/website-embed.png">
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:domain" content="jeffemmett.com">
|
||||
<meta property="twitter:url" content="https://jeffemmett.com">
|
||||
<meta name="twitter:title" content="Jeff Emmett">
|
||||
<meta name="twitter:description"
|
||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||
<meta name="twitter:image" content="/website-embed.png">
|
||||
|
||||
<!-- Analytics -->
|
||||
<script data-goatcounter="https://jeff.goatcounter.com/count" async src="//gc.zgo.at/count.js"></script>
|
||||
|
||||
<script src="https://accounts.google.com/gsi/client" async defer></script>
|
||||
|
||||
<!-- Add CSP meta tag here, after other meta tags -->
|
||||
<meta http-equiv="Content-Security-Policy" content="
|
||||
default-src 'self';
|
||||
script-src 'self' 'unsafe-inline' 'unsafe-eval' https://accounts.google.com https://gc.zgo.at https://va.vercel-scripts.com https://apis.google.com https://www.google.com;
|
||||
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://accounts.google.com https://www.google.com;
|
||||
frame-src 'self' https://docs.google.com https://accounts.google.com https://*.google.com https://drive.google.com;
|
||||
connect-src 'self' http://localhost:5172 ws://localhost:5172 wss://localhost:5172 https://www.googleapis.com https://*.vercel-scripts.com wss://jeffemmett-canvas.jeffemmett.workers.dev https://jeffemmett-canvas.jeffemmett.workers.dev https://cdn.tldraw.com https://oauth2.googleapis.com https://www.google.com;
|
||||
font-src 'self' data: https://fonts.gstatic.com https://cdn.tldraw.com;
|
||||
img-src 'self' data: https: blob:;
|
||||
worker-src 'self' blob:;
|
||||
">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/App.tsx"></script>
|
||||
</body>
|
||||
|
||||
<!-- Analytics -->
|
||||
<script data-goatcounter="https://jeff.goatcounter.com/count"
|
||||
async src="//gc.zgo.at/count.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/App.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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
|
||||
|
||||
<meta Content-Security-Policy-Report-Only: script-src
|
||||
https://accounts.google.com/gsi/client; frame-src
|
||||
https://accounts.google.com/gsi/; connect-src
|
||||
https://accounts.google.com/gsi/; />
|
||||
|
||||
https://github.com/vercel/next.js/discussions/51135
|
||||
|
||||
15
package.json
15
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
51
src/App.tsx
51
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(<App />);
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
// <React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/card/contact" element={<Contact />} />
|
||||
<Route path="/posts/:slug" element={<Post />} />
|
||||
<Route path="/board/:slug" element={<Board />} />
|
||||
<Route path="/inbox" element={<Inbox />} />
|
||||
<Route path="/books" element={<Books />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
// </React.StrictMode>
|
||||
<GoogleAuthProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/" element={<Home />} />
|
||||
<Route path="/card/contact" element={<Contact />} />
|
||||
<Route path="/posts/:slug" element={<Post />} />
|
||||
<Route path="/board/:slug" element={<Board />} />
|
||||
<Route path="/inbox" element={<Inbox />} />
|
||||
<Route path="/books" element={<Books />} />
|
||||
<Route path="/test" element={<EnvCheck />} />
|
||||
<Route path="/embed-test" element={<EmbedTest />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</GoogleAuthProvider>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function Home() {
|
||||
const { isCanvasEnabled, elementsInfo } = useCanvas();
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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<HTMLInputElement>) => {
|
||||
setUserName(event.target.value);
|
||||
|
|
@ -45,6 +49,29 @@ export function Board() {
|
|||
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
{isAuthenticated && user?.picture && (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
top: '10px',
|
||||
right: '10px',
|
||||
zIndex: 10000,
|
||||
borderRadius: '50%',
|
||||
overflow: 'hidden',
|
||||
width: '32px',
|
||||
height: '32px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
|
||||
}}>
|
||||
<img
|
||||
src={user.picture}
|
||||
alt="User Profile"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Tldraw
|
||||
store={store}
|
||||
shapeUtils={shapeUtils}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
import { createContext, useContext, ReactNode, useState } from 'react';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
|
||||
interface GoogleAuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
setIsAuthenticated: (value: boolean) => void;
|
||||
user: any;
|
||||
setUser: (user: any) => void;
|
||||
accessToken: string | null;
|
||||
setAccessToken: (token: string | null) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const GoogleAuthContext = createContext<GoogleAuthContextType | undefined>(undefined);
|
||||
|
||||
export function GoogleAuthProvider({ children }: { children: ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [user, setUser] = useState(null);
|
||||
const [accessToken, setAccessToken] = useState<string | null>(null);
|
||||
|
||||
const logout = () => {
|
||||
setIsAuthenticated(false);
|
||||
setUser(null);
|
||||
setAccessToken(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID}>
|
||||
<GoogleAuthContext.Provider value={{
|
||||
isAuthenticated,
|
||||
setIsAuthenticated,
|
||||
user,
|
||||
setUser,
|
||||
accessToken,
|
||||
setAccessToken,
|
||||
logout,
|
||||
}}>
|
||||
{children}
|
||||
</GoogleAuthContext.Provider>
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useGoogleAuth() {
|
||||
const context = useContext(GoogleAuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useGoogleAuth must be used within a GoogleAuthProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
|
@ -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 <div>Processing authentication...</div>;
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
/// <reference path="../types/google.accounts.d.ts" />
|
||||
|
||||
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<IEmbedShape> {
|
|||
|
||||
getDefaultProps(): IEmbedShape['props'] {
|
||||
return {
|
||||
w: 400,
|
||||
h: 300,
|
||||
url: null,
|
||||
w: 640,
|
||||
h: 480,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
indicator(shape: IEmbedShape) {
|
||||
return (
|
||||
<g>
|
||||
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
|
||||
</g>
|
||||
);
|
||||
return <rect width={shape.props.w} height={shape.props.h} />
|
||||
}
|
||||
|
||||
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<IEmbedShape>({
|
||||
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<IEmbedShape>({
|
||||
id: shape.id,
|
||||
type: 'Embed',
|
||||
props: { ...shape.props, url: embedUrl }
|
||||
});
|
||||
} else {
|
||||
setError('Invalid YouTube URL');
|
||||
return;
|
||||
// For non-Google URLs
|
||||
this.editor.updateShape<IEmbedShape>({
|
||||
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<IEmbedShape>({ 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 (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
|
||||
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={inputUrl}
|
||||
onChange={(e) => 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 && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
|
||||
</form>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '20px',
|
||||
pointerEvents: 'all'
|
||||
}}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{ width: '100%' }}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div
|
||||
contentEditable
|
||||
suppressContentEditableWarning
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
marginBottom: '10px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px',
|
||||
minHeight: '36px',
|
||||
cursor: 'text',
|
||||
background: 'white'
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
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();
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
pointerEvents: 'all',
|
||||
touchAction: 'manipulation',
|
||||
padding: '8px 16px',
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Embed Content
|
||||
</button>
|
||||
</form>
|
||||
{error && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={wrapperStyle}>
|
||||
<div style={contentStyle}>
|
||||
<iframe
|
||||
src={shape.props.url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 'none' }}
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
width: `${shape.props.w}px`,
|
||||
height: `${shape.props.h}px`,
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<iframe
|
||||
src={shape.props.url}
|
||||
width="100%"
|
||||
height="100%"
|
||||
style={{ border: 'none' }}
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ... rest of your utility methods ...
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
export const EmbedTest = () => {
|
||||
const [docUrl, setDocUrl] = useState('');
|
||||
const [embedUrl, setEmbedUrl] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleEmbed = () => {
|
||||
const docId = docUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
|
||||
if (!docId) {
|
||||
setError('Invalid Google Docs URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const newEmbedUrl = `https://docs.google.com/document/d/${docId}/preview`;
|
||||
setEmbedUrl(newEmbedUrl);
|
||||
setError('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '20px' }}>
|
||||
<h2>Embed Test</h2>
|
||||
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={docUrl}
|
||||
onChange={(e) => setDocUrl(e.target.value)}
|
||||
placeholder="Paste Google Doc URL"
|
||||
style={{ width: '100%', padding: '8px' }}
|
||||
/>
|
||||
<button onClick={handleEmbed} style={{ marginTop: '10px' }}>
|
||||
Embed Document
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
|
||||
{embedUrl && (
|
||||
<div>
|
||||
<h3>Embedded Document:</h3>
|
||||
<iframe
|
||||
src={embedUrl}
|
||||
style={{ width: '100%', height: '500px', border: '1px solid #ccc' }}
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function EnvCheck() {
|
||||
console.log('Client ID:', process.env.VITE_GOOGLE_CLIENT_ID);
|
||||
console.log('API Key:', process.env.VITE_GOOGLE_API_KEY);
|
||||
console.log(process.env)
|
||||
return <div>Check console for environment variables</div>;
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
declare namespace google {
|
||||
namespace accounts {
|
||||
namespace oauth2 {
|
||||
interface TokenClient {
|
||||
callback: (response: { access_token?: string }) => void;
|
||||
client_id: string;
|
||||
scope: string;
|
||||
}
|
||||
|
||||
function initTokenClient(config: {
|
||||
client_id: string;
|
||||
scope: string;
|
||||
callback: (response: { access_token?: string }) => void;
|
||||
error_callback?: (error: any) => void;
|
||||
}): {
|
||||
requestAccessToken(options?: { prompt?: string }): void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace picker {
|
||||
class PickerBuilder {
|
||||
addView(view: any): PickerBuilder;
|
||||
setOAuthToken(token: string): PickerBuilder;
|
||||
setDeveloperKey(key: string): PickerBuilder;
|
||||
setCallback(callback: (data: PickerResponse) => void): PickerBuilder;
|
||||
build(): Picker;
|
||||
}
|
||||
|
||||
interface PickerResponse {
|
||||
action: string;
|
||||
docs: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
mimeType: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
class Picker {
|
||||
setVisible(visible: boolean): void;
|
||||
}
|
||||
|
||||
const ViewId: {
|
||||
DOCS: string;
|
||||
DOCUMENTS: string;
|
||||
PRESENTATIONS: string;
|
||||
SPREADSHEETS: string;
|
||||
FOLDERS: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { io, Socket } from 'socket.io-client';
|
||||
|
||||
export const wsConfig = {
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 5,
|
||||
reconnectionDelay: 1000,
|
||||
timeout: 10000
|
||||
};
|
||||
|
||||
class WebSocketManager {
|
||||
private socket: Socket | null = null;
|
||||
private retryCount = 0;
|
||||
|
||||
connect(url: string) {
|
||||
this.socket = io(url, wsConfig);
|
||||
|
||||
this.socket.on('connect', () => {
|
||||
console.log('WebSocket connected');
|
||||
this.retryCount = 0;
|
||||
});
|
||||
|
||||
this.socket.on('error', (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
this.handleRetry();
|
||||
});
|
||||
|
||||
this.socket.on('disconnect', () => {
|
||||
console.log('WebSocket disconnected');
|
||||
this.handleRetry();
|
||||
});
|
||||
}
|
||||
|
||||
private handleRetry() {
|
||||
if (this.retryCount < wsConfig.reconnectionAttempts) {
|
||||
this.retryCount++;
|
||||
setTimeout(() => {
|
||||
console.log(`Attempting reconnection ${this.retryCount}/${wsConfig.reconnectionAttempts}`);
|
||||
this.socket?.connect();
|
||||
}, wsConfig.reconnectionDelay);
|
||||
}
|
||||
}
|
||||
|
||||
// Add methods to send/receive messages
|
||||
send(event: string, data: any) {
|
||||
this.socket?.emit(event, data);
|
||||
}
|
||||
|
||||
subscribe(event: string, callback: (data: any) => void) {
|
||||
this.socket?.on(event, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export const webSocketManager = new WebSocketManager();
|
||||
|
|
@ -1,16 +1,23 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./src",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["*"],
|
||||
"src/*": ["./src/*"],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"src/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
|
|
@ -18,14 +25,20 @@
|
|||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "worker", "src/client"],
|
||||
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
"include": [
|
||||
"src",
|
||||
"worker",
|
||||
"src/client"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,37 +1,49 @@
|
|||
import { markdownPlugin } from './build/markdownPlugin';
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import wasm from "vite-plugin-wasm";
|
||||
import topLevelAwait from "vite-plugin-top-level-await";
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd(), '');
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
'process.env.TLDRAW_WORKER_URL': JSON.stringify('https://jeffemmett-canvas.jeffemmett.workers.dev')
|
||||
},
|
||||
plugins: [
|
||||
react(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
markdownPlugin,
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: 'src/posts/',
|
||||
dest: '.'
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
base: '/',
|
||||
publicDir: 'src/public',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src',
|
||||
return {
|
||||
define: {
|
||||
'process.env.TLDRAW_WORKER_URL': JSON.stringify('https://jeffemmett-canvas.jeffemmett.workers.dev'),
|
||||
'process.env.VITE_GOOGLE_CLIENT_ID': JSON.stringify(env.VITE_GOOGLE_CLIENT_ID),
|
||||
'process.env.VITE_GOOGLE_API_KEY': JSON.stringify(env.VITE_GOOGLE_API_KEY)
|
||||
},
|
||||
},
|
||||
})
|
||||
plugins: [
|
||||
react(),
|
||||
wasm(),
|
||||
topLevelAwait(),
|
||||
markdownPlugin,
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{
|
||||
src: 'src/posts/',
|
||||
dest: '.'
|
||||
}
|
||||
]
|
||||
})
|
||||
],
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
base: '/',
|
||||
publicDir: 'src/public',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
},
|
||||
},
|
||||
server: {
|
||||
headers: {
|
||||
'Cross-Origin-Opener-Policy': 'same-origin-allow-popups',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import {
|
|||
import { AutoRouter, IRequest, error } from 'itty-router'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { Environment } from './types'
|
||||
import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
|
||||
import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
|
||||
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
||||
import { ChatBoxShape } from '../src/shapes/ChatBoxShapeUtil'
|
||||
import { VideoChatShape } from '../src/shapes/VideoChatShapeUtil'
|
||||
import { EmbedShape } from '../src/shapes/EmbedShapeUtil'
|
||||
|
||||
// add custom shapes and bindings here if needed:
|
||||
export const customSchema = createTLSchema({
|
||||
|
|
@ -74,23 +74,25 @@ export class TldrawDurableObject {
|
|||
|
||||
// what happens when someone tries to connect to this room?
|
||||
async handleConnect(request: IRequest): Promise<Response> {
|
||||
// extract query params from request
|
||||
const sessionId = request.query.sessionId as string
|
||||
if (!sessionId) return error(400, 'Missing sessionId')
|
||||
|
||||
// Create the websocket pair for the client
|
||||
const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair()
|
||||
// @ts-ignore
|
||||
serverWebSocket.accept()
|
||||
const webSocketPair = new WebSocketPair()
|
||||
const [client, server] = Object.values(webSocketPair)
|
||||
|
||||
server.accept()
|
||||
|
||||
// load the room, or retrieve it if it's already loaded
|
||||
const room = await this.getRoom()
|
||||
room.handleSocketConnect({ sessionId, socket: server })
|
||||
|
||||
// connect the client to the room
|
||||
room.handleSocketConnect({ sessionId, socket: serverWebSocket })
|
||||
|
||||
// return the websocket connection to the client
|
||||
return new Response(null, { status: 101, webSocket: clientWebSocket })
|
||||
return new Response(null, {
|
||||
status: 101,
|
||||
webSocket: client,
|
||||
headers: {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getRoom() {
|
||||
|
|
|
|||
|
|
@ -8,20 +8,53 @@ export { TldrawDurableObject } from './TldrawDurableObject'
|
|||
|
||||
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
||||
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
||||
const { preflight, corsify } = cors({ origin: '*' })
|
||||
const { preflight, corsify } = cors({
|
||||
origin: '*',
|
||||
allowMethods: 'GET, POST, PUT, DELETE, OPTIONS',
|
||||
allowHeaders: {
|
||||
'Access-Control-Allow-Headers': '*',
|
||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
'Sec-WebSocket-Key': '*',
|
||||
'Sec-WebSocket-Version': '*',
|
||||
'Sec-WebSocket-Protocol': '*'
|
||||
}
|
||||
})
|
||||
|
||||
const addSecurityHeaders = (response: Response): Response => {
|
||||
const headers = new Headers(response.headers)
|
||||
headers.set('Cross-Origin-Opener-Policy', 'same-origin')
|
||||
headers.set('Cross-Origin-Embedder-Policy', 'require-corp')
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
headers
|
||||
})
|
||||
}
|
||||
|
||||
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||
before: [preflight],
|
||||
finally: [corsify],
|
||||
finally: [(response) => corsify(addSecurityHeaders(response))],
|
||||
catch: (e) => {
|
||||
console.error(e)
|
||||
return error(e)
|
||||
},
|
||||
})
|
||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||
.get('/connect/:roomId', (request, env) => {
|
||||
.get('/connect/:roomId', async (request, env) => {
|
||||
const upgradeHeader = request.headers.get('Upgrade')
|
||||
if (!upgradeHeader || upgradeHeader.toLowerCase() !== 'websocket') {
|
||||
return new Response('Expected Upgrade: websocket', { status: 426 })
|
||||
}
|
||||
|
||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||
return room.fetch(request.url, { headers: request.headers, body: request.body })
|
||||
return room.fetch(request.url, {
|
||||
headers: request.headers,
|
||||
body: request.body,
|
||||
method: request.method
|
||||
})
|
||||
})
|
||||
|
||||
// assets can be uploaded to the bucket under /uploads:
|
||||
|
|
|
|||
|
|
@ -31,4 +31,8 @@ logpush = true
|
|||
# wrangler.toml (wrangler v3.79.0^)
|
||||
[observability]
|
||||
enabled = true
|
||||
head_sampling_rate = 1
|
||||
head_sampling_rate = 1
|
||||
|
||||
[build]
|
||||
command = "yarn build"
|
||||
watch_dir = "src"
|
||||
Loading…
Reference in New Issue