feat: enhance BlockLibrary with create, delete, search and category features
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user