diff --git a/.env.sample b/.env.sample index c87806f..7662c93 100644 --- a/.env.sample +++ b/.env.sample @@ -1 +1,7 @@ -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 +# .env +VITE_API_BASE_URL=http://localhost:3001/api +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..b803208 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,25 +1,37 @@ -import { BrowserRouter, Routes, Route, useLocation } from "react-router-dom"; -import { useLayoutEffect } from "react"; -import Editor from "./pages/Editor"; -import BugReport from "./pages/BugReport"; -import Templates from "./pages/Templates"; -import LandingPage from "./pages/LandingPage"; -import SettingsContextProvider from "./context/SettingsContext"; -import NotFound from "./pages/NotFound"; +// App.jsx (Final Updated Version) +import React, { useEffect } from 'react'; +import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom'; +import { useLayoutEffect } from 'react'; +import Editor from './pages/Editor'; +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 { SyncProvider } from './context/synccontext'; +import SyncToolbar from './components/synctoolbar'; +import NotFound from './pages/NotFound'; export default function App() { return ( - - - - } /> - } /> - } /> - } /> - } /> - - + + + + +
+ + + } /> + } /> + } /> + } /> + } /> + +
+
+
+
); } @@ -30,4 +42,4 @@ function RestoreScroll() { window.scroll(0, 0); }, [location.pathname]); return null; -} +} \ No newline at end of file diff --git a/src/components/authmodal.jsx b/src/components/authmodal.jsx new file mode 100644 index 0000000..9866eca --- /dev/null +++ b/src/components/authmodal.jsx @@ -0,0 +1,137 @@ +// components/AuthModal.jsx +import React, { useState } from 'react'; +import { useSync } from '../context/synccontext'; +import './AuthModal.css'; + +const AuthModal = ({ isOpen, onClose, onSuccess }) => { + const [activeTab, setActiveTab] = useState('login'); + const [formData, setFormData] = useState({ + email: '', + password: '', + username: '', + }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(''); + + const { register, login } = useSync(); + + const handleInputChange = (e) => { + setFormData({ + ...formData, + [e.target.name]: e.target.value, + }); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + setError(''); + + try { + if (activeTab === 'register') { + await register(formData.email, formData.password, formData.username); + } else { + await login(formData.email, formData.password); + } + + onSuccess?.(); + onClose(); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + const handleTabChange = (tab) => { + setActiveTab(tab); + setError(''); + setFormData({ email: '', password: '', username: '' }); + }; + + if (!isOpen) return null; + + return ( +
+
+
+

Sync Your Work

+ +
+ +
+ + +
+ +
+ {error &&
{error}
} + + {activeTab === 'register' && ( +
+ + +
+ )} + +
+ + +
+ +
+ + +
+ + +
+ +
+

With Sync You Can:

+
    +
  • ✨ Save projects to the cloud
  • +
  • 🔄 Continue work on any device
  • +
  • 🔒 Automatic backups
  • +
  • 📱 Access your diagrams anywhere
  • +
+
+
+
+ ); +}; + +export default AuthModal; \ No newline at end of file diff --git a/src/components/datatypeselector.jsx b/src/components/datatypeselector.jsx new file mode 100644 index 0000000..048feed --- /dev/null +++ b/src/components/datatypeselector.jsx @@ -0,0 +1,160 @@ +// components/DataTypeSelector.jsx +import React, { useState } from 'react'; +import { + BASE_DATA_TYPES, + DATA_TYPE_CATEGORIES, + ENUM_TYPE, + CUSTOM_TYPE_TEMPLATES, + getDataTypeDefinition +} from '../utils/dataTypes'; +import './DataTypeSelector.css'; + +const DataTypeSelector = ({ value, onChange, onEnumCreate }) => { + const [showCustomTypes, setShowCustomTypes] = useState(false); + const [customTypeName, setCustomTypeName] = useState(''); + const [enumValues, setEnumValues] = useState(['']); + const [showEnumCreator, setShowEnumCreator] = useState(false); + + const handleDataTypeChange = (dataType) => { + if (dataType === ENUM_TYPE) { + setShowEnumCreator(true); + } else { + onChange({ + type: dataType, + definition: getDataTypeDefinition(dataType), + }); + } + }; + + const handleEnumCreate = () => { + const validValues = enumValues.filter(val => val.trim() !== ''); + if (validValues.length > 0) { + onEnumCreate?.(validValues); + onChange({ + type: ENUM_TYPE, + definition: `ENUM('${validValues.join("','")}')`, + enumValues: validValues, + }); + setShowEnumCreator(false); + setEnumValues(['']); + } + }; + + const addEnumValue = () => { + setEnumValues([...enumValues, '']); + }; + + const updateEnumValue = (index, value) => { + const newValues = [...enumValues]; + newValues[index] = value; + setEnumValues(newValues); + }; + + const removeEnumValue = (index) => { + if (enumValues.length > 1) { + setEnumValues(enumValues.filter((_, i) => i !== index)); + } + }; + + const addCustomType = () => { + if (customTypeName.trim()) { + // Here you would save the custom type to user preferences or context + console.log('Adding custom type:', customTypeName); + setCustomTypeName(''); + setShowCustomTypes(false); + } + }; + + return ( +
+
+ {Object.entries(DATA_TYPE_CATEGORIES).map(([categoryKey, category]) => ( +
+

{category.label}

+
+ {category.types.map((dataType) => ( + + ))} +
+
+ ))} +
+ + {/* Enum Creator Modal */} + {showEnumCreator && ( +
+
+

Create ENUM Type

+

Define the possible values for your ENUM type:

+ +
+ {enumValues.map((value, index) => ( +
+ updateEnumValue(index, e.target.value)} + placeholder={`Value ${index + 1}`} + /> + {enumValues.length > 1 && ( + + )} +
+ ))} +
+ + + +
+ + +
+
+
+ )} + + {/* Custom Type Creator */} + {showCustomTypes && ( +
+
+

Add Custom Data Type

+ setCustomTypeName(e.target.value)} + placeholder="Enter custom type name (e.g., status_type)" + /> +
+ + +
+
+
+ )} +
+ ); +}; + +export default DataTypeSelector; \ 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/components/partitionconfigure.jsx b/src/components/partitionconfigure.jsx new file mode 100644 index 0000000..8629ffd --- /dev/null +++ b/src/components/partitionconfigure.jsx @@ -0,0 +1,244 @@ +// components/PartitionConfigurator.jsx +import React, { useState, useEffect } from 'react'; +import { + PARTITION_METHODS, + PARTITION_STRATEGIES, + createPartitionConfig, + generateDefaultPartitions, + validatePartitionConfig, +} from '../utils/partitioning'; +import './PartitionConfigurator.css'; + +const PartitionConfigurator = ({ tableColumns, onConfigChange }) => { + const [isPartitioned, setIsPartitioned] = useState(false); + const [partitionMethod, setPartitionMethod] = useState(PARTITION_METHODS.RANGE); + const [partitionColumns, setPartitionColumns] = useState([]); + const [partitions, setPartitions] = useState([]); + const [errors, setErrors] = useState([]); + + useEffect(() => { + if (isPartitioned) { + const config = createPartitionConfig(partitionMethod, partitionColumns); + const validationErrors = validatePartitionConfig(config, partitions); + setErrors(validationErrors); + + onConfigChange({ + enabled: isPartitioned, + config: validationErrors.length === 0 ? config : null, + partitions: validationErrors.length === 0 ? partitions : [], + errors: validationErrors, + }); + } else { + onConfigChange({ enabled: false, config: null, partitions: [], errors: [] }); + } + }, [isPartitioned, partitionMethod, partitionColumns, partitions, onConfigChange]); + + const handleMethodChange = (method) => { + setPartitionMethod(method); + setPartitions(generateDefaultPartitions(method)); + }; + + const handleAddPartition = () => { + const newPartition = { name: `p${partitions.length + 1}` }; + + switch (partitionMethod) { + case PARTITION_METHODS.RANGE: + newPartition.from = ''; + newPartition.to = ''; + break; + case PARTITION_METHODS.LIST: + newPartition.values = ['']; + break; + case PARTITION_METHODS.HASH: + newPartition.modulus = partitions.length + 1; + newPartition.remainder = partitions.length; + break; + } + + setPartitions([...partitions, newPartition]); + }; + + const handlePartitionChange = (index, field, value) => { + const updatedPartitions = [...partitions]; + + if (field === 'values' && partitionMethod === PARTITION_METHODS.LIST) { + // For list values, split by comma and trim + updatedPartitions[index].values = value.split(',').map(v => v.trim()).filter(v => v); + } else { + updatedPartitions[index][field] = value; + } + + setPartitions(updatedPartitions); + }; + + const handleRemovePartition = (index) => { + setPartitions(partitions.filter((_, i) => i !== index)); + }; + + const addListValue = (partitionIndex) => { + const updatedPartitions = [...partitions]; + updatedPartitions[partitionIndex].values.push(''); + setPartitions(updatedPartitions); + }; + + const removeListValue = (partitionIndex, valueIndex) => { + const updatedPartitions = [...partitions]; + updatedPartitions[partitionIndex].values = updatedPartitions[partitionIndex].values.filter((_, i) => i !== valueIndex); + setPartitions(updatedPartitions); + }; + + return ( +
+
+ +
+ + {isPartitioned && ( +
+
+ + +
+ +
+ + + Hold Ctrl/Cmd to select multiple columns +
+ +
+ + {partitions.map((partition, index) => ( +
+ handlePartitionChange(index, 'name', e.target.value)} + placeholder="Partition name" + /> + + {partitionMethod === PARTITION_METHODS.RANGE && ( + <> + handlePartitionChange(index, 'from', e.target.value)} + placeholder="FROM value" + /> + handlePartitionChange(index, 'to', e.target.value)} + placeholder="TO value" + /> + + )} + + {partitionMethod === PARTITION_METHODS.LIST && ( +
+ {partition.values.map((value, valueIndex) => ( +
+ { + const newValues = [...partition.values]; + newValues[valueIndex] = e.target.value; + handlePartitionChange(index, 'values', newValues.join(',')); + }} + placeholder="Value" + /> + {partition.values.length > 1 && ( + + )} +
+ ))} + +
+ )} + + {partitionMethod === PARTITION_METHODS.HASH && ( + <> + handlePartitionChange(index, 'modulus', parseInt(e.target.value))} + placeholder="Modulus" + /> + handlePartitionChange(index, 'remainder', parseInt(e.target.value))} + placeholder="Remainder" + /> + + )} + + +
+ ))} + + +
+ + {errors.length > 0 && ( +
+

Configuration Errors:

+
    + {errors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} +
+ )} +
+ ); +}; + +export default PartitionConfigurator; \ No newline at end of file diff --git a/src/components/synctoolbar.jsx b/src/components/synctoolbar.jsx new file mode 100644 index 0000000..c52b450 --- /dev/null +++ b/src/components/synctoolbar.jsx @@ -0,0 +1,94 @@ +// components/SyncToolbar.jsx +import React, { useState, useEffect } from 'react'; +import { useSync } from '../context/synccontext'; +import AuthModal from './authmodal'; +import ProjectModal from './ProjectModal'; +import './SyncToolbar.css'; + +const SyncToolbar = () => { + const [showAuthModal, setShowAuthModal] = useState(false); + const [showProjectModal, setShowProjectModal] = useState(false); + const [lastSaved, setLastSaved] = useState(null); + + const { isAuthenticated, user, logout, autoSave } = useSync(); + + const handleSave = async () => { + // Get current project data from your editor state + const projectData = getCurrentProjectData(); // Implement this based on your editor + + try { + await autoSave('current-project-id', projectData); + setLastSaved(new Date()); + } catch (error) { + console.error('Save failed:', error); + } + }; + + const getCurrentProjectData = () => { + // This should return the current diagram/schema data from your editor + // Replace with actual implementation + return { + name: 'Current Project', + schema: {}, // Your diagram schema + metadata: { + lastModified: new Date().toISOString(), + }, + }; + }; + + return ( +
+ {isAuthenticated ? ( +
+ Hello, {user?.username} + + + {lastSaved && ( + + Last saved: {lastSaved.toLocaleTimeString()} + + )} + +
+ ) : ( + + )} + + setShowAuthModal(false)} + onSuccess={() => setShowAuthModal(false)} + /> + + setShowProjectModal(false)} + /> +
+ ); +}; + +export default SyncToolbar; \ No newline at end of file diff --git a/src/components/tableeditor.jsx b/src/components/tableeditor.jsx new file mode 100644 index 0000000..e8df30f --- /dev/null +++ b/src/components/tableeditor.jsx @@ -0,0 +1,117 @@ +// components/TableEditor.jsx (Updated) +import React, { useState } from 'react'; +import PartitionConfigurator from './PartitionConfigurator'; +import DataTypeSelector from './datatypeselector'; + +const TableEditor = ({ table, onSave, onCancel }) => { + const [tableData, setTableData] = useState(table || { name: '', columns: [] }); + const [showPartitioning, setShowPartitioning] = useState(false); + const [partitionConfig, setPartitionConfig] = useState(null); + + const handleAddColumn = () => { + setTableData({ + ...tableData, + columns: [ + ...tableData.columns, + { name: '', type: 'VARCHAR', nullable: false, primaryKey: false } + ] + }); + }; + + const handleColumnChange = (index, field, value) => { + const updatedColumns = [...tableData.columns]; + updatedColumns[index][field] = value; + setTableData({ ...tableData, columns: updatedColumns }); + }; + + const handleRemoveColumn = (index) => { + setTableData({ + ...tableData, + columns: tableData.columns.filter((_, i) => i !== index) + }); + }; + + return ( +
+
+

{table ? 'Edit Table' : 'Create Table'}

+
+ +
+ + setTableData({ ...tableData, name: e.target.value })} + placeholder="Enter table name" + /> +
+ +
+

Columns

+ {tableData.columns.map((column, index) => ( +
+ handleColumnChange(index, 'name', e.target.value)} + placeholder="Column name" + /> + + handleColumnChange(index, 'type', dataType.type)} + /> + + + + + + +
+ ))} + + +
+ +
+ + + {showPartitioning && ( + + )} +
+ +
+ + +
+
+ ); +}; + +export default TableEditor; \ 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/context/synccontext.js b/src/context/synccontext.js new file mode 100644 index 0000000..889f064 --- /dev/null +++ b/src/context/synccontext.js @@ -0,0 +1,239 @@ +// context/SyncContext.js +import React, { createContext, useContext, useState, useCallback } from 'react'; + +const SyncContext = createContext(); + +export const useSync = () => { + const context = useContext(SyncContext); + if (!context) { + throw new Error('useSync must be used within a SyncProvider'); + } + return context; +}; + +export const SyncProvider = ({ children }) => { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [user, setUser] = useState(null); + const [syncToken, setSyncToken] = useState(localStorage.getItem('drawdb_sync_token')); + + // API base URL - adjust based on your environment + const API_BASE = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3001/api'; + + // Register new account + const register = useCallback(async (email, password, username) => { + try { + const response = await fetch(`${API_BASE}/auth/register`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password, username }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Registration failed'); + } + + const data = await response.json(); + setUser(data.user); + setSyncToken(data.token); + setIsAuthenticated(true); + localStorage.setItem('drawdb_sync_token', data.token); + + return data; + } catch (error) { + console.error('Registration error:', error); + throw error; + } + }, [API_BASE]); + + // Login to existing account + const login = useCallback(async (email, password) => { + try { + const response = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || 'Login failed'); + } + + const data = await response.json(); + setUser(data.user); + setSyncToken(data.token); + setIsAuthenticated(true); + localStorage.setItem('drawdb_sync_token', data.token); + + return data; + } catch (error) { + console.error('Login error:', error); + throw error; + } + }, [API_BASE]); + + // Login with sync token + const loginWithToken = useCallback(async (token) => { + try { + const response = await fetch(`${API_BASE}/auth/me`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Token authentication failed'); + } + + const data = await response.json(); + setUser(data.user); + setSyncToken(token); + setIsAuthenticated(true); + + return data; + } catch (error) { + console.error('Token login error:', error); + localStorage.removeItem('drawdb_sync_token'); + throw error; + } + }, [API_BASE]); + + // Logout + const logout = useCallback(() => { + setUser(null); + setSyncToken(null); + setIsAuthenticated(false); + localStorage.removeItem('drawdb_sync_token'); + }, []); + + // Save project to server + const saveProject = useCallback(async (projectId, projectData) => { + if (!syncToken) { + throw new Error('Not authenticated'); + } + + try { + const response = await fetch(`${API_BASE}/projects/${projectId || ''}`, { + method: projectId ? 'PUT' : 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${syncToken}`, + }, + body: JSON.stringify({ + name: projectData.name || 'Untitled Project', + schema: projectData.schema, + metadata: projectData.metadata, + }), + }); + + if (!response.ok) { + throw new Error('Failed to save project'); + } + + return await response.json(); + } catch (error) { + console.error('Save project error:', error); + throw error; + } + }, [syncToken, API_BASE]); + + // Load project from server + const loadProject = useCallback(async (projectId) => { + if (!syncToken) { + throw new Error('Not authenticated'); + } + + try { + const response = await fetch(`${API_BASE}/projects/${projectId}`, { + headers: { + 'Authorization': `Bearer ${syncToken}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to load project'); + } + + return await response.json(); + } catch (error) { + console.error('Load project error:', error); + throw error; + } + }, [syncToken, API_BASE]); + + // List user's projects + const listProjects = useCallback(async () => { + if (!syncToken) { + throw new Error('Not authenticated'); + } + + try { + const response = await fetch(`${API_BASE}/projects`, { + headers: { + 'Authorization': `Bearer ${syncToken}`, + }, + }); + + if (!response.ok) { + throw new Error('Failed to load projects'); + } + + return await response.json(); + } catch (error) { + console.error('List projects error:', error); + throw error; + } + }, [syncToken, API_BASE]); + + // Auto-save project + const autoSave = useCallback(async (projectId, projectData) => { + if (!syncToken) return null; + + try { + const response = await fetch(`${API_BASE}/projects/${projectId}/autosave`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${syncToken}`, + }, + body: JSON.stringify({ + schema: projectData.schema, + lastModified: new Date().toISOString(), + }), + }); + + if (response.ok) { + return await response.json(); + } + } catch (error) { + console.error('Auto-save error:', error); + } + return null; + }, [syncToken, API_BASE]); + + const value = { + isAuthenticated, + user, + syncToken, + register, + login, + loginWithToken, + logout, + saveProject, + loadProject, + listProjects, + autoSave, + }; + + 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 diff --git a/src/utils/dataType.js b/src/utils/dataType.js new file mode 100644 index 0000000..8895c7b --- /dev/null +++ b/src/utils/dataType.js @@ -0,0 +1,168 @@ +// utils/dataTypes.js +export const BASE_DATA_TYPES = { + // PostgreSQL standard types + INTEGER: 'INTEGER', + BIGINT: 'BIGINT', + SMALLINT: 'SMALLINT', + SERIAL: 'SERIAL', + BIGSERIAL: 'BIGSERIAL', + NUMERIC: 'NUMERIC', + DECIMAL: 'DECIMAL', + REAL: 'REAL', + DOUBLE_PRECISION: 'DOUBLE_PRECISION', + VARCHAR: 'VARCHAR', + CHAR: 'CHAR', + TEXT: 'TEXT', + BOOLEAN: 'BOOLEAN', + DATE: 'DATE', + TIME: 'TIME', + TIMESTAMP: 'TIMESTAMP', + TIMESTAMPTZ: 'TIMESTAMPTZ', + INTERVAL: 'INTERVAL', + JSON: 'JSON', + JSONB: 'JSONB', + UUID: 'UUID', + BYTEA: 'BYTEA', +}; + +export const CUSTOM_DATA_TYPES = { + // Popular custom types + ULID: 'ULID', + EMAIL: 'EMAIL', + URL: 'URL', + IP_ADDRESS: 'IP_ADDRESS', + MAC_ADDR: 'MAC_ADDR', + INET: 'INET', + CIDR: 'CIDR', + TSVECTOR: 'TSVECTOR', + LTREE: 'LTREE', +}; + +export const ENUM_TYPE = 'USER_DEFINED_ENUM'; + +// Data type categories for organization +export const DATA_TYPE_CATEGORIES = { + NUMERIC: { + label: 'Numeric', + types: [ + BASE_DATA_TYPES.INTEGER, + BASE_DATA_TYPES.BIGINT, + BASE_DATA_TYPES.SMALLINT, + BASE_DATA_TYPES.SERIAL, + BASE_DATA_TYPES.BIGSERIAL, + BASE_DATA_TYPES.NUMERIC, + BASE_DATA_TYPES.DECIMAL, + BASE_DATA_TYPES.REAL, + BASE_DATA_TYPES.DOUBLE_PRECISION, + ] + }, + STRING: { + label: 'String', + types: [ + BASE_DATA_TYPES.VARCHAR, + BASE_DATA_TYPES.CHAR, + BASE_DATA_TYPES.TEXT, + ] + }, + TEMPORAL: { + label: 'Date/Time', + types: [ + BASE_DATA_TYPES.DATE, + BASE_DATA_TYPES.TIME, + BASE_DATA_TYPES.TIMESTAMP, + BASE_DATA_TYPES.TIMESTAMPTZ, + BASE_DATA_TYPES.INTERVAL, + ] + }, + BOOLEAN: { + label: 'Boolean', + types: [BASE_DATA_TYPES.BOOLEAN] + }, + JSON: { + label: 'JSON', + types: [ + BASE_DATA_TYPES.JSON, + BASE_DATA_TYPES.JSONB, + ] + }, + NETWORK: { + label: 'Network', + types: [ + CUSTOM_DATA_TYPES.INET, + CUSTOM_DATA_TYPES.CIDR, + CUSTOM_DATA_TYPES.MAC_ADDR, + CUSTOM_DATA_TYPES.IP_ADDRESS, + ] + }, + CUSTOM: { + label: 'Custom', + types: [ + CUSTOM_DATA_TYPES.ULID, + CUSTOM_DATA_TYPES.EMAIL, + CUSTOM_DATA_TYPES.URL, + ENUM_TYPE, + ] + }, + OTHER: { + label: 'Other', + types: [ + BASE_DATA_TYPES.UUID, + BASE_DATA_TYPES.BYTEA, + CUSTOM_DATA_TYPES.TSVECTOR, + CUSTOM_DATA_TYPES.LTREE, + ] + } +}; + +// Data type templates for custom types +export const CUSTOM_TYPE_TEMPLATES = { + [CUSTOM_DATA_TYPES.ULID]: { + name: 'ULID', + description: 'Universally Unique Lexicographically Sortable Identifier', + definition: 'ULID', + requiresExtension: false, + }, + [CUSTOM_DATA_TYPES.EMAIL]: { + name: 'EMAIL', + description: 'Email address with validation', + definition: 'VARCHAR(255) CHECK (value ~* \'^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$\')', + requiresExtension: false, + }, + [ENUM_TYPE]: { + name: 'ENUM', + description: 'User-defined enumerated type', + definition: null, // Will be defined by user + requiresExtension: false, + isEnum: true, + }, +}; + +// Get SQL definition for a data type +export const getDataTypeDefinition = (dataType, enumValues = [], length = null) => { + if (dataType === ENUM_TYPE) { + return enumValues.length > 0 ? `ENUM('${enumValues.join("','")}')` : 'TEXT'; + } + + const template = CUSTOM_TYPE_TEMPLATES[dataType]; + if (template) { + return template.definition; + } + + // Handle length for string types + if ((dataType === BASE_DATA_TYPES.VARCHAR || dataType === BASE_DATA_TYPES.CHAR) && length) { + return `${dataType}(${length})`; + } + + if (dataType === BASE_DATA_TYPES.NUMERIC && length) { + return `${dataType}(${length})`; + } + + return dataType; +}; + +// Validate data type +export const isValidDataType = (dataType) => { + return Object.values(BASE_DATA_TYPES).includes(dataType) || + Object.values(CUSTOM_DATA_TYPES).includes(dataType) || + dataType === ENUM_TYPE; +}; \ No newline at end of file diff --git a/src/utils/partitioning.js b/src/utils/partitioning.js new file mode 100644 index 0000000..40da8fe --- /dev/null +++ b/src/utils/partitioning.js @@ -0,0 +1,151 @@ +// utils/partitioning.js +export const PARTITION_METHODS = { + RANGE: 'RANGE', + LIST: 'LIST', + HASH: 'HASH', +}; + +export const PARTITION_STRATEGIES = { + BY_RANGE: 'BY RANGE', + BY_LIST: 'BY LIST', + BY_HASH: 'BY HASH', +}; + +// Partition configuration template +export const createPartitionConfig = (method, columns, options = {}) => { + const config = { + method, + columns: Array.isArray(columns) ? columns : [columns], + options, + }; + + switch (method) { + case PARTITION_METHODS.RANGE: + config.definition = `PARTITION BY RANGE (${config.columns.join(', ')})`; + break; + case PARTITION_METHODS.LIST: + config.definition = `PARTITION BY LIST (${config.columns.join(', ')})`; + break; + case PARTITION_METHODS.HASH: + config.definition = `PARTITION BY HASH (${config.columns.join(', ')})`; + break; + default: + config.definition = ''; + } + + return config; +}; + +// Generate partition DDL +export const generatePartitionDDL = (tableName, partitionConfig, partitions = []) => { + const baseDDL = `CREATE TABLE ${tableName} (\n -- your columns here\n) ${partitionConfig.definition};\n\n`; + + const partitionDDL = partitions.map(partition => { + switch (partitionConfig.method) { + case PARTITION_METHODS.RANGE: + return `CREATE TABLE ${tableName}_${partition.name}\nPARTITION OF ${tableName}\nFOR VALUES FROM ('${partition.from}') TO ('${partition.to}');`; + + case PARTITION_METHODS.LIST: + const values = partition.values.map(v => `'${v}'`).join(', '); + return `CREATE TABLE ${tableName}_${partition.name}\nPARTITION OF ${tableName}\nFOR VALUES IN (${values});`; + + case PARTITION_METHODS.HASH: + return `CREATE TABLE ${tableName}_${partition.name}\nPARTITION OF ${tableName}\nFOR VALUES WITH (MODULUS ${partition.modulus}, REMAINDER ${partition.remainder});`; + + default: + return ''; + } + }).join('\n\n'); + + return baseDDL + partitionDDL; +}; + +// Validate partition configuration +export const validatePartitionConfig = (partitionConfig, partitions) => { + const errors = []; + + if (!partitionConfig.columns || partitionConfig.columns.length === 0) { + errors.push('At least one partition column is required'); + } + + if (partitions.length === 0) { + errors.push('At least one partition is required'); + } + + switch (partitionConfig.method) { + case PARTITION_METHODS.RANGE: + partitions.forEach((partition, index) => { + if (!partition.from || !partition.to) { + errors.push(`Partition ${partition.name} must have both FROM and TO values`); + } + }); + break; + + case PARTITION_METHODS.LIST: + partitions.forEach((partition, index) => { + if (!partition.values || partition.values.length === 0) { + errors.push(`Partition ${partition.name} must have at least one value`); + } + }); + break; + + case PARTITION_METHODS.HASH: + partitions.forEach((partition, index) => { + if (partition.modulus === undefined || partition.remainder === undefined) { + errors.push(`Partition ${partition.name} must have modulus and remainder`); + } + if (partition.remainder >= partition.modulus) { + errors.push(`Partition ${partition.name} remainder must be less than modulus`); + } + }); + + // Check that all remainders are unique and cover the range + const remainders = partitions.map(p => p.remainder); + const uniqueRemainders = new Set(remainders); + if (uniqueRemainders.size !== remainders.length) { + errors.push('All partition remainders must be unique'); + } + break; + } + + return errors; +}; + +// Generate default partitions based on method +export const generateDefaultPartitions = (method, count = 4) => { + switch (method) { + case PARTITION_METHODS.RANGE: + // Generate monthly partitions for the next year + const ranges = []; + const now = new Date(); + for (let i = 0; i < count; i++) { + const month = new Date(now.getFullYear(), now.getMonth() + i, 1); + const nextMonth = new Date(now.getFullYear(), now.getMonth() + i + 1, 1); + ranges.push({ + name: `p${(month.getMonth() + 1).toString().padStart(2, '0')}_${month.getFullYear()}`, + from: month.toISOString().split('T')[0], + to: nextMonth.toISOString().split('T')[0], + }); + } + return ranges; + + case PARTITION_METHODS.LIST: + // Generate partitions for common categories + return [ + { name: 'active', values: ['active', 'enabled'] }, + { name: 'inactive', values: ['inactive', 'disabled'] }, + { name: 'pending', values: ['pending', 'draft'] }, + ]; + + case PARTITION_METHODS.HASH: + // Generate hash partitions + return Array.from({ length: count }, (_, i) => ({ + name: `p${i}`, + modulus: count, + remainder: i, + })); + + default: + return []; + } +}; \ No newline at end of file