feat: enhance BlockLibrary with create, delete, search and category features

This commit is contained in:
2026-06-22 20:59:35 +00:00
parent 79ef1597a4
commit ffea270fbc
+251 -15
View File
@@ -1,32 +1,112 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import api from '../services/api';
const BlockLibrary = ({ canvasRef }) => {
const BlockLibrary = ({ canvasRef, yjsDoc }) => {
const [blocks, setBlocks] = useState([]);
const [categories, setCategories] = useState([]);
const [activeCategory, setActiveCategory] = useState('Alle');
const [loading, setLoading] = useState(true);
const [selectedElements, setSelectedElements] = useState([]);
const [newBlockName, setNewBlockName] = useState('');
const [newBlockCategory, setNewBlockCategory] = useState('');
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
// Load blocks on component mount
useEffect(() => {
api.get('/blocks')
.then(res => {
const data = res.data || [];
setBlocks(data);
const cats = ['Alle', ...new Set(data.map(b => b.category || 'Allgemein'))];
setCategories(cats);
setLoading(false);
})
.catch(() => setLoading(false));
loadBlocks();
}, []);
const filteredBlocks = activeCategory === 'Alle'
? blocks
: blocks.filter(b => (b.category || 'Allgemein') === activeCategory);
const loadBlocks = async () => {
try {
const res = await api.get('/blocks');
const data = res.data || [];
setBlocks(data);
// Extract unique categories
const cats = ['Alle', ...new Set(data.map(b => b.category || 'Allgemein'))];
setCategories(cats);
} catch (error) {
console.error('Failed to load blocks:', error);
} finally {
setLoading(false);
}
};
// Filter blocks by category and search query
const filteredBlocks = blocks.filter(block => {
const matchesCategory = activeCategory === 'Alle' || (block.category || 'Allgemein') === activeCategory;
const matchesSearch = block.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(block.category || '').toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
// Handle creating a new block from selected elements
const handleCreateBlock = async () => {
if (!newBlockName.trim() || selectedElements.length === 0) return;
try {
const response = await api.post('/blocks', {
name: newBlockName,
category: newBlockCategory || 'Allgemein',
elements: selectedElements
});
// Add to Yjs document
if (yjsDoc) {
const blockId = response.data.id;
yjsDoc.blocks.set(blockId, response.data);
}
// Refresh blocks list
loadBlocks();
// Reset form
setNewBlockName('');
setNewBlockCategory('');
setShowCreateDialog(false);
setSelectedElements([]);
} catch (error) {
console.error('Failed to create block:', error);
alert('Fehler beim Erstellen des Blocks');
}
};
// Handle deleting a block
const handleDeleteBlock = async (blockId, blockName) => {
if (!window.confirm(`Sind Sie sicher, dass Sie den Block "${blockName}" löschen möchten?`)) {
return;
}
try {
await api.delete(`/blocks/${blockId}`);
// Remove from Yjs document
if (yjsDoc) {
yjsDoc.blocks.delete(blockId);
}
// Refresh blocks list
loadBlocks();
} catch (error) {
console.error('Failed to delete block:', error);
alert('Fehler beim Löschen des Blocks');
}
};
// Handle inserting a block instance on canvas
const handleInsertBlock = (block) => {
if (canvasRef.current) {
canvasRef.current.addSvgObject(block.svg_data);
}
};
// Handle drag start for block insertion
const handleDragStart = (e, block) => {
e.dataTransfer.setData('text/plain', block.svg_data);
};
// Handle drop on canvas
const handleDrop = (e) => {
e.preventDefault();
const svgData = e.dataTransfer.getData('text/plain');
@@ -47,6 +127,23 @@ const BlockLibrary = ({ canvasRef }) => {
onDrop={handleDrop}
onDragOver={handleDragOver}
>
{/* Search bar */}
<div style={{ marginBottom: 8 }}>
<input
type="text"
placeholder="Blöcke suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
width: '100%',
padding: '4px 8px',
fontSize: 12,
border: '1px solid #ddd',
borderRadius: 4
}}
/>
</div>
{/* Category filter */}
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginBottom: 8 }}>
{categories.map(cat => (
@@ -67,6 +164,26 @@ const BlockLibrary = ({ canvasRef }) => {
</button>
))}
</div>
{/* Create block button */}
<div style={{ marginBottom: 8 }}>
<button
onClick={() => setShowCreateDialog(true)}
style={{
width: '100%',
padding: '6px',
fontSize: 12,
background: '#52c41a',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
+ Neuer Block
</button>
</div>
{/* Block grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 6 }}>
{filteredBlocks.map(block => (
@@ -74,6 +191,7 @@ const BlockLibrary = ({ canvasRef }) => {
key={block.id}
draggable
onDragStart={(e) => handleDragStart(e, block)}
onClick={() => handleInsertBlock(block)}
style={{
border: '1px solid #ddd',
borderRadius: 4,
@@ -81,6 +199,7 @@ const BlockLibrary = ({ canvasRef }) => {
background: 'white',
cursor: 'grab',
textAlign: 'center',
position: 'relative',
}}
title={block.name}
>
@@ -91,11 +210,128 @@ const BlockLibrary = ({ canvasRef }) => {
<div style={{ fontSize: 10, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{block.name}
</div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteBlock(block.id, block.name);
}}
style={{
position: 'absolute',
top: 2,
right: 2,
width: 16,
height: 16,
fontSize: 10,
background: '#ff4d4f',
color: 'white',
border: 'none',
borderRadius: '50%',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
×
</button>
</div>
))}
</div>
{filteredBlocks.length === 0 && (
<p style={{ fontSize: 12, color: '#999' }}>Keine Blöcke in dieser Kategorie.</p>
<p style={{ fontSize: 12, color: '#999' }}>Keine Blöcke gefunden.</p>
)}
{/* Create Block Dialog */}
{showCreateDialog && (
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}>
<div style={{
background: 'white',
padding: 20,
borderRadius: 8,
width: 300,
}}>
<h3 style={{ marginTop: 0 }}>Neuen Block erstellen</h3>
<div style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>Name:</label>
<input
type="text"
value={newBlockName}
onChange={(e) => setNewBlockName(e.target.value)}
style={{
width: '100%',
padding: '6px',
fontSize: 12,
border: '1px solid #ddd',
borderRadius: 4,
}}
/>
</div>
<div style={{ marginBottom: 16 }}>
<label style={{ display: 'block', marginBottom: 4, fontSize: 12 }}>Kategorie:</label>
<input
type="text"
value={newBlockCategory}
onChange={(e) => setNewBlockCategory(e.target.value)}
placeholder="Allgemein"
style={{
width: '100%',
padding: '6px',
fontSize: 12,
border: '1px solid #ddd',
borderRadius: 4,
}}
/>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button
onClick={handleCreateBlock}
disabled={!newBlockName.trim()}
style={{
flex: 1,
padding: '8px',
fontSize: 12,
background: '#1890ff',
color: 'white',
border: 'none',
borderRadius: 4,
cursor: 'pointer',
}}
>
Erstellen
</button>
<button
onClick={() => {
setShowCreateDialog(false);
setNewBlockName('');
setNewBlockCategory('');
}}
style={{
flex: 1,
padding: '8px',
fontSize: 12,
background: '#f0f0f0',
border: '1px solid #ddd',
borderRadius: 4,
cursor: 'pointer',
}}
>
Abbrechen
</button>
</div>
</div>
</div>
)}
</div>
);