Fix healthchecks: install curl in backend and frontend images
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import api from '../services/api';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [drawings, setDrawings] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
api.get('/drawings').then(res => {
|
||||
setDrawings(res.data);
|
||||
setLoading(false);
|
||||
}).catch(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
const res = await api.post('/drawings', { name: 'Neue Zeichnung' });
|
||||
navigate(`/editor/${res.data.id}`);
|
||||
} catch (err) {
|
||||
alert('Fehler beim Erstellen');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Zeichnung wirklich löschen?')) return;
|
||||
try {
|
||||
await api.delete(`/drawings/${id}`);
|
||||
setDrawings(drawings.filter(d => d.id !== id));
|
||||
} catch (err) {
|
||||
alert('Fehler beim Löschen');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div style={{ padding: 20 }}>Lade...</div>;
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 800, margin: '20px auto', padding: 20 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 20 }}>
|
||||
<h2>Meine Zeichnungen ({user?.email})</h2>
|
||||
<button onClick={logout} style={{ padding: '8px 16px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Abmelden</button>
|
||||
</div>
|
||||
<button onClick={handleCreate} style={{ padding: '10px 20px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer', marginBottom: 20 }}>Neue Zeichnung</button>
|
||||
{drawings.length === 0 ? (
|
||||
<p>Keine Zeichnungen vorhanden.</p>
|
||||
) : (
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Name</th><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Zuletzt geändert</th><th style={{ textAlign: 'left', padding: 8, borderBottom: '1px solid #ddd' }}>Aktionen</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{drawings.map(d => (
|
||||
<tr key={d.id}>
|
||||
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>{d.name}</td>
|
||||
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>{new Date(d.updatedAt).toLocaleString()}</td>
|
||||
<td style={{ padding: 8, borderBottom: '1px solid #eee' }}>
|
||||
<button onClick={() => navigate(`/editor/${d.id}`)} style={{ marginRight: 8, padding: '4px 12px', backgroundColor: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Öffnen</button>
|
||||
<button onClick={() => handleDelete(d.id)} style={{ padding: '4px 12px', backgroundColor: '#ff4d4f', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Löschen</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import api from '../services/api';
|
||||
import CADCanvas from '../components/CADCanvas';
|
||||
import Toolbar from '../components/Toolbar';
|
||||
import LayerPanel from '../components/LayerPanel';
|
||||
import BlockLibrary from '../components/BlockLibrary';
|
||||
import pluginRegistry from '../components/PluginRegistry';
|
||||
|
||||
const Editor = () => {
|
||||
const { id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const canvasRef = useRef(null);
|
||||
const [drawing, setDrawing] = useState(null);
|
||||
const [activeTool, setActiveTool] = useState('select');
|
||||
const [layers, setLayers] = useState([
|
||||
{ name: 'Layer 0', visible: true, locked: false },
|
||||
]);
|
||||
const [activeLayer, setActiveLayer] = useState('Layer 0');
|
||||
const [showLayers, setShowLayers] = useState(true);
|
||||
const [showBlocks, setShowBlocks] = useState(true);
|
||||
const [saveStatus, setSaveStatus] = useState('');
|
||||
const fileInputRef = useRef(null);
|
||||
|
||||
// Load drawing
|
||||
useEffect(() => {
|
||||
if (id && id !== 'new') {
|
||||
api.get(`/drawings/${id}`).then(res => {
|
||||
setDrawing(res.data);
|
||||
if (res.data.canvas_json) {
|
||||
try {
|
||||
const json = JSON.parse(res.data.canvas_json);
|
||||
canvasRef.current?.loadFromJSON?.(json);
|
||||
} catch (e) {
|
||||
console.warn('Canvas JSON parse error, starting fresh');
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
navigate('/dashboard');
|
||||
});
|
||||
} else {
|
||||
api.post('/drawings', { name: 'Neue Zeichnung' }).then(res => {
|
||||
setDrawing(res.data);
|
||||
navigate(`/editor/${res.data.id}`, { replace: true });
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
navigate('/dashboard');
|
||||
});
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
// Initialize plugin registry when canvas is ready
|
||||
const handleCanvasReady = (canvas) => {
|
||||
const cadApp = {
|
||||
getCanvas: () => canvas,
|
||||
getApi: () => api,
|
||||
getDrawing: () => drawing,
|
||||
registerTool: (id, label, icon, handler) => {
|
||||
console.log(`Plugin registered tool: ${id}`);
|
||||
// Would integrate into toolbar dynamically
|
||||
},
|
||||
};
|
||||
pluginRegistry.init(cadApp);
|
||||
};
|
||||
|
||||
// Save canvas JSON to backend
|
||||
const saveCanvas = async () => {
|
||||
if (!drawing || !canvasRef.current) return;
|
||||
try {
|
||||
setSaveStatus('Speichere...');
|
||||
const json = canvasRef.current.toJSON();
|
||||
await api.put(`/drawings/${drawing.id}`, {
|
||||
canvas_json: JSON.stringify(json),
|
||||
name: drawing.name,
|
||||
});
|
||||
setSaveStatus('Gespeichert ✓');
|
||||
setTimeout(() => setSaveStatus(''), 2000);
|
||||
} catch (err) {
|
||||
setSaveStatus('Fehler ✗');
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
// DXF Import
|
||||
const handleDxfImport = async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await api.post('/dxf/import', formData);
|
||||
if (res.data?.objects && canvasRef.current) {
|
||||
canvasRef.current.addObjects(res.data.objects);
|
||||
setSaveStatus('DXF importiert ✓');
|
||||
setTimeout(() => setSaveStatus(''), 2000);
|
||||
}
|
||||
e.target.value = '';
|
||||
} catch (err) {
|
||||
console.error('DXF import error:', err);
|
||||
setSaveStatus('DXF-Import Fehler ✗');
|
||||
setTimeout(() => setSaveStatus(''), 3000);
|
||||
e.target.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
// DXF Export
|
||||
const handleDxfExport = async () => {
|
||||
if (!canvasRef.current) return;
|
||||
try {
|
||||
const json = canvasRef.current.toJSON();
|
||||
const res = await api.post('/dxf/export', { objects: json.objects || [] }, {
|
||||
responseType: 'blob',
|
||||
});
|
||||
const blob = new Blob([res.data], { type: 'application/dxf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (drawing?.name || 'zeichnung') + '.dxf';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setSaveStatus('DXF exportiert ✓');
|
||||
setTimeout(() => setSaveStatus(''), 2000);
|
||||
} catch (err) {
|
||||
console.error('DXF export error:', err);
|
||||
setSaveStatus('DXF-Export Fehler ✗');
|
||||
setTimeout(() => setSaveStatus(''), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
// SVG Export (client-side)
|
||||
const handleSvgExport = () => {
|
||||
if (!canvasRef.current) return;
|
||||
const canvas = canvasRef.current.getCanvas();
|
||||
if (!canvas) return;
|
||||
try {
|
||||
const svg = canvas.toSVG();
|
||||
const blob = new Blob([svg], { type: 'image/svg+xml' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = (drawing?.name || 'zeichnung') + '.svg';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
setSaveStatus('SVG exportiert ✓');
|
||||
setTimeout(() => setSaveStatus(''), 2000);
|
||||
} catch (err) {
|
||||
console.error('SVG export error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!confirm('Ausgewähltes Objekt löschen?')) return;
|
||||
const canvas = canvasRef.current?.getCanvas();
|
||||
if (canvas) {
|
||||
const active = canvas.getActiveObject();
|
||||
if (active) {
|
||||
canvas.remove(active);
|
||||
canvas.renderAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard shortcuts
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
e.preventDefault();
|
||||
saveCanvas();
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
handleDelete();
|
||||
} else if (e.ctrlKey && e.key === 'z') {
|
||||
// Undo would need command stack
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [drawing]);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', height: '100vh', overflow: 'hidden' }}>
|
||||
{/* Toolbar */}
|
||||
<div style={{ width: 50, background: '#2c3e50', color: 'white', display: 'flex', flexDirection: 'column', alignItems: 'center', paddingTop: 10, gap: 10, zIndex: 20 }}>
|
||||
<Toolbar activeTool={activeTool} setActiveTool={setActiveTool} canvasRef={canvasRef} />
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
title="Löschen"
|
||||
style={{ width: 36, height: 36, background: '#c0392b', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer', marginTop: 'auto', marginBottom: 10 }}
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Canvas area */}
|
||||
<div style={{ flex: 1, position: 'relative', background: '#f0f0f0' }}>
|
||||
<div style={{ position: 'absolute', top: 10, left: 10, zIndex: 10, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
|
||||
<button onClick={() => navigate('/dashboard')} style={{ padding: '4px 12px', background: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>← Zurück</button>
|
||||
<button onClick={saveCanvas} style={{ padding: '4px 12px', background: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Speichern</button>
|
||||
<button onClick={() => fileInputRef.current?.click()} style={{ padding: '4px 12px', background: '#faad14', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>DXF Import</button>
|
||||
<button onClick={handleDxfExport} style={{ padding: '4px 12px', background: '#722ed1', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>DXF Export</button>
|
||||
<button onClick={handleSvgExport} style={{ padding: '4px 12px', background: '#13c2c2', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>SVG Export</button>
|
||||
<span style={{ padding: '4px 12px', background: 'white', borderRadius: 4, fontSize: 14 }}>{saveStatus}</span>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleDxfImport}
|
||||
accept=".dxf"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<CADCanvas
|
||||
ref={canvasRef}
|
||||
activeTool={activeTool}
|
||||
setActiveTool={setActiveTool}
|
||||
onCanvasReady={handleCanvasReady}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right panels */}
|
||||
<div style={{ width: 250, background: '#fafafa', borderLeft: '1px solid #ddd', overflowY: 'auto' }}>
|
||||
<div style={{ padding: 10, borderBottom: '1px solid #ddd', cursor: 'pointer', fontWeight: 'bold', userSelect: 'none' }} onClick={() => setShowLayers(!showLayers)}>
|
||||
Layer {showLayers ? '▲' : '▼'}
|
||||
</div>
|
||||
{showLayers && (
|
||||
<LayerPanel layers={layers} setLayers={setLayers} activeLayer={activeLayer} setActiveLayer={setActiveLayer} />
|
||||
)}
|
||||
<div style={{ padding: 10, borderBottom: '1px solid #ddd', cursor: 'pointer', fontWeight: 'bold', userSelect: 'none' }} onClick={() => setShowBlocks(!showBlocks)}>
|
||||
Teilebibliothek {showBlocks ? '▲' : '▼'}
|
||||
</div>
|
||||
{showBlocks && (
|
||||
<BlockLibrary canvasRef={canvasRef} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Editor;
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const Login = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
try {
|
||||
await login(email, password);
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Login fehlgeschlagen');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20, background: 'white', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
|
||||
<h2>Web-CAD Anmeldung</h2>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label>E-Mail</label><br />
|
||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label>Passwort</label><br />
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
<button type="submit" style={{ width: '100%', padding: 10, backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Anmelden</button>
|
||||
</form>
|
||||
<p style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
Kein Konto? <Link to="/register">Registrieren</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -0,0 +1,54 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, Link } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const Register = () => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [passwordConfirm, setPasswordConfirm] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { register } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
if (password !== passwordConfirm) {
|
||||
setError('Passwörter stimmen nicht überein');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await register(email, password);
|
||||
navigate('/dashboard');
|
||||
} catch (err) {
|
||||
setError(err.response?.data?.error || 'Registrierung fehlgeschlagen');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 400, margin: '100px auto', padding: 20, background: 'white', borderRadius: 8, boxShadow: '0 2px 8px rgba(0,0,0,0.15)' }}>
|
||||
<h2>Registrierung</h2>
|
||||
{error && <p style={{ color: 'red' }}>{error}</p>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label>E-Mail</label><br />
|
||||
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<label>Passwort</label><br />
|
||||
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<label>Passwort bestätigen</label><br />
|
||||
<input type="password" value={passwordConfirm} onChange={(e) => setPasswordConfirm(e.target.value)} required style={{ width: '100%', padding: 8, borderRadius: 4, border: '1px solid #ccc' }} />
|
||||
</div>
|
||||
<button type="submit" style={{ width: '100%', padding: 10, backgroundColor: '#52c41a', color: 'white', border: 'none', borderRadius: 4, cursor: 'pointer' }}>Registrieren</button>
|
||||
</form>
|
||||
<p style={{ marginTop: 12, textAlign: 'center' }}>
|
||||
Bereits registriert? <Link to="/login">Anmelden</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Register;
|
||||
Reference in New Issue
Block a user