Refactor project structure and update configurations
- Update .gitignore to include additional build artifacts, logs, and editor files - Modify package.json scripts for improved development workflow and remove unused scripts - Change Vite output directory to 'dist' for consistency - Simplify App.jsx by removing unused state and functions, enhancing readability - Adjust server.js to serve static files from the new 'dist' directory
This commit is contained in:
+18
-4
@@ -1,6 +1,20 @@
|
||||
node_modules
|
||||
.env
|
||||
# Dependencies
|
||||
node_modules/
|
||||
package-lock.json
|
||||
.cursor
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
public/dist/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Editor artifacts
|
||||
.cursor/
|
||||
.claude/
|
||||
|
||||
# Development artifacts
|
||||
*.excalidraw
|
||||
public/dist/assets/
|
||||
+3
-356
@@ -5,24 +5,8 @@ import '@excalidraw/excalidraw/index.css'
|
||||
function App() {
|
||||
const [excalidrawAPI, setExcalidrawAPI] = useState(null)
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
const [elementCount, setElementCount] = useState(0)
|
||||
const [apiPanelVisible, setApiPanelVisible] = useState(true)
|
||||
const [notifications, setNotifications] = useState([])
|
||||
const websocketRef = useRef(null)
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState({
|
||||
type: 'rectangle',
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
text: '',
|
||||
backgroundColor: '#ffffff',
|
||||
strokeColor: '#000000',
|
||||
strokeWidth: 2
|
||||
})
|
||||
|
||||
// WebSocket connection
|
||||
useEffect(() => {
|
||||
connectWebSocket()
|
||||
@@ -36,17 +20,11 @@ function App() {
|
||||
// Load existing elements when Excalidraw API becomes available
|
||||
useEffect(() => {
|
||||
if (excalidrawAPI) {
|
||||
console.log('ExcalidrawAPI ready, setting up real-time sync')
|
||||
|
||||
// Load existing elements immediately
|
||||
loadExistingElements()
|
||||
|
||||
// Ensure WebSocket is connected for real-time updates
|
||||
if (!isConnected) {
|
||||
console.log('ExcalidrawAPI ready but WebSocket not connected, connecting...')
|
||||
connectWebSocket()
|
||||
} else {
|
||||
console.log('Both ExcalidrawAPI and WebSocket are ready for real-time sync')
|
||||
}
|
||||
}
|
||||
}, [excalidrawAPI, isConnected])
|
||||
@@ -57,9 +35,7 @@ function App() {
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.elements && result.elements.length > 0) {
|
||||
console.log('Loading existing elements from API:', result.elements.length)
|
||||
const convertedElements = convertToExcalidrawElements(result.elements)
|
||||
console.log('Converted existing elements:', convertedElements)
|
||||
excalidrawAPI.updateScene({ elements: convertedElements })
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -69,24 +45,19 @@ function App() {
|
||||
|
||||
const connectWebSocket = () => {
|
||||
if (websocketRef.current && websocketRef.current.readyState === WebSocket.OPEN) {
|
||||
console.log('WebSocket already connected')
|
||||
return
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}`
|
||||
|
||||
console.log('Connecting to WebSocket:', wsUrl)
|
||||
websocketRef.current = new WebSocket(wsUrl)
|
||||
|
||||
websocketRef.current.onopen = () => {
|
||||
console.log('WebSocket connected successfully')
|
||||
setIsConnected(true)
|
||||
|
||||
// Request existing elements when WebSocket connects and API is ready
|
||||
if (excalidrawAPI) {
|
||||
console.log('WebSocket connected and ExcalidrawAPI ready - requesting refresh')
|
||||
setTimeout(loadExistingElements, 100) // Small delay to ensure connection is stable
|
||||
setTimeout(loadExistingElements, 100)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,12 +71,10 @@ function App() {
|
||||
}
|
||||
|
||||
websocketRef.current.onclose = (event) => {
|
||||
console.log('WebSocket disconnected:', event.code, event.reason)
|
||||
setIsConnected(false)
|
||||
|
||||
// Reconnect after 3 seconds if not a clean close
|
||||
if (event.code !== 1000) {
|
||||
console.log('Attempting to reconnect in 3 seconds...')
|
||||
setTimeout(connectWebSocket, 3000)
|
||||
}
|
||||
}
|
||||
@@ -117,62 +86,44 @@ function App() {
|
||||
}
|
||||
|
||||
const handleWebSocketMessage = (data) => {
|
||||
console.log('WebSocket message received:', data.type, data)
|
||||
|
||||
if (!excalidrawAPI) {
|
||||
console.log('ExcalidrawAPI not ready, skipping message:', data.type)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const currentElements = excalidrawAPI.getSceneElements()
|
||||
console.log('Current elements count:', currentElements.length)
|
||||
|
||||
switch (data.type) {
|
||||
case 'initial_elements':
|
||||
if (data.elements && data.elements.length > 0) {
|
||||
console.log('Loading initial elements:', data.elements.length)
|
||||
const convertedElements = convertToExcalidrawElements(data.elements)
|
||||
console.log('Converted initial elements:', convertedElements.length)
|
||||
excalidrawAPI.updateScene({ elements: convertedElements })
|
||||
}
|
||||
break
|
||||
|
||||
case 'element_created':
|
||||
console.log('Processing element_created:', data.element)
|
||||
const newElement = convertToExcalidrawElements([data.element])
|
||||
console.log('Converted new element:', newElement)
|
||||
const updatedElementsAfterCreate = [...currentElements, ...newElement]
|
||||
console.log('Total elements after create:', updatedElementsAfterCreate.length)
|
||||
excalidrawAPI.updateScene({ elements: updatedElementsAfterCreate })
|
||||
showNotification('Element created successfully!', 'success')
|
||||
break
|
||||
|
||||
case 'element_updated':
|
||||
console.log('Processing element_updated:', data.element.id)
|
||||
const convertedUpdatedElement = convertToExcalidrawElements([data.element])[0]
|
||||
const updatedElements = currentElements.map(el =>
|
||||
el.id === data.element.id ? convertedUpdatedElement : el
|
||||
)
|
||||
excalidrawAPI.updateScene({ elements: updatedElements })
|
||||
showNotification('Element updated successfully!', 'success')
|
||||
break
|
||||
|
||||
case 'element_deleted':
|
||||
console.log('Processing element_deleted:', data.elementId)
|
||||
const filteredElements = currentElements.filter(el => el.id !== data.elementId)
|
||||
excalidrawAPI.updateScene({ elements: filteredElements })
|
||||
showNotification('Element deleted successfully!', 'success')
|
||||
break
|
||||
|
||||
case 'elements_batch_created':
|
||||
console.log('Processing elements_batch_created:', data.elements.length)
|
||||
const batchElements = convertToExcalidrawElements(data.elements)
|
||||
console.log('Converted batch elements:', batchElements.length)
|
||||
const updatedElementsAfterBatch = [...currentElements, ...batchElements]
|
||||
console.log('Total elements after batch:', updatedElementsAfterBatch.length)
|
||||
excalidrawAPI.updateScene({ elements: updatedElementsAfterBatch })
|
||||
showNotification(`${data.elements.length} elements created!`, 'success')
|
||||
break
|
||||
|
||||
default:
|
||||
@@ -180,344 +131,51 @@ function App() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error processing WebSocket message:', error, data)
|
||||
showNotification('Error processing real-time update', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const showNotification = (message, type = 'success') => {
|
||||
const id = Date.now()
|
||||
const notification = { id, message, type }
|
||||
setNotifications(prev => [...prev, notification])
|
||||
|
||||
setTimeout(() => {
|
||||
setNotifications(prev => prev.filter(n => n.id !== id))
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
const handleExcalidrawChange = (elements, appState, files) => {
|
||||
setElementCount(elements.length)
|
||||
}
|
||||
|
||||
const handleFormSubmit = async (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
const elementData = {
|
||||
type: formData.type,
|
||||
x: parseInt(formData.x),
|
||||
y: parseInt(formData.y),
|
||||
width: parseInt(formData.width) || undefined,
|
||||
height: parseInt(formData.height) || undefined,
|
||||
backgroundColor: formData.backgroundColor,
|
||||
strokeColor: formData.strokeColor,
|
||||
strokeWidth: parseInt(formData.strokeWidth)
|
||||
}
|
||||
|
||||
if (formData.text) {
|
||||
elementData.text = formData.text
|
||||
}
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(elementData).forEach(key => {
|
||||
if (elementData[key] === undefined) {
|
||||
delete elementData[key]
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/elements', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(elementData)
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
showNotification('Element created via API!', 'success')
|
||||
// Reset form positions
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
x: Math.floor(Math.random() * 400) + 50,
|
||||
y: Math.floor(Math.random() * 300) + 50
|
||||
}))
|
||||
} else {
|
||||
showNotification(`Error: ${result.error}`, 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating element:', error)
|
||||
showNotification('Failed to create element', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const createSampleElements = async () => {
|
||||
const sampleElements = [
|
||||
{
|
||||
type: 'rectangle',
|
||||
x: 50,
|
||||
y: 50,
|
||||
width: 150,
|
||||
height: 100,
|
||||
backgroundColor: '#ffeaa7',
|
||||
strokeColor: '#2d3436'
|
||||
},
|
||||
{
|
||||
type: 'ellipse',
|
||||
x: 250,
|
||||
y: 50,
|
||||
width: 120,
|
||||
height: 120,
|
||||
backgroundColor: '#74b9ff',
|
||||
strokeColor: '#0984e3'
|
||||
},
|
||||
{
|
||||
type: 'diamond',
|
||||
x: 50,
|
||||
y: 200,
|
||||
width: 100,
|
||||
height: 100,
|
||||
backgroundColor: '#fd79a8',
|
||||
strokeColor: '#e84393'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
x: 250,
|
||||
y: 220,
|
||||
text: 'Hello from API!',
|
||||
fontSize: 20,
|
||||
strokeColor: '#2d3436'
|
||||
}
|
||||
]
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/elements/batch', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ elements: sampleElements })
|
||||
})
|
||||
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success) {
|
||||
showNotification(`${result.count} sample elements created!`, 'success')
|
||||
} else {
|
||||
showNotification(`Error: ${result.error}`, 'error')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating sample elements:', error)
|
||||
showNotification('Failed to create sample elements', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const clearCanvas = async () => {
|
||||
if (excalidrawAPI) {
|
||||
try {
|
||||
// First, get all current elements
|
||||
// Get all current elements and delete them from backend
|
||||
const response = await fetch('/api/elements')
|
||||
const result = await response.json()
|
||||
|
||||
if (result.success && result.elements) {
|
||||
// Delete all elements from backend
|
||||
const deletePromises = result.elements.map(element =>
|
||||
fetch(`/api/elements/${element.id}`, { method: 'DELETE' })
|
||||
)
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
console.log('All elements deleted from backend')
|
||||
}
|
||||
|
||||
// Clear the frontend canvas
|
||||
excalidrawAPI.updateScene({ elements: [] })
|
||||
showNotification('Canvas cleared completely!', 'success')
|
||||
} catch (error) {
|
||||
console.error('Error clearing canvas:', error)
|
||||
// Still clear frontend even if backend fails
|
||||
excalidrawAPI.updateScene({ elements: [] })
|
||||
showNotification('Canvas cleared (frontend only)', 'warning')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const refreshElements = async () => {
|
||||
console.log('Manual refresh requested')
|
||||
if (excalidrawAPI) {
|
||||
await loadExistingElements()
|
||||
showNotification('Elements refreshed!', 'success')
|
||||
} else {
|
||||
showNotification('Canvas not ready yet', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const forceReconnectWebSocket = () => {
|
||||
console.log('Force reconnecting WebSocket')
|
||||
if (websocketRef.current) {
|
||||
websocketRef.current.close()
|
||||
}
|
||||
setTimeout(connectWebSocket, 100)
|
||||
}
|
||||
|
||||
const toggleApiPanel = () => {
|
||||
setApiPanelVisible(!apiPanelVisible)
|
||||
}
|
||||
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value } = e.target
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
[name]: value
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
{/* Header */}
|
||||
<div className="header">
|
||||
<h1>Excalidraw POC - Backend API Integration</h1>
|
||||
<h1>Excalidraw Canvas</h1>
|
||||
<div className="controls">
|
||||
<div className="status">
|
||||
<div className={`status-dot ${isConnected ? 'status-connected' : 'status-disconnected'}`}></div>
|
||||
<span>{isConnected ? 'Connected' : 'Disconnected'}</span>
|
||||
</div>
|
||||
<div className="element-count">
|
||||
Elements: <span>{elementCount}</span>
|
||||
</div>
|
||||
<button className="btn-secondary" onClick={clearCanvas}>Clear Canvas</button>
|
||||
<button className="btn-secondary" onClick={refreshElements}>Refresh Elements</button>
|
||||
<button className="btn-primary" onClick={createSampleElements}>Create Sample Elements</button>
|
||||
{!isConnected && (
|
||||
<button className="btn-danger" onClick={forceReconnectWebSocket}>Reconnect WS</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Panel Toggle */}
|
||||
<button className="toggle-panel" onClick={toggleApiPanel}>
|
||||
API Panel
|
||||
</button>
|
||||
|
||||
{/* API Panel */}
|
||||
{apiPanelVisible && (
|
||||
<div className="api-panel">
|
||||
<h3>Create Element via API</h3>
|
||||
<form className="api-form" onSubmit={handleFormSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="type">Element Type:</label>
|
||||
<select
|
||||
name="type"
|
||||
value={formData.type}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
>
|
||||
<option value="rectangle">Rectangle</option>
|
||||
<option value="ellipse">Ellipse</option>
|
||||
<option value="diamond">Diamond</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="arrow">Arrow</option>
|
||||
<option value="line">Line</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="x">X Position:</label>
|
||||
<input
|
||||
type="number"
|
||||
name="x"
|
||||
value={formData.x}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="y">Y Position:</label>
|
||||
<input
|
||||
type="number"
|
||||
name="y"
|
||||
value={formData.y}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="width">Width:</label>
|
||||
<input
|
||||
type="number"
|
||||
name="width"
|
||||
value={formData.width}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="height">Height:</label>
|
||||
<input
|
||||
type="number"
|
||||
name="height"
|
||||
value={formData.height}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="text">Text (for text elements):</label>
|
||||
<input
|
||||
type="text"
|
||||
name="text"
|
||||
value={formData.text}
|
||||
onChange={handleInputChange}
|
||||
placeholder="Enter text content"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-row">
|
||||
<div className="form-group">
|
||||
<label htmlFor="backgroundColor">Background Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
name="backgroundColor"
|
||||
value={formData.backgroundColor}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="strokeColor">Stroke Color:</label>
|
||||
<input
|
||||
type="color"
|
||||
name="strokeColor"
|
||||
value={formData.strokeColor}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="strokeWidth">Stroke Width:</label>
|
||||
<input
|
||||
type="number"
|
||||
name="strokeWidth"
|
||||
value={formData.strokeWidth}
|
||||
onChange={handleInputChange}
|
||||
min="1"
|
||||
max="10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn-success">Create Element</button>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Canvas Container */}
|
||||
<div className="canvas-container">
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api) => setExcalidrawAPI(api)}
|
||||
onChange={handleExcalidrawChange}
|
||||
initialData={{
|
||||
elements: [],
|
||||
appState: {
|
||||
@@ -527,17 +185,6 @@ function App() {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notifications */}
|
||||
{notifications.map(notification => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className={`notification ${notification.type}`}
|
||||
style={{ top: `${20 + notifications.indexOf(notification) * 60}px` }}
|
||||
>
|
||||
{notification.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+4
-19
@@ -4,24 +4,12 @@
|
||||
"description": "MCP server for Excalidraw",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"excalidraw-mcp": "./src/cli.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"server": "node src/server.js",
|
||||
"server:dev": "nodemon src/server.js",
|
||||
"frontend:dev": "vite",
|
||||
"frontend:build": "vite build",
|
||||
"frontend:preview": "vite preview",
|
||||
"build": "npm run frontend:build",
|
||||
"poc": "concurrently \"npm run server\" \"npm run frontend:dev\"",
|
||||
"test": "jest",
|
||||
"lint": "eslint src/**/*.js",
|
||||
"docker:build": "docker build -t mcp/excalidraw .",
|
||||
"docker:run": "docker run -p 3000:3000 mcp/excalidraw",
|
||||
"prepare": "node -e \"try { require('fs').chmodSync('./src/cli.js', '755') } catch(e) { console.log(e) }\""
|
||||
"canvas": "node src/server.js",
|
||||
"build": "vite build",
|
||||
"dev": "concurrently \"npm run canvas\" \"vite\"",
|
||||
"production": "npm run build && npm run canvas"
|
||||
},
|
||||
"dependencies": {
|
||||
"@excalidraw/excalidraw": "^0.18.0",
|
||||
@@ -40,9 +28,6 @@
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"concurrently": "^9.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "^3.0.2",
|
||||
"vite": "^6.3.5"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
+3
-8
@@ -27,11 +27,8 @@ const wss = new WebSocketServer({ server });
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Serve static files from the build directory for production
|
||||
// or from the old public directory for development
|
||||
const staticDir = process.env.NODE_ENV === 'production'
|
||||
? path.join(__dirname, '../public/dist')
|
||||
: path.join(__dirname, '../public');
|
||||
// Serve static files from the build directory
|
||||
const staticDir = path.join(__dirname, '../dist');
|
||||
app.use(express.static(staticDir));
|
||||
|
||||
// WebSocket connections
|
||||
@@ -345,9 +342,7 @@ app.post('/api/elements/batch', (req, res) => {
|
||||
|
||||
// Serve the frontend
|
||||
app.get('/', (req, res) => {
|
||||
const htmlFile = process.env.NODE_ENV === 'production'
|
||||
? path.join(__dirname, '../public/dist/index.html')
|
||||
: path.join(__dirname, '../public/index.html');
|
||||
const htmlFile = path.join(__dirname, '../dist/index.html');
|
||||
res.sendFile(htmlFile);
|
||||
});
|
||||
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
build: {
|
||||
outDir: 'public/dist',
|
||||
outDir: 'dist',
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: './frontend/index.html',
|
||||
|
||||
Reference in New Issue
Block a user