From ca8798a36c61d3875a6ab0c0f71b0cc41da103d1 Mon Sep 17 00:00:00 2001 From: Raunit Thakur Date: Wed, 15 Oct 2025 14:52:46 +0530 Subject: [PATCH] Expert Google Drive --- .env.sample | 4 +- src/App.jsx | 26 +++-- src/components/gdmodal.css | 191 +++++++++++++++++++++++++++++++ src/components/gdmodal.js | 164 +++++++++++++++++++++++++++ src/context/gdcontext.js | 227 +++++++++++++++++++++++++++++++++++++ src/pages/Editor.jsx | 61 ++++++++++ 6 files changed, 661 insertions(+), 12 deletions(-) create mode 100644 src/components/gdmodal.css create mode 100644 src/components/gdmodal.js create mode 100644 src/context/gdcontext.js diff --git a/.env.sample b/.env.sample index c87806f..4e46a0f 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,3 @@ -VITE_BACKEND_URL=http://backend.com \ No newline at end of file +VITE_BACKEND_URL=http://backend.com +REACT_APP_GOOGLE_CLIENT_ID=your_google_client_id_here +REACT_APP_GOOGLE_API_KEY=your_google_api_key_here \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index fcb0981..d7464db 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,3 +1,4 @@ +// App.js (updated) import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; import { useLayoutEffect } from "react"; import Editor from "./pages/Editor"; @@ -5,21 +6,24 @@ import BugReport from "./pages/BugReport"; import Templates from "./pages/Templates"; import LandingPage from "./pages/LandingPage"; import SettingsContextProvider from "./context/SettingsContext"; +import { GoogleDriveProvider } from "./context/GoogleDriveContext"; import NotFound from "./pages/NotFound"; export default function App() { return ( - - - - } /> - } /> - } /> - } /> - } /> - - + + + + + } /> + } /> + } /> + } /> + } /> + + + ); } @@ -30,4 +34,4 @@ function RestoreScroll() { window.scroll(0, 0); }, [location.pathname]); return null; -} +} \ No newline at end of file diff --git a/src/components/gdmodal.css b/src/components/gdmodal.css new file mode 100644 index 0000000..4f76bf2 --- /dev/null +++ b/src/components/gdmodal.css @@ -0,0 +1,191 @@ +/* components/GoogleDriveModal.css */ +.modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.modal-content { + background: white; + border-radius: 8px; + padding: 0; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #e1e5e9; +} + +.modal-header h2 { + margin: 0; + color: #333; +} + +.close-button { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; +} + +.close-button:hover { + color: #333; +} + +.auth-section { + padding: 40px 20px; + text-align: center; +} + +.sign-in-button { + background: #4285f4; + color: white; + border: none; + padding: 12px 24px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + margin-top: 16px; +} + +.sign-in-button:hover { + background: #3367d6; +} + +.tab-navigation { + display: flex; + border-bottom: 1px solid #e1e5e9; +} + +.tab-button { + flex: 1; + padding: 16px; + background: none; + border: none; + cursor: pointer; + font-size: 16px; + color: #666; +} + +.tab-button.active { + color: #4285f4; + border-bottom: 2px solid #4285f4; +} + +.error-message { + background: #ffeaea; + color: #d32f2f; + padding: 12px 20px; + margin: 20px; + border-radius: 4px; + border-left: 4px solid #d32f2f; +} + +.file-list-section, .save-section { + padding: 20px; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.refresh-button { + background: #f8f9fa; + border: 1px solid #dadce0; + padding: 8px 16px; + border-radius: 4px; + cursor: pointer; +} + +.refresh-button:hover { + background: #e8f0fe; +} + +.loading, .empty-state { + text-align: center; + padding: 40px; + color: #666; +} + +.file-list { + max-height: 400px; + overflow-y: auto; +} + +.file-item { + padding: 16px; + border: 1px solid #e1e5e9; + border-radius: 4px; + margin-bottom: 8px; + cursor: pointer; +} + +.file-item:hover { + background: #f8f9fa; + border-color: #4285f4; +} + +.file-name { + font-weight: 500; + color: #333; +} + +.file-modified { + font-size: 14px; + color: #666; + margin-top: 4px; +} + +.input-group { + margin-bottom: 20px; +} + +.input-group label { + display: block; + margin-bottom: 8px; + color: #333; + font-weight: 500; +} + +.input-group input { + width: 100%; + padding: 12px; + border: 1px solid #dadce0; + border-radius: 4px; + font-size: 16px; + box-sizing: border-box; +} + +.save-button { + background: #34a853; + color: white; + border: none; + padding: 12px 24px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + width: 100%; +} + +.save-button:hover { + background: #2e8b47; +} \ No newline at end of file diff --git a/src/components/gdmodal.js b/src/components/gdmodal.js new file mode 100644 index 0000000..124e16f --- /dev/null +++ b/src/components/gdmodal.js @@ -0,0 +1,164 @@ +// components/GoogleDriveModal.js +import React, { useState, useEffect } from 'react'; +import { useGoogleDrive } from '../context/GoogleDriveContext'; +import './GoogleDriveModal.css'; + +const GoogleDriveModal = ({ isOpen, onClose, onFileSelect, onSave }) => { + const [files, setFiles] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + const [activeTab, setActiveTab] = useState('open'); // 'open' or 'save' + const [fileName, setFileName] = useState(''); + + const { + isAuthenticated, + signIn, + listFiles, + getFileContent, + saveToDrive + } = useGoogleDrive(); + + useEffect(() => { + if (isOpen && isAuthenticated) { + loadFiles(); + } + }, [isOpen, isAuthenticated]); + + const loadFiles = async () => { + setLoading(true); + setError(''); + try { + const fileList = await listFiles(); + setFiles(fileList); + } catch (err) { + setError('Failed to load files from Google Drive'); + console.error(err); + } finally { + setLoading(false); + } + }; + + const handleSignIn = async () => { + try { + await signIn(); + } catch (err) { + setError('Failed to sign in to Google Drive'); + } + }; + + const handleFileSelect = async (file) => { + try { + const content = await getFileContent(file.id); + onFileSelect(content, file.name); + onClose(); + } catch (err) { + setError('Failed to load file content'); + } + }; + + const handleSave = async () => { + if (!fileName.trim()) { + setError('Please enter a file name'); + return; + } + + try { + await onSave(fileName); + onClose(); + } catch (err) { + setError('Failed to save file to Google Drive'); + } + }; + + if (!isOpen) return null; + + return ( +
+
+
+

Google Drive

+ +
+ + {!isAuthenticated ? ( +
+

Connect to Google Drive to save and open your diagrams

+ +
+ ) : ( + <> +
+ + +
+ + {error &&
{error}
} + + {activeTab === 'open' && ( +
+
+

Your Diagrams

+ +
+ {loading ? ( +
Loading files...
+ ) : files.length === 0 ? ( +
No diagram files found in your Google Drive
+ ) : ( +
+ {files.map((file) => ( +
handleFileSelect(file)} + > +
{file.name}
+
+ Modified: {new Date(file.modifiedTime).toLocaleDateString()} +
+
+ ))} +
+ )} +
+ )} + + {activeTab === 'save' && ( +
+
+ + setFileName(e.target.value)} + placeholder="Enter file name (e.g., my-diagram.json)" + /> +
+ +
+ )} + + )} +
+
+ ); +}; + +export default GoogleDriveModal; \ No newline at end of file diff --git a/src/context/gdcontext.js b/src/context/gdcontext.js new file mode 100644 index 0000000..2857939 --- /dev/null +++ b/src/context/gdcontext.js @@ -0,0 +1,227 @@ +// context/GoogleDriveContext.js +import React, { createContext, useContext, useState, useCallback } from 'react'; +import { google } from 'googleapis'; + +const GoogleDriveContext = createContext(); + +export const useGoogleDrive = () => { + const context = useContext(GoogleDriveContext); + if (!context) { + throw new Error('useGoogleDrive must be used within a GoogleDriveProvider'); + } + return context; +}; + +export const GoogleDriveProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [gapiClient, setGapiClient] = useState(null); + const [tokenClient, setTokenClient] = useState(null); + + // Initialize Google API client + const initializeGapi = useCallback(async () => { + try { + // Load the Google API client library + await new Promise((resolve) => { + if (window.gapi) { + resolve(); + return; + } + const script = document.createElement('script'); + script.src = 'https://apis.google.com/js/api.js'; + script.onload = resolve; + document.head.appendChild(script); + }); + + await window.gapi.load('client', async () => { + await window.gapi.client.init({ + apiKey: process.env.REACT_APP_GOOGLE_API_KEY, + discoveryDocs: ['https://www.googleapis.com/discovery/v1/apis/drive/v3/rest'], + }); + setGapiClient(window.gapi); + }); + } catch (error) { + console.error('Error initializing Google API:', error); + } + }, []); + + // Initialize Google Identity Services + const initializeGIS = useCallback(() => { + return new Promise((resolve) => { + if (window.google) { + resolve(); + return; + } + const script = document.createElement('script'); + script.src = 'https://accounts.google.com/gsi/client'; + script.onload = resolve; + document.head.appendChild(script); + }); + }, []); + + // Sign in to Google + const signIn = useCallback(async () => { + try { + await initializeGapi(); + await initializeGIS(); + + const client = window.google.accounts.oauth2.initTokenClient({ + client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID, + scope: 'https://www.googleapis.com/auth/drive.file', + callback: (response) => { + if (response.access_token) { + window.gapi.client.setToken({ + access_token: response.access_token, + }); + setIsAuthenticated(true); + } + }, + }); + + client.requestAccessToken(); + } catch (error) { + console.error('Error signing in:', error); + throw error; + } + }, [initializeGapi, initializeGIS]); + + // Sign out from Google + const signOut = useCallback(() => { + if (window.gapi?.client?.getToken()) { + const token = window.gapi.client.getToken(); + window.gapi.client.setToken(null); + setIsAuthenticated(false); + + if (token) { + window.google.accounts.oauth2.revoke(token.access_token); + } + } + }, []); + + // Save file to Google Drive + const saveToDrive = useCallback(async (fileName, content, mimeType = 'application/json') => { + if (!gapiClient || !isAuthenticated) { + throw new Error('Not authenticated with Google Drive'); + } + + try { + const file = new Blob([content], { type: mimeType }); + const metadata = { + name: fileName, + mimeType: mimeType, + }; + + const form = new FormData(); + form.append('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' })); + form.append('file', file); + + const response = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', { + method: 'POST', + headers: { + Authorization: `Bearer ${gapiClient.client.getToken().access_token}`, + }, + body: form, + }); + + if (!response.ok) { + throw new Error(`Failed to save file: ${response.statusText}`); + } + + const result = await response.json(); + return result; + } catch (error) { + console.error('Error saving to Google Drive:', error); + throw error; + } + }, [gapiClient, isAuthenticated]); + + // Update existing file in Google Drive + const updateFile = useCallback(async (fileId, content, mimeType = 'application/json') => { + if (!gapiClient || !isAuthenticated) { + throw new Error('Not authenticated with Google Drive'); + } + + try { + const file = new Blob([content], { type: mimeType }); + + const response = await fetch(`https://www.googleapis.com/upload/drive/v3/files/${fileId}?uploadType=multipart`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${gapiClient.client.getToken().access_token}`, + }, + body: file, + }); + + if (!response.ok) { + throw new Error(`Failed to update file: ${response.statusText}`); + } + + const result = await response.json(); + return result; + } catch (error) { + console.error('Error updating file in Google Drive:', error); + throw error; + } + }, [gapiClient, isAuthenticated]); + + // List files from Google Drive + const listFiles = useCallback(async () => { + if (!gapiClient || !isAuthenticated) { + throw new Error('Not authenticated with Google Drive'); + } + + try { + const response = await gapiClient.client.drive.files.list({ + pageSize: 100, + fields: 'files(id, name, mimeType, modifiedTime, createdTime)', + q: "mimeType='application/json' or mimeType='application/xml'", + orderBy: 'modifiedTime desc', + }); + + return response.result.files; + } catch (error) { + console.error('Error listing files:', error); + throw error; + } + }, [gapiClient, isAuthenticated]); + + // Get file content from Google Drive + const getFileContent = useCallback(async (fileId) => { + if (!gapiClient || !isAuthenticated) { + throw new Error('Not authenticated with Google Drive'); + } + + try { + const response = await fetch(`https://www.googleapis.com/drive/v3/files/${fileId}?alt=media`, { + headers: { + Authorization: `Bearer ${gapiClient.client.getToken().access_token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to get file content: ${response.statusText}`); + } + + const content = await response.text(); + return content; + } catch (error) { + console.error('Error getting file content:', error); + throw error; + } + }, [gapiClient, isAuthenticated]); + + const value = { + isAuthenticated, + signIn, + signOut, + saveToDrive, + updateFile, + listFiles, + getFileContent, + }; + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/pages/Editor.jsx b/src/pages/Editor.jsx index 24b5afe..e2348e6 100644 --- a/src/pages/Editor.jsx +++ b/src/pages/Editor.jsx @@ -41,3 +41,64 @@ export default function Editor() { ); } + +// pages/Editor.js +import React, { useState, useCallback } from 'react'; +import { useGoogleDrive } from '../context/GoogleDriveContext'; +import GoogleDriveModal from '../components/GoogleDriveModal'; + +const Editor = () => { + const [isDriveModalOpen, setIsDriveModalOpen] = useState(false); + const { saveToDrive } = useGoogleDrive(); + + // Your existing editor state and functions... + const [diagramData, setDiagramData] = useState(''); // Your actual diagram data + + const handleSaveToDrive = useCallback(async (fileName) => { + // Convert your diagram data to the appropriate format + const content = JSON.stringify({ + diagramData: diagramData, + version: '1.0', + timestamp: new Date().toISOString(), + }); + + await saveToDrive(fileName, content); + // Show success message or handle completion + }, [diagramData, saveToDrive]); + + const handleOpenFromDrive = useCallback((content, fileName) => { + // Parse the content and load it into your editor + try { + const data = JSON.parse(content); + setDiagramData(data.diagramData); + // Show success message + } catch (error) { + console.error('Error parsing file content:', error); + } + }, []); + + return ( +
+ {/* Your existing editor UI */} + + {/* Add Google Drive buttons to your toolbar */} +
+ + + {/* Your other toolbar buttons */} +
+ + setIsDriveModalOpen(false)} + onFileSelect={handleOpenFromDrive} + onSave={handleSaveToDrive} + /> +
+ ); +}; \ No newline at end of file