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