Spaces:
Runtime error
Runtime error
import React, { useState, useEffect, useCallback } from 'react'; | |
import { API, APIHandler, Validators } from './api.js'; | |
const CategoryManager = () => { | |
const [categories, setCategories] = useState([]); | |
const [newCategoryName, setNewCategoryName] = useState(''); | |
const [loading, setLoading] = useState(true); | |
useEffect(() => { | |
fetchCategories(); | |
}, []); | |
const fetchCategories = async () => { | |
try { | |
const response = await fetch('/backend/categories'); | |
const data = await response.json(); | |
setCategories(data); | |
} catch (error) { | |
console.error('Fehler beim Laden der Kategorien:', error); | |
} finally { | |
setLoading(false); | |
} | |
}; | |
const handleSubmit = async (e) => { | |
e.preventDefault(); | |
try { | |
const response = await fetch('/create_category', { | |
method: 'POST', | |
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, | |
body: `name=${encodeURIComponent(newCategoryName)}` | |
}); | |
if (!response.ok) throw new Error('Fehler beim Erstellen'); | |
await fetchCategories(); | |
setNewCategoryName(''); | |
} catch (error) { | |
console.error('Fehler:', error); | |
} | |
}; | |
const handleDelete = async (id) => { | |
if (!confirm('Kategorie wirklich löschen?')) return; | |
try { | |
const response = await fetch(`/delete_category/${id}`, { | |
method: 'DELETE' | |
}); | |
if (!response.ok) throw new Error('Fehler beim Löschen'); | |
await fetchCategories(); | |
} catch (error) { | |
console.error('Fehler:', error); | |
} | |
}; | |
if (loading) return <div>Lade...</div>; | |
return ( | |
<div className="card-body"> | |
<form onSubmit={handleSubmit} className="mb-3"> | |
<div className="input-group"> | |
<input | |
type="text" | |
className="form-control" | |
value={newCategoryName} | |
onChange={(e) => setNewCategoryName(e.target.value)} | |
placeholder="Neue Kategorie" | |
required | |
/> | |
<button type="submit" className="btn btn-primary">Erstellen</button> | |
</div> | |
</form> | |
<div className="table-responsive"> | |
<table className="table table-hover"> | |
<thead> | |
<tr> | |
<th>Name</th> | |
<th>Bilder</th> | |
<th>Aktionen</th> | |
</tr> | |
</thead> | |
<tbody> | |
{categories.map(category => ( | |
<tr key={category.id}> | |
<td>{category.name}</td> | |
<td>{category.imageCount}</td> | |
<td> | |
<button | |
onClick={() => handleDelete(category.id)} | |
className="btn btn-sm btn-outline-danger" | |
> | |
Löschen | |
</button> | |
</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
</div> | |
); | |
}; | |
export default CategoryManager; | |
### END: category-manager.txt | |
### START: category-manager-updated.txt | |
import React, { useState, useEffect, useCallback } from 'react'; | |
import { API, APIHandler, Validators } from '@/api'; | |
const CategoryManager = () => { | |
const [categories, setCategories] = useState([]); | |
const [loading, setLoading] = useState(true); | |
const [error, setError] = useState(null); | |
const [selectedCategories, setSelectedCategories] = useState(new Set()); | |
const [editingCategory, setEditingCategory] = useState(null); | |
const [searchTerm, setSearchTerm] = useState(''); | |
const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' }); | |
// Daten laden | |
const fetchCategories = useCallback(async () => { | |
try { | |
setLoading(true); | |
const data = await APIHandler.get(API.categories.list); | |
setCategories(data); | |
setError(null); | |
} catch (err) { | |
setError('Fehler beim Laden der Kategorien: ' + err.message); | |
console.error('Fetch error:', err); | |
} finally { | |
setLoading(false); | |
} | |
}, []); | |
useEffect(() => { | |
fetchCategories(); | |
}, [fetchCategories]); | |
// Sortierung | |
const sortedCategories = React.useMemo(() => { | |
const sorted = [...categories]; | |
sorted.sort((a, b) => { | |
if (sortConfig.key === 'imageCount') { | |
return sortConfig.direction === 'asc' | |
? a.imageCount - b.imageCount | |
: b.imageCount - a.imageCount; | |
} | |
return sortConfig.direction === 'asc' | |
? a[sortConfig.key].localeCompare(b[sortConfig.key]) | |
: b[sortConfig.key].localeCompare(a[sortConfig.key]); | |
}); | |
return sorted; | |
}, [categories, sortConfig]); | |
// Kategorie erstellen | |
const handleCreate = async (name) => { | |
try { | |
//const validationErrors = Validators.category({ name }); | |
//if (Object.keys(validationErrors).length > 0) { | |
// throw new Error(Object.values(validationErrors).join(', ')); | |
//} | |
await APIHandler.post(API.categories.create, { name }); | |
await fetchCategories(); | |
window.showToast('Erfolg', 'Kategorie wurde erstellt', 'success'); | |
} catch (err) { | |
window.showToast('Fehler', err.message, 'danger'); | |
} | |
}; | |
// Kategorie löschen | |
const handleDelete = async (id) => { | |
if (!window.confirm('Möchten Sie diese Kategorie wirklich löschen?')) return; | |
try { | |
await APIHandler.delete(API.categories.delete(id)); | |
await fetchCategories(); | |
window.showToast('Erfolg', 'Kategorie wurde gelöscht', 'success'); | |
} catch (err) { | |
window.showToast('Fehler', err.message, 'danger'); | |
} | |
}; | |
// Kategorie aktualisieren | |
const handleUpdate = async (id, updates) => { | |
try { | |
//const validationErrors = Validators.category(updates); | |
//if (Object.keys(validationErrors).length > 0) { | |
// throw new Error(Object.values(validationErrors).join(', ')); | |
//} | |
await APIHandler.put(API.categories.update(id), updates); | |
await fetchCategories(); | |
setEditingCategory(null); | |
window.showToast('Erfolg', 'Kategorie wurde aktualisiert', 'success'); | |
} catch (err) { | |
window.showToast('Fehler', err.message, 'danger'); | |
} | |
}; | |
// Massenbearbeitung | |
const handleBulkDelete = async () => { | |
if (selectedCategories.size === 0) return; | |
if (!window.confirm(`Möchten Sie ${selectedCategories.size} Kategorien wirklich löschen?`)) return; | |
try { | |
await Promise.all( | |
Array.from(selectedCategories).map(id => | |
APIHandler.delete(API.categories.delete(id)) | |
) | |
); | |
setSelectedCategories(new Set()); | |
await fetchCategories(); | |
window.showToast('Erfolg', 'Ausgewählte Kategorien wurden gelöscht', 'success'); | |
} catch (err) { | |
window.showToast('Fehler', err.message, 'danger'); | |
} | |
}; | |
// Massenbearbeitung - Kategorien zusammenführen | |
const handleMergeCategories = async (targetId) => { | |
const selectedIds = Array.from(selectedCategories); | |
if (selectedIds.length < 2) return; | |
try { | |
await APIHandler.post(API.categories.merge, { | |
targetId, | |
sourceIds: selectedIds.filter(id => id !== targetId) | |
}); | |
setSelectedCategories(new Set()); | |
await fetchCategories(); | |
window.showToast('Erfolg', 'Kategorien wurden zusammengeführt', 'success'); | |
} catch (err) { | |
window.showToast('Fehler', err.message, 'danger'); | |
} | |
}; | |
const handleSelectAll = (event) => { | |
if (event.target.checked) { | |
setSelectedCategories(new Set(categories.map(cat => cat.id))); | |
} else { | |
setSelectedCategories(new Set()); | |
} | |
}; | |
const handleSort = (key) => { | |
setSortConfig(current => ({ | |
key, | |
direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc' | |
})); | |
}; | |
if (loading) { | |
return ( | |
<div className="d-flex justify-content-center p-5"> | |
<div className="spinner-border text-primary" role="status"> | |
<span className="visually-hidden">Laden...</span> | |
</div> | |
</div> | |
); | |
} | |
if (error) { | |
return ( | |
<div className="alert alert-danger m-3" role="alert"> | |
<h4 className="alert-heading">Fehler</h4> | |
<p>{error}</p> | |
<button | |
className="btn btn-outline-danger" | |
onClick={fetchCategories} | |
> | |
Erneut versuchen | |
</button> | |
</div> | |
); | |
} | |
// Filtern basierend auf Suchbegriff | |
const filteredCategories = searchTerm | |
? sortedCategories.filter(cat => | |
cat.name.toLowerCase().includes(searchTerm.toLowerCase()) | |
) | |
: sortedCategories; | |
return ( | |
<div className="card-body"> | |
{/* Toolbar */} | |
<div className="d-flex flex-wrap justify-content-between mb-3 gap-2"> | |
<div className="d-flex gap-2"> | |
<button | |
className="btn btn-primary" | |
onClick={() => setEditingCategory({ name: '' })} | |
> | |
<i className="bi bi-plus-lg"></i> Neue Kategorie | |
</button> | |
{selectedCategories.size > 0 && ( | |
<div className="btn-group"> | |
<button | |
className="btn btn-danger" | |
onClick={handleBulkDelete} | |
> | |
<i className="bi bi-trash"></i> | |
{selectedCategories.size} löschen | |
</button> | |
{selectedCategories.size > 1 && ( | |
<button | |
className="btn btn-warning" | |
onClick={() => { | |
const firstId = Array.from(selectedCategories)[0]; | |
handleMergeCategories(firstId); | |
}} | |
> | |
<i className="bi bi-arrow-join"></i> | |
Zusammenführen | |
</button> | |
)} | |
</div> | |
)} | |
</div> | |
<div className="flex-grow-1 max-w-xs"> | |
<input | |
type="search" | |
className="form-control" | |
placeholder="Kategorien durchsuchen..." | |
value={searchTerm} | |
onChange={(e) => setSearchTerm(e.target.value)} | |
/> | |
</div> | |
</div> | |
{/* Kategorie Liste */} | |
<div className="table-responsive"> | |
<table className="table table-hover"> | |
<thead> | |
<tr> | |
<th> | |
<input | |
type="checkbox" | |
className="form-check-input" | |
onChange={handleSelectAll} | |
checked={selectedCategories.size === categories.length} | |
/> | |
</th> | |
<th | |
className="cursor-pointer" | |
onClick={() => handleSort('name')} | |
> | |
Name {sortConfig.key === 'name' && ( | |
<i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i> | |
)} | |
</th> | |
<th | |
className="cursor-pointer" | |
onClick={() => handleSort('imageCount')} | |
> | |
Bilder {sortConfig.key === 'imageCount' && ( | |
<i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i> | |
)} | |
</th> | |
<th>Aktionen</th> | |
</tr> | |
</thead> | |
<tbody> | |
{filteredCategories.map(category => ( | |
<tr key={category.id}> | |
<td> | |
<input | |
type="checkbox" | |
className="form-check-input" | |
checked={selectedCategories.has(category.id)} | |
onChange={(e) => { | |
const newSelected = new Set(selectedCategories); | |
if (e.target.checked) { | |
newSelected.add(category.id); | |
} else { | |
newSelected.delete(category.id); | |
} | |
setSelectedCategories(newSelected); | |
}} | |
/> | |
</td> | |
<td> | |
{editingCategory?.id === category.id ? ( | |
<input | |
type="text" | |
className="form-control" | |
value={editingCategory.name} | |
onChange={(e) => setEditingCategory({ | |
...editingCategory, | |
name: e.target.value | |
})} | |
onKeyPress={(e) => { | |
if (e.key === 'Enter') { | |
handleUpdate(category.id, editingCategory); | |
} | |
}} | |
/> | |
) : category.name} | |
</td> | |
<td>{category.imageCount}</td> | |
<td> | |
<div className="btn-group"> | |
{editingCategory?.id === category.id ? ( | |
<> | |
<button | |
className="btn btn-sm btn-success" | |
onClick={() => handleUpdate(category.id, editingCategory)} | |
> | |
<i className="bi bi-check"></i> | |
</button> | |
<button | |
className="btn btn-sm btn-secondary" | |
onClick={() => setEditingCategory(null)} | |
> | |
<i className="bi bi-x"></i> | |
</button> | |
</> | |
) : ( | |
<> | |
<button | |
className="btn btn-sm btn-outline-primary" | |
onClick={() => setEditingCategory(category)} | |
> | |
<i className="bi bi-pencil"></i> | |
</button> | |
<button | |
className="btn btn-sm btn-outline-danger" | |
onClick={() => handleDelete(category.id)} | |
> | |
<i className="bi bi-trash"></i> | |
</button> | |
</> | |
)} | |
</div> | |
</td> | |
</tr> | |
))} | |
</tbody> | |
</table> | |
</div> | |
{filteredCategories.length === 0 && ( | |
<div className="text-center text-muted p-5"> | |
<i className="bi bi-tags display-4"></i> | |
<p className="mt-3"> | |
{searchTerm | |
? 'Keine Kategorien gefunden' | |
: 'Keine Kategorien vorhanden'} | |
</p> | |
</div> | |
)} | |
</div> | |
); | |
}; | |
export default CategoryManager; | |
window.CategoryManager = CategoryManager; // <-- Diese Zeile ans Ende setzen | |