142 lines
7.1 KiB
TypeScript
142 lines
7.1 KiB
TypeScript
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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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" strokeWidth="2" strokeLinecap="round" strokeLinejoin="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="lib-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;
|