2026-06-26 10:50:24 +02:00
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 ] ) = > (
2026-06-27 14:12:37 +02:00
< div key = { cat } className = "lib-tree-node" >
2026-06-26 10:50:24 +02:00
< 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 ;