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 | |