Add frontend React app and server components
- Add React frontend with Excalidraw integration (App.jsx, main.jsx) - Add Express server with MCP protocol support (server.js) - Update CLI with new functionality (cli.js) - Add Vite configuration for frontend build - Update package.json with new dependencies - Add public assets and build files - Update .gitignore to exclude build artifacts
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Excalidraw POC - Backend API Integration</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 10px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #545b62;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #218838;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
.canvas-container {
|
||||
height: calc(100vh - 80px);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.api-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 100px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
padding: 20px;
|
||||
width: 300px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.api-panel h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.api-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toggle-panel {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
top: 60px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.api-panel.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
z-index: 1002;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background-color: #28a745;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background-color: #dc3545;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.element-count {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-content div:first-child {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,523 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import { Excalidraw, convertToExcalidrawElements } from '@excalidraw/excalidraw'
|
||||
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()
|
||||
return () => {
|
||||
if (websocketRef.current) {
|
||||
websocketRef.current.close()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 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])
|
||||
|
||||
const loadExistingElements = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/elements')
|
||||
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) {
|
||||
console.error('Error loading existing elements:', error)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
websocketRef.current.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
handleWebSocketMessage(data)
|
||||
} catch (error) {
|
||||
console.error('Error parsing WebSocket message:', error, event.data)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
websocketRef.current.onerror = (error) => {
|
||||
console.error('WebSocket error:', error)
|
||||
setIsConnected(false)
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
console.log('Unknown WebSocket message type:', data.type)
|
||||
}
|
||||
} 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 = () => {
|
||||
if (excalidrawAPI) {
|
||||
excalidrawAPI.updateScene({ elements: [] })
|
||||
showNotification('Canvas cleared!', 'success')
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
<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: {
|
||||
theme: 'light',
|
||||
viewBackgroundColor: '#ffffff'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user