feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
import React, { useState, useRef } from 'react';
import type { BlockLibraryProps } from '../types/ui.types';
import type { BlockDefinition } from '../types/cad.types';
const categories = ['Alle', 'Bestuhlung', 'Tische', 'Bühne', 'Architektur', 'Custom'];
const BlockLibrary: React.FC<BlockLibraryProps> = ({ blocks, category, onCategoryChange, onSearch, onDragBlock, onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock }) => {
const [searchQuery, setSearchQuery] = useState('');
const [expandedCats, setExpandedCats] = useState<Set<string>>(new Set(['Bestuhlung']));
const fileInputRef = useRef<HTMLInputElement>(null);
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const q = e.target.value;
setSearchQuery(q);
onSearch(q);
};
const filteredBlocks = blocks.filter(b => {
const catMatch = category === 'Alle' || b.category === category;
const q = searchQuery.toLowerCase().trim();
const searchMatch = !q || b.name.toLowerCase().includes(q) || b.description.toLowerCase().includes(q);
return catMatch && searchMatch;
});
// Group by category
const grouped: Record<string, BlockDefinition[]> = {};
for (const b of filteredBlocks) {
if (!grouped[b.category]) grouped[b.category] = [];
grouped[b.category].push(b);
}
const toggleCat = (cat: string) => {
setExpandedCats((prev) => {
const next = new Set(prev);
if (next.has(cat)) next.delete(cat);
else next.add(cat);
return next;
});
};
const handleDragStart = (e: React.DragEvent, blockId: string) => {
e.dataTransfer.setData('text/block-id', blockId);
e.dataTransfer.effectAllowed = 'copy';
onDragBlock(blockId);
};
const handleSvgImport = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
const svgContent = ev.target?.result as string;
if (onSvgImport) {
onSvgImport(svgContent, file.name.replace(/\.svg$/i, ''), 'Custom');
} else {
window.dispatchEvent(new CustomEvent('block-svg-import', { detail: { svg: svgContent, name: file.name.replace(/\.svg$/i, ''), category: 'Custom' } }));
}
};
reader.readAsText(file);
e.target.value = '';
};
return (
<>
<div className="lib-search">
<span className="lib-search-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</span>
<input type="text" placeholder="Block suchen..." aria-label="Bibliothek durchsuchen" value={searchQuery} onChange={handleSearch} />
</div>
<div className="lib-categories">
{categories.map((cat) => (
<button key={cat} className={`lib-cat${category === cat ? ' active' : ''}`} onClick={() => onCategoryChange(cat)}>{cat}</button>
))}
</div>
<div className="lib-import">
<button className="lib-import-btn" title="SVG importieren" onClick={() => fileInputRef.current?.click()}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span>SVG importieren</span>
</button>
{onSaveGroupAsBlock && (
<button className="lib-import-btn" title="Auswahl als Block speichern" onClick={() => { const name = window.prompt('Block-Name:', ''); if (name) onSaveGroupAsBlock(name); }}>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>
<span>Als Block speichern</span>
</button>
)}
<input ref={fileInputRef} type="file" accept=".svg,image/svg+xml" style={{ display: 'none' }} onChange={handleSvgImport} />
</div>
<div className="tree tree-library" role="tree" aria-label="Bibliothek-Struktur">
{Object.entries(grouped).map(([cat, catBlocks]) => (
<div key={cat} className="tree-node">
<div className="tree-item tree-item-folder" onClick={() => toggleCat(cat)}>
<span className="tree-toggle">{expandedCats.has(cat) ? '▾' : '▸'}</span>
<span className="tree-icon">📁</span>
<span className="tree-label">{cat}</span>
<span className="tree-count">{catBlocks.length}</span>
</div>
{expandedCats.has(cat) && (
<div className="tree-children">
{catBlocks.map((block) => (
<div
key={block.id}
className="tree-item tree-item-leaf draggable"
draggable
onDragStart={(e) => handleDragStart(e, block.id)}
title={block.description}
>
<span className="tree-icon">📦</span>
<span className="tree-label">{block.name}</span>
<span className="tree-actions" onClick={(e) => e.stopPropagation()}>
{onRenameBlock && (
<button className="layer-action-btn" title="Umbenennen" onClick={() => { const name = window.prompt('Neuer Name:', block.name); if (name) onRenameBlock(block.id, name); }}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
</button>
)}
{onDuplicateBlock && (
<button className="layer-action-btn" title="Duplizieren" onClick={() => onDuplicateBlock(block.id)}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
)}
{onDeleteBlock && (
<button className="layer-action-btn" title="Löschen" onClick={() => onDeleteBlock(block.id)}>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
</button>
)}
</span>
</div>
))}
</div>
)}
</div>
))}
{filteredBlocks.length === 0 && (
<div className="lib-empty">Keine Blöcke gefunden</div>
)}
</div>
</>
);
};
export default BlockLibrary;