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>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en">
|
||||||
<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">
|
|
||||||
|
|
||||||
<!-- Social Meta Tags -->
|
<head>
|
||||||
<meta name="description"
|
<title>Jeff Emmett</title>
|
||||||
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 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">
|
<!-- Social Meta Tags -->
|
||||||
<meta property="og:type" content="website">
|
<meta name="description"
|
||||||
<meta property="og:title" content="Jeff Emmett">
|
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 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="og:url" content="https://jeffemmett.com">
|
||||||
<meta property="twitter:domain" content="jeffemmett.com">
|
<meta property="og:type" content="website">
|
||||||
<meta property="twitter:url" content="https://jeffemmett.com">
|
<meta property="og:title" content="Jeff Emmett">
|
||||||
<meta name="twitter:title" content="Jeff Emmett">
|
<meta property="og:description"
|
||||||
<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.">
|
||||||
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: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>
|
</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
|
||||||
|
|
||||||
13
package.json
13
package.json
|
|
@ -9,13 +9,15 @@
|
||||||
"dev:worker": "wrangler dev",
|
"dev:worker": "wrangler dev",
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"watch": "tsc --noEmit --watch"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Jeff Emmett",
|
"author": "Jeff Emmett",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier2d": "^0.11.2",
|
"@dimforge/rapier2d": "^0.11.2",
|
||||||
|
"@react-oauth/google": "^0.12.1",
|
||||||
"@tldraw/sync": "^2.4.6",
|
"@tldraw/sync": "^2.4.6",
|
||||||
"@tldraw/sync-core": "^2.4.6",
|
"@tldraw/sync-core": "^2.4.6",
|
||||||
"@tldraw/tlschema": "^2.4.6",
|
"@tldraw/tlschema": "^2.4.6",
|
||||||
|
|
@ -25,13 +27,19 @@
|
||||||
"cloudflare-workers-unfurl": "^0.0.7",
|
"cloudflare-workers-unfurl": "^0.0.7",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"itty-router": "^5.0.17",
|
"itty-router": "^5.0.17",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"markdown-it-latex2img": "^0.0.6",
|
"markdown-it-latex2img": "^0.0.6",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^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",
|
"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": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.4.1",
|
"@biomejs/biome": "1.4.1",
|
||||||
|
|
@ -40,6 +48,7 @@
|
||||||
"@types/lodash.throttle": "^4",
|
"@types/lodash.throttle": "^4",
|
||||||
"@types/react": "^18.2.15",
|
"@types/react": "^18.2.15",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
|
"@types/react-google-picker": "^0.1.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
"@typescript-eslint/eslint-plugin": "^5.59.0",
|
||||||
"@typescript-eslint/parser": "^5.59.0",
|
"@typescript-eslint/parser": "^5.59.0",
|
||||||
"@vitejs/plugin-react": "^4.0.3",
|
"@vitejs/plugin-react": "^4.0.3",
|
||||||
|
|
|
||||||
51
src/App.tsx
51
src/App.tsx
|
|
@ -1,16 +1,16 @@
|
||||||
import { inject } from '@vercel/analytics';
|
import { inject } from '@vercel/analytics';
|
||||||
import "tldraw/tldraw.css";
|
import "tldraw/tldraw.css";
|
||||||
import "@/css/style.css"
|
import "./css/style.css"
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { Default } from "@/components/Default";
|
import { Default } from "./components/Default";
|
||||||
import { Canvas } from "@/components/Canvas";
|
import { Canvas } from "./components/Canvas";
|
||||||
import { Toggle } from "@/components/Toggle";
|
import { Toggle } from "./components/Toggle";
|
||||||
import { useCanvas } from "@/hooks/useCanvas"
|
import { useCanvas } from "./hooks/useCanvas"
|
||||||
import { createShapes } from "@/utils/utils";
|
import { createShapes } from "./utils/utils";
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
import { Contact } from "@/components/Contact";
|
import { Contact } from "./components/Contact";
|
||||||
import { Post } from '@/components/Post';
|
import { Post } from "./components/Post";
|
||||||
import { Board } from './components/Board';
|
import { Board } from './components/Board';
|
||||||
import { Inbox } from './components/Inbox';
|
import { Inbox } from './components/Inbox';
|
||||||
import { Books } from './components/Books';
|
import { Books } from './components/Books';
|
||||||
|
|
@ -26,6 +26,11 @@ import { ChatBoxShape } from './shapes/ChatBoxShapeUtil';
|
||||||
import { VideoChatShape } from './shapes/VideoChatShapeUtil';
|
import { VideoChatShape } from './shapes/VideoChatShapeUtil';
|
||||||
import { ChatBoxTool } from './tools/ChatBoxTool';
|
import { ChatBoxTool } from './tools/ChatBoxTool';
|
||||||
import { VideoChatTool } from './tools/VideoChatTool';
|
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();
|
inject();
|
||||||
|
|
||||||
|
|
@ -76,22 +81,24 @@ export default function InteractiveShapeExample() {
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <React.StrictMode>
|
<GoogleAuthProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/card/contact" element={<Contact />} />
|
<Route path="/card/contact" element={<Contact />} />
|
||||||
<Route path="/posts/:slug" element={<Post />} />
|
<Route path="/posts/:slug" element={<Post />} />
|
||||||
<Route path="/board/:slug" element={<Board />} />
|
<Route path="/board/:slug" element={<Board />} />
|
||||||
<Route path="/inbox" element={<Inbox />} />
|
<Route path="/inbox" element={<Inbox />} />
|
||||||
<Route path="/books" element={<Books />} />
|
<Route path="/books" element={<Books />} />
|
||||||
</Routes>
|
<Route path="/test" element={<EnvCheck />} />
|
||||||
</BrowserRouter>
|
<Route path="/embed-test" element={<EmbedTest />} />
|
||||||
// </React.StrictMode>
|
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
</GoogleAuthProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
const { isCanvasEnabled, elementsInfo } = useCanvas();
|
const { isCanvasEnabled, elementsInfo } = useCanvas();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -14,12 +14,15 @@ import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
|
||||||
import { customSchema } from '../../worker/TldrawDurableObject'
|
import { customSchema } from '../../worker/TldrawDurableObject'
|
||||||
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
||||||
import { EmbedTool } from '@/tools/EmbedTool'
|
import { EmbedTool } from '@/tools/EmbedTool'
|
||||||
|
import { useGoogleAuth } from '@/context/GoogleAuthContext';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ChatBox } from '@/shapes/ChatBoxShapeUtil';
|
import { ChatBox } from '@/shapes/ChatBoxShapeUtil';
|
||||||
import { components, uiOverrides } from '@/ui-overrides'
|
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 shapeUtils = [ChatBoxShape, VideoChatShape, EmbedShape]
|
||||||
const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools
|
const tools = [ChatBoxTool, VideoChatTool, EmbedTool]; // Array of tools
|
||||||
|
|
@ -38,6 +41,7 @@ export function Board() {
|
||||||
const [isChatBoxVisible, setChatBoxVisible] = useState(false);
|
const [isChatBoxVisible, setChatBoxVisible] = useState(false);
|
||||||
const [userName, setUserName] = useState('');
|
const [userName, setUserName] = useState('');
|
||||||
const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility
|
const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility
|
||||||
|
const { isAuthenticated, user } = useGoogleAuth();
|
||||||
|
|
||||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setUserName(event.target.value);
|
setUserName(event.target.value);
|
||||||
|
|
@ -45,6 +49,29 @@ export function Board() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<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
|
<Tldraw
|
||||||
store={store}
|
store={store}
|
||||||
shapeUtils={shapeUtils}
|
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 { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
|
||||||
import { useCallback, useState } from "react";
|
import { useState, useCallback } from "react";
|
||||||
|
import { useGoogleAuth } from '@/context/GoogleAuthContext';
|
||||||
|
|
||||||
export type IEmbedShape = TLBaseShape<
|
export type IEmbedShape = TLBaseShape<
|
||||||
'Embed',
|
'Embed',
|
||||||
|
|
@ -15,124 +18,214 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
|
|
||||||
getDefaultProps(): IEmbedShape['props'] {
|
getDefaultProps(): IEmbedShape['props'] {
|
||||||
return {
|
return {
|
||||||
|
w: 400,
|
||||||
|
h: 300,
|
||||||
url: null,
|
url: null,
|
||||||
w: 640,
|
}
|
||||||
h: 480,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
indicator(shape: IEmbedShape) {
|
indicator(shape: IEmbedShape) {
|
||||||
return (
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
<g>
|
|
||||||
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: IEmbedShape) {
|
component(shape: IEmbedShape) {
|
||||||
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
|
const [inputUrl, setInputUrl] = useState('');
|
||||||
const [error, setError] = 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) => {
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`;
|
setError('');
|
||||||
|
|
||||||
// Handle YouTube links
|
try {
|
||||||
if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) {
|
// Check if it's a Google Docs URL
|
||||||
const videoId = extractYouTubeVideoId(completedUrl);
|
if (inputUrl.includes('docs.google.com')) {
|
||||||
if (videoId) {
|
const docId = inputUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
|
||||||
completedUrl = `https://www.youtube.com/embed/${videoId}`;
|
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 {
|
} else {
|
||||||
setError('Invalid YouTube URL');
|
// For non-Google URLs
|
||||||
return;
|
this.editor.updateShape<IEmbedShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'Embed',
|
||||||
|
props: { ...shape.props, url: inputUrl }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setError('Error processing URL');
|
||||||
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
}, [inputUrl, isAuthenticated]);
|
||||||
// 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',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!shape.props.url) {
|
if (!shape.props.url) {
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div
|
||||||
<div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
|
style={{
|
||||||
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
|
width: '100%',
|
||||||
<input
|
height: '100%',
|
||||||
type="text"
|
display: 'flex',
|
||||||
value={inputUrl}
|
flexDirection: 'column',
|
||||||
onChange={(e) => setInputUrl(e.target.value)}
|
alignItems: 'center',
|
||||||
placeholder="Enter URL"
|
justifyContent: 'center',
|
||||||
style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }}
|
padding: '20px',
|
||||||
onKeyDown={(e) => {
|
pointerEvents: 'all'
|
||||||
if (e.key === 'Enter') {
|
}}
|
||||||
handleSubmit(e);
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
}
|
>
|
||||||
}}
|
<form
|
||||||
/>
|
onSubmit={handleSubmit}
|
||||||
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
|
style={{ width: '100%' }}
|
||||||
</form>
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
</div>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={wrapperStyle}>
|
<div style={{
|
||||||
<div style={contentStyle}>
|
width: `${shape.props.w}px`,
|
||||||
<iframe
|
height: `${shape.props.h}px`,
|
||||||
src={shape.props.url}
|
overflow: 'hidden',
|
||||||
width="100%"
|
}}>
|
||||||
height="100%"
|
<iframe
|
||||||
style={{ border: 'none' }}
|
src={shape.props.url}
|
||||||
allowFullScreen
|
width="100%"
|
||||||
/>
|
height="100%"
|
||||||
</div>
|
style={{ border: 'none' }}
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
</div>
|
</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": {
|
"compilerOptions": {
|
||||||
"baseUrl": "./src",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["*"],
|
"@/*": [
|
||||||
"src/*": ["./src/*"],
|
"./src/*"
|
||||||
|
],
|
||||||
|
"src/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
|
@ -18,14 +25,20 @@
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true
|
"noFallthroughCasesInSwitch": true
|
||||||
},
|
},
|
||||||
"include": ["src", "worker", "src/client"],
|
"include": [
|
||||||
|
"src",
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"worker",
|
||||||
|
"src/client"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1,37 +1,49 @@
|
||||||
import { markdownPlugin } from './build/markdownPlugin';
|
import { markdownPlugin } from './build/markdownPlugin';
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig, loadEnv } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import wasm from "vite-plugin-wasm";
|
import wasm from "vite-plugin-wasm";
|
||||||
import topLevelAwait from "vite-plugin-top-level-await";
|
import topLevelAwait from "vite-plugin-top-level-await";
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, process.cwd(), '');
|
||||||
|
|
||||||
export default defineConfig({
|
return {
|
||||||
define: {
|
define: {
|
||||||
'process.env.TLDRAW_WORKER_URL': JSON.stringify('https://jeffemmett-canvas.jeffemmett.workers.dev')
|
'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),
|
||||||
plugins: [
|
'process.env.VITE_GOOGLE_API_KEY': JSON.stringify(env.VITE_GOOGLE_API_KEY)
|
||||||
react(),
|
|
||||||
wasm(),
|
|
||||||
topLevelAwait(),
|
|
||||||
markdownPlugin,
|
|
||||||
viteStaticCopy({
|
|
||||||
targets: [
|
|
||||||
{
|
|
||||||
src: 'src/posts/',
|
|
||||||
dest: '.'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
],
|
|
||||||
build: {
|
|
||||||
sourcemap: true,
|
|
||||||
},
|
|
||||||
base: '/',
|
|
||||||
publicDir: 'src/public',
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
'@': '/src',
|
|
||||||
},
|
},
|
||||||
},
|
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 { AutoRouter, IRequest, error } from 'itty-router'
|
||||||
import throttle from 'lodash.throttle'
|
import throttle from 'lodash.throttle'
|
||||||
import { Environment } from './types'
|
import { Environment } from './types'
|
||||||
import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
|
import { ChatBoxShape } from '../src/shapes/ChatBoxShapeUtil'
|
||||||
import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
|
import { VideoChatShape } from '../src/shapes/VideoChatShapeUtil'
|
||||||
import { EmbedShape } from '@/shapes/EmbedShapeUtil'
|
import { EmbedShape } from '../src/shapes/EmbedShapeUtil'
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// add custom shapes and bindings here if needed:
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
|
|
@ -74,23 +74,25 @@ export class TldrawDurableObject {
|
||||||
|
|
||||||
// what happens when someone tries to connect to this room?
|
// what happens when someone tries to connect to this room?
|
||||||
async handleConnect(request: IRequest): Promise<Response> {
|
async handleConnect(request: IRequest): Promise<Response> {
|
||||||
// extract query params from request
|
|
||||||
const sessionId = request.query.sessionId as string
|
const sessionId = request.query.sessionId as string
|
||||||
if (!sessionId) return error(400, 'Missing sessionId')
|
if (!sessionId) return error(400, 'Missing sessionId')
|
||||||
|
|
||||||
// Create the websocket pair for the client
|
const webSocketPair = new WebSocketPair()
|
||||||
const { 0: clientWebSocket, 1: serverWebSocket } = new WebSocketPair()
|
const [client, server] = Object.values(webSocketPair)
|
||||||
// @ts-ignore
|
|
||||||
serverWebSocket.accept()
|
server.accept()
|
||||||
|
|
||||||
// load the room, or retrieve it if it's already loaded
|
|
||||||
const room = await this.getRoom()
|
const room = await this.getRoom()
|
||||||
|
room.handleSocketConnect({ sessionId, socket: server })
|
||||||
|
|
||||||
// connect the client to the room
|
return new Response(null, {
|
||||||
room.handleSocketConnect({ sessionId, socket: serverWebSocket })
|
status: 101,
|
||||||
|
webSocket: client,
|
||||||
// return the websocket connection to the client
|
headers: {
|
||||||
return new Response(null, { status: 101, webSocket: clientWebSocket })
|
'Upgrade': 'websocket',
|
||||||
|
'Connection': 'Upgrade'
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getRoom() {
|
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 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.
|
// 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]>({
|
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
before: [preflight],
|
before: [preflight],
|
||||||
finally: [corsify],
|
finally: [(response) => corsify(addSecurityHeaders(response))],
|
||||||
catch: (e) => {
|
catch: (e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return error(e)
|
return error(e)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
// 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 id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
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:
|
// assets can be uploaded to the bucket under /uploads:
|
||||||
|
|
|
||||||
|
|
@ -32,3 +32,7 @@ logpush = true
|
||||||
[observability]
|
[observability]
|
||||||
enabled = true
|
enabled = true
|
||||||
head_sampling_rate = 1
|
head_sampling_rate = 1
|
||||||
|
|
||||||
|
[build]
|
||||||
|
command = "yarn build"
|
||||||
|
watch_dir = "src"
|
||||||
Loading…
Reference in New Issue