Ich habe Gemini die Hauptattribute einer Kategorie (Name, Farbe, Tags usw.) gegeben und gefragt: “Kannst du ein JS/HTML5-Tool ausgeben, um Kategorien mit diesen Attributen zu erstellen und zu organisieren? Wir sollten auch schnell bearbeiten und neu anordnen können”.
Das erstellte Tool war bereits großartig, aber ich habe es verfeinert, indem ich bat, Attribute und Funktionen hinzuzufügen/entfernen sowie das Design anzupassen.
Wahrscheinlich. Du kannst den Code an Gemini senden (verwende den Canvas-Modus) und ihn bitten, ihn so anzupassen, wie du möchtest:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Discourse Kategorie-Planer</title>
<style>
/* Basis-Styling */
body { font-family: 'Inter', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; }
h1 { border-bottom: 2px solid #e9ecef; padding-bottom: 10px; margin-bottom: 20px; color: #343a40; }
/* Layout */
.main-container { display: flex; gap: 30px; }
@media (max-width: 900px) {
.main-container { flex-direction: column; }
}
.form-section { flex: 1.2; padding: 15px; border-radius: 8px; background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.list-section { flex: 1.8; padding: 15px; border-radius: 8px; background: #fff; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
/* Formular-Steuerelemente */
.form-row { display: flex; gap: 15px; margin-bottom: 10px; }
.form-group { flex: 1; display: flex; flex-direction: column; }
label { font-weight: 600; margin-bottom: 3px; font-size: 0.9em; color: #495057; }
input[type="text"], select, textarea { padding: 8px; border: 1px solid #ced4da; border-radius: 4px; }
/* Schaltflächen */
.action-button { padding: 10px 15px; border: none; border-radius: 44px; cursor: pointer; background: #007bff; color: white; margin-top: 10px; transition: background 0.2s; }
.action-button:hover { background: #0056b3; }
.utility-buttons { display: flex; gap: 10px; margin-top: 20px; }
.utility-buttons .action-button { background: #6c757d; }
.utility-buttons .action-button:hover { background: #5a6268; }
/* --- Farfeingabe-Styling --- */
input[type="color"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
border: 1px solid #ced4da;
padding: 0;
height: 38px;
cursor: pointer;
border-radius: 4px;
overflow: hidden;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 4px;
height: 100%;
display: block;
}
/* --- Ende Farfeingabe-Styling --- */
/* Stile für die Verwaltungsliste (Ziehbar) */
#category-list { list-style: none; padding: 0; margin-top: 15px; }
.category-item {
display: flex;
align-items: center;
padding: 12px;
border: 1px solid #dee2e6;
border-radius: 6px;
margin-bottom: 8px;
cursor: move;
background: #ffffff;
transition: box-shadow 0.2s;
}
.category-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.category-item.dragging { opacity: 0.6; border: 2px dashed #007bff; background: #eaf3ff; }
/* Einrückung von Unterkategorien in der Verwaltungsliste (Mittlere Spalte) */
.category-item.is-sub {
margin-left: 30px;
border-left: 3px solid #ced4da;
background-color: #f7f7f7;
}
/* --- STILE FÜR ZIEH-HINWEISE --- */
.category-item.drag-over { border-bottom: 2px solid #007bff; }
.category-item.indent-hint {
border-bottom: 2px solid #28a745;
box-shadow: 5px 0 0 0 #28a745 inset;
margin-left: 30px !important;
background-color: #eaf3ff;
}
.category-item.outdent-hint {
border-bottom: 2px solid #dc3545;
box-shadow: -5px 0 0 0 #dc3545 inset;
margin-left: 0 !important;
background-color: #ffeaea;
}
/* --- ENDE STILE FÜR ZIEH-HINWEISE --- */
.category-actions { flex-shrink: 0; display: flex; gap: 5px; }
/* --- Kategorie-Anzeigebereich (Gilt sowohl für Verwaltungsliste als auch Vorschau) --- */
.category-info, .preview-category {
flex-grow: 1;
display: flex;
align-items: flex-start;
padding: 0;
}
/* Wrapper für die farbigen Elemente (Vertikaler Balken + Quadrat + Text) */
.category-display-block {
display: flex;
flex-direction: column;
padding-left: 8px;
border-left: 4px solid transparent;
flex-grow: 1;
}
/* Wrapper für das Quadrat und den Namen/Text */
.square-and-text-wrapper {
display: flex;
align-items: flex-start;
}
/* Farbquadrat - border-radius entfernt */
.color-square {
width: 12px;
height: 12px;
border-radius: 0;
margin-right: 8px;
flex-shrink: 0;
border: 1px solid rgba(0,0,0,0.1);
margin-top: 3px;
}
/* Kategoriennamen verwenden Standardfarbe (dunkelgrau/schwarz) und sind fett */
.category-name {
font-size: 1em;
font-weight: 700;
color: #343a40;
}
.category-desc { font-size: 0.8em; color: #6c757d; }
/* --- Pill-Styling --- */
.pill-container, .preview-pill-wrapper {
margin-top: 5px;
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
/* Berechtigungen - Dezentleres Styling */
.management-permission-pill, .preview-permission-pill {
font-size: 0.7em;
font-weight: 600;
background: #e9ecef; /* Hellerer Hintergrund */
color: #495057; /* Dunklerer Text */
padding: 3px 6px;
border-radius: 4px;
text-transform: uppercase;
border: 1px solid #ced4da; /* Füge einen dezenten Rand hinzu */
}
.attr-pill, .preview-tag-pill { /* Tags */
font-size: 0.7em;
background: #f8f9fa; /* Sehr heller Hintergrund für weniger Betonung */
color: #495057;
padding: 3px 6px;
border-radius: 4px;
display: inline-block;
border: 1px solid #e9ecef; /* Dezenter Rand */
}
/* Stile für Bearbeiten/Löschen-Schaltflächen */
.edit-btn, .delete-btn {
background: #6c757d;
color: white;
padding: 5px 8px;
font-size: 0.75em;
border-radius: 4px;
border: none;
}
.delete-btn {
background: #dc3545;
}
</style>
</head>
<body>
<h1>📋 Discourse Kategorie- & Hierarchie-Planer</h1>
<div class="main-container">
<div class="form-section">
<h2>Kategorieattribute hinzufügen/bearbeiten</h2>
<form id="category-form">
<input type="hidden" id="category-id">
<div class="form-row">
<div class="form-group">
<label for="name">1. Name</label>
<input type="text" id="name" required oninput="updateLiveFormPreview()">
</div>
<div class="form-group">
<label for="color">2. Farbe</label>
<!-- Custom styled color input -->
<input type="color" id="color" value="#007bff" oninput="updateLiveFormPreview()">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="description">3. Beschreibung</label>
<textarea id="description" rows="2" oninput="updateLiveFormPreview()"></textarea>
</div>
<div class="form-group">
<label for="permissions">4. Berechtigungen (z.B. öffentlich, Personal, Gruppe)</label>
<input type="text" id="permissions" value="Öffentlich" oninput="updateLiveFormPreview()">
</div>
</div>
<div class="form-row">
<div class="form-group" style="flex: 2;">
<label for="tags">5. Tags (Komma-getrennt, z.B. "Bug, Feature")</label>
<input type="text" id="tags" placeholder="Optionale Tags für Beiträge in dieser Kategorie" oninput="updateLiveFormPreview()">
</div>
</div>
<input type="hidden" id="parentId" value="">
<div class="form-row">
<div class="form-group" style="flex: 2;">
<button type="submit" id="submit-btn" class="action-button">Neue Kategorie hinzufügen</button>
</div>
</div>
</form>
<div id="live-form-preview" style="padding: 10px; border: 1px dashed #ced4da; border-radius: 6px; background: #fefefe;">
<h3 style="margin-top: 0; font-size: 1em;">Vorschau</h3>
<div class="preview-category" style="padding: 10px; border: none; align-items: flex-start;">
<div class="category-display-block" id="preview-display-block" style="border-left-color: #007bff; padding-left: 8px;">
<div class="square-and-text-wrapper">
<div class="color-square" id="preview-color-bar-field" style="background-color: #007bff;"></div>
<div class="category-name" id="preview-name-field">[Kategoriename]</div>
</div>
<div class="category-desc" id="preview-desc-field" style="color: #6c757d;">[Beschreibung hier]</div>
<div class="preview-pill-wrapper" id="preview-pills-field">
<!-- Pills werden hier per JS eingefügt -->
</div>
</div>
</div>
</div>
<div class="utility-buttons">
<button id="export-btn" class="action-button">Exportieren (CSV)</button>
<button id="import-btn" class="action-button">Importieren (CSV)</button>
<input type="file" id="csv-file-input" accept=".csv" style="display: none;">
</div>
</div>
<div class="list-section">
<h2>Kategorieverwaltung (Zum Neuordnen ziehen)</h2>
<p style="font-size: 0.85em; color: #6c757d;">
Ziehe ein Element, um es neu zu ordnen. Ziehe nach **rechts**, um eine Unterkategorie zu erstellen (Grüner Hinweis), ziehe nach **links**, um zu befördern (Roter Hinweis).
</p>
<ul id="category-list">
<!-- Kategorien werden hier per JavaScript gerendert -->
</ul>
<ul id="live-preview-list" style="display: none;"></ul>
</div>
</div>
<script>
let categories = [];
let dragStartX = 0;
const form = document.getElementById('category-form');
const list = document.getElementById('category-list');
const previewList = document.getElementById('live-preview-list');
const submitBtn = document.getElementById('submit-btn');
const exportBtn = document.getElementById('export-btn');
const importBtn = document.getElementById('import-btn');
const csvFileInput = document.getElementById('csv-file-input');
// --- CSV Export/Import Handler ---
function arrayToCsv(data) {
const headers = ['id', 'name', 'description', 'color', 'permissions', 'tags', 'parentId'];
let csv = headers.join(',') + '\n';
data.forEach(row => {
const values = headers.map(header => {
let value = row[header] || '';
// Escape commas and quotes for CSV format
if (typeof value === 'string') {
value = value.replace(/"/g, '""');
if (value.includes(',') || value.includes('\n')) {
value = `"${value}"`;
}
}
return value;
});
csv += values.join(',') + '\n';
});
return csv;
}
function csvToArray(csv) {
const lines = csv.split('\n').filter(line => line.trim() !== '');
if (lines.length < 2) return [];
const headers = lines[0].split(',');
const data = [];
for (let i = 1; i < lines.length; i++) {
const values = lines[i].match(/(".*?"|[^",\s]+)(?=\s*,|\s*$)/g) || lines[i].split(',');
if (values.length === headers.length) {
let row = {};
for (let j = 0; j < headers.length; j++) {
let value = values[j].trim();
if (value.startsWith('"') && value.endsWith('"')) {
value = value.substring(1, value.length - 1).replace(/""/g, '"');
}
row[headers[j]] = value;
}
data.push(row);
}
}
return data;
}
exportBtn.addEventListener('click', () => {
const csvData = arrayToCsv(categories);
const blob = new Blob([csvData], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
if (link.download !== undefined) {
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', 'discourse_categories.csv');
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} else {
alert('Export wird in diesem Browser nicht unterstützt.');
}
});
importBtn.addEventListener('click', () => {
csvFileInput.click();
});
csvFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const csvContent = e.target.result;
const importedData = csvToArray(csvContent);
if (importedData.length > 0) {
categories = importedData;
renderAll();
alert(`${importedData.length} Kategorien erfolgreich importiert.`);
} else {
alert('Fehler beim Importieren der CSV: Daten sind leer oder falsch formatiert.');
}
};
reader.readAsText(file);
});
// --- Core Data Management & Rendering ---
const findParent = (parentId) => categories.find(c => c.id === parentId);
// Recursive function to find all descendants of a category ID
function getDescendantIds(parentId) {
let descendants = [];
const children = categories.filter(c => c.parentId === parentId);
children.forEach(child => {
descendants.push(child.id);
descendants = descendants.concat(getDescendantIds(child.id));
});
return descendants;
}
// Function to update the simple preview box next to the form
function updateLiveFormPreview() {
const name = document.getElementById('name').value || '[Kategoriename]';
const description = document.getElementById('description').value || '[Beschreibung hier]';
const color = document.getElementById('color').value;
const permissions = document.getElementById('permissions').value;
const tagsInput = document.getElementById('tags').value;
// Apply color to the square and left border
document.getElementById('preview-color-bar-field').style.backgroundColor = color;
document.getElementById('preview-display-block').style.borderLeftColor = color;
document.getElementById('preview-name-field').textContent = name;
document.getElementById('preview-desc-field').textContent = description;
// Generate Pills HTML
let pillsHtml = `<span class="preview-permission-pill">${permissions}</span>`;
if (tagsInput) {
const tagsArray = tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag.length > 0);
tagsArray.forEach(tag => {
pillsHtml += `<span class="preview-tag-pill">${tag}</span>`;
});
}
document.getElementById('preview-pills-field').innerHTML = pillsHtml;
}
// Function to render the management list
function renderAll() {
list.innerHTML = '';
previewList.innerHTML = '';
let finalOrder = [];
const parentCategories = categories.filter(c => !c.parentId);
const subCategories = categories.filter(c => c.parentId);
parentCategories.sort((a, b) => categories.indexOf(a) - categories.indexOf(b)).forEach(parent => {
finalOrder.push(parent);
const children = subCategories.filter(c => c.parentId === parent.id);
children.sort((a, b) => categories.indexOf(a) - categories.indexOf(b)).forEach(child => {
finalOrder.push(child);
});
});
subCategories.filter(c => !findParent(c.parentId)).forEach(orphan => {
finalOrder.push(orphan);
});
finalOrder.forEach(cat => {
const isSub = !!cat.parentId;
// --- 1. Render Management List Item (Draggable) ---
const managementItem = document.createElement('li');
managementItem.className = 'category-item';
if (isSub) {
managementItem.classList.add('is-sub');
}
managementItem.setAttribute('data-id', cat.id);
managementItem.setAttribute('draggable', true);
// Build tags HTML
const tagsHtml = cat.tags ? cat.tags.split(',').map(tag => `<span class="attr-pill">${tag.trim()}</span>`).join('') : '';
managementItem.innerHTML = `
<div class="category-info">
<div class="category-display-block" style="border-left-color: ${cat.color};">
<div class="square-and-text-wrapper">
<span class="color-square" style="background-color: ${cat.color};"></span>
<div class="category-name">${cat.name} ${isSub ? '— Unterkategorie' : ''}</div>
</div>
<div class="category-desc">${cat.description || ''}</div>
<div class="pill-container">
<span class="management-permission-pill">${cat.permissions}</span>
${tagsHtml}
</div>
</div>
</div>
<div class="category-actions">
<button class="edit-btn" onclick="editCategory('${cat.id}')">Bearbeiten</button>
<button class="delete-btn" onclick="deleteCategory('${cat.id}')">Löschen</button>
</div>
`;
list.appendChild(managementItem);
});
addDragDropListeners();
}
// Function to handle form submission (Add or Update)
form.addEventListener('submit', (e) => {
e.preventDefault();
const idField = document.getElementById('category-id');
const id = idField.value;
const newCategory = {
id: id || 'cat_' + Date.now(),
name: document.getElementById('name').value,
description: document.getElementById('description').value,
color: document.getElementById('color').value,
permissions: document.getElementById('permissions').value,
tags: document.getElementById('tags').value.trim(),
parentId: '',
};
if (id) {
const index = categories.findIndex(c => c.id === id);
if (index !== -1) {
newCategory.parentId = categories[index].parentId;
categories[index] = newCategory;
}
} else {
categories.push(newCategory);
}
form.reset();
idField.value = '';
submitBtn.textContent = 'Neue Kategorie hinzufügen';
updateLiveFormPreview();
renderAll();
});
// Function to populate the form for editing
function editCategory(id) {
const cat = categories.find(c => c.id === id);
if (!cat) return;
document.getElementById('category-id').value = cat.id;
document.getElementById('name').value = cat.name;
document.getElementById('description').value = cat.description;
document.getElementById('color').value = cat.color;
document.getElementById('permissions').value = cat.permissions;
document.getElementById('tags').value = cat.tags || '';
submitBtn.textContent = 'Kategorie aktualisieren';
updateLiveFormPreview();
window.scrollTo(0, 0);
}
// Function to delete a category
function deleteCategory(id) {
// Removed confirm() call to allow execution in sandboxed environment
categories = categories.filter(c => c.id !== id && c.parentId !== id);
renderAll();
}
// --- Drag and Drop Logic for Rearrangement and Nesting ---
let draggedItem = null;
function removeAllDragHints() {
document.querySelectorAll('.category-item').forEach(li => {
li.classList.remove('indent-hint', 'outdent-hint', 'drag-over');
const cat = categories.find(c => c.id === li.dataset.id);
if (cat) {
li.style.marginLeft = cat.parentId ? '30px' : '0';
}
});
}
function addDragDropListeners() {
document.querySelectorAll('.category-item').forEach(item => {
item.addEventListener('dragstart', (e) => {
draggedItem = item;
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', item.dataset.id);
dragStartX = e.clientX; // Capture starting X position
setTimeout(() => item.classList.add('dragging'), 0);
});
item.addEventListener('dragend', () => {
draggedItem.classList.remove('dragging');
draggedItem = null;
removeAllDragHints();
});
item.addEventListener('dragleave', (e) => {
e.currentTarget.classList.remove('indent-hint', 'outdent-hint', 'drag-over');
});
item.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const dropTargetElement = e.currentTarget;
const dropTargetId = dropTargetElement.dataset.id;
const dropTargetCat = categories.find(c => c.id === dropTargetId);
const draggedCat = categories.find(c => c.id === draggedItem.dataset.id);
if (draggedItem === dropTargetElement) return;
removeAllDragHints();
dropTargetElement.classList.add('drag-over');
const INDENT_THRESHOLD_PX = 40;
const OUTDENT_SHIFT_PX = 50;
const currentX = e.clientX;
const horizontalShift = currentX - dragStartX;
// --- LOGIC FOR HINT APPLICATION ---
// 1. Check for INDENT (Drag Right)
if (horizontalShift > INDENT_THRESHOLD_PX) {
// Check if the target can accept children (i.e., is not already a child)
if (!dropTargetCat.parentId) {
dropTargetElement.classList.add('indent-hint');
dropTargetElement.classList.remove('drag-over');
}
}
// 2. Check for OUTDENT (Drag Left)
else if (draggedCat.parentId && horizontalShift < -OUTDENT_SHIFT_PX) {
dropTargetElement.classList.add('outdent-hint');
dropTargetElement.classList.remove('drag-over');
}
// 3. Middle Ground: No hint means only vertical reordering occurs on drop.
});
item.addEventListener('drop', (e) => {
e.preventDefault();
if (draggedItem !== e.currentTarget) {
const dropTargetElement = e.currentTarget;
const dropTargetId = dropTargetElement.dataset.id;
const draggedId = draggedItem.dataset.id;
let draggedIndex = categories.findIndex(c => c.id === draggedId);
let dropIndex = categories.findIndex(c => c.id === dropTargetId);
// 1. Re-sequence the categories array (Linear Order)
if (draggedIndex !== -1 && dropIndex !== -1) {
const [temp] = categories.splice(draggedIndex, 1);
categories.splice(dropIndex, 0, temp);
}
// Re-find indices after resequencing
const newDraggedIndex = categories.findIndex(c => c.id === draggedId);
const draggedCat = categories[newDraggedIndex];
// 2. Determine New Parent ID based on the active hint
let newParentId = draggedCat.parentId; // Default: Keep current hierarchy status
let updateDescendants = false;
if (dropTargetElement.classList.contains('indent-hint')) {
// Promoting to child: The dragged category's new parent is the drop target.
newParentId = dropTargetId;
updateDescendants = true;
} else if (dropTargetElement.classList.contains('outdent-hint')) {
// Promoting to parent: The dragged category's new parent is null/empty.
newParentId = '';
updateDescendants = true;
}
draggedCat.parentId = newParentId;
// 3. Update Descendants if hierarchy changed for the dragged parent
if (updateDescendants) {
const descendantIds = getDescendantIds(draggedId);
descendantIds.forEach(descendantId => {
const descendantCat = categories.find(c => c.id === descendantId);
if (descendantCat) {
// Descendants follow the dragged parent's new status
descendantCat.parentId = newParentId;
}
});
}
removeAllDragHints();
renderAll();
}
});
});
}
// --- Initial Load (Updated Categories) ---
categories.push({
id: 'cat_001', name: 'Allgemein', description: 'Allgemeine Community-Diskussion und Hilfe.', color: '#007bff', permissions: 'Öffentlich', parentId: '', tags: 'Tag 1, Tag 2'
});
categories.push({
id: 'cat_004', name: 'Unterkategorie-Beispiel', description: '', color: '#007bff', permissions: 'Öffentlich', parentId: 'cat_001', tags: 'Tag 3'
});
categories.push({
id: 'cat_002', name: 'Personal', description: 'Privater Bereich für Moderation und Administration.', color: '#ffc107', permissions: 'Nur Personal', parentId: '', tags: ''
});
categories.push({
id: 'cat_003', name: 'Website-Feedback', description: 'Diskutiere das Forum selbst und schlage Verbesserungen vor.', color: '#28a745', permissions: 'Öffentlich', parentId: '', tags: 'Website, Feedback'
});
renderAll();
updateLiveFormPreview();
</script>
</body>
</html>