Better ui tag system
This commit is contained in:
		@@ -8,8 +8,9 @@
 | 
				
			|||||||
  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
					  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
 | 
					  <!-- Toast container for notifications -->
 | 
				
			||||||
 | 
					  <div id="toast-container"></div>
 | 
				
			||||||
  <h1>Photo WebUI</h1>
 | 
					  <h1>Photo WebUI</h1>
 | 
				
			||||||
 | 
					 | 
				
			||||||
  <!-- Toolbar with refresh and save buttons -->
 | 
					  <!-- Toolbar with refresh and save buttons -->
 | 
				
			||||||
  <div class="toolbar">
 | 
					  <div class="toolbar">
 | 
				
			||||||
    <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
 | 
					    <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,14 @@
 | 
				
			|||||||
// --- Arrays to store gallery and hero images ---
 | 
					// --- Arrays to store gallery and hero images ---
 | 
				
			||||||
let galleryImages = [];
 | 
					let galleryImages = [];
 | 
				
			||||||
let heroImages = [];
 | 
					let heroImages = [];
 | 
				
			||||||
 | 
					let allTags = []; // global tag list
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Load images from server on page load ---
 | 
					// --- Load images from server on page load ---
 | 
				
			||||||
async function loadData() {
 | 
					async function loadData() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    const galleryRes = await fetch('/api/gallery');
 | 
					    const galleryRes = await fetch('/api/gallery');
 | 
				
			||||||
    galleryImages = await galleryRes.json();
 | 
					    galleryImages = await galleryRes.json();
 | 
				
			||||||
 | 
					    updateAllTags();
 | 
				
			||||||
    renderGallery();
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const heroRes = await fetch('/api/hero');
 | 
					    const heroRes = await fetch('/api/hero');
 | 
				
			||||||
@@ -14,10 +16,20 @@ async function loadData() {
 | 
				
			|||||||
    renderHero();
 | 
					    renderHero();
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err);
 | 
				
			||||||
    alert("Error loading images!");
 | 
					    showToast("Error loading images!", "error");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Update global tag list from galleryImages ---
 | 
				
			||||||
 | 
					function updateAllTags() {
 | 
				
			||||||
 | 
					  allTags = [];
 | 
				
			||||||
 | 
					  galleryImages.forEach(img => {
 | 
				
			||||||
 | 
					    if (img.tags) img.tags.forEach(t => {
 | 
				
			||||||
 | 
					      if (!allTags.includes(t)) allTags.push(t);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render gallery images with tags and delete buttons ---
 | 
					// --- Render gallery images with tags and delete buttons ---
 | 
				
			||||||
function renderGallery() {
 | 
					function renderGallery() {
 | 
				
			||||||
  const container = document.getElementById('gallery');
 | 
					  const container = document.getElementById('gallery');
 | 
				
			||||||
@@ -27,14 +39,132 @@ function renderGallery() {
 | 
				
			|||||||
    div.className = 'photo';
 | 
					    div.className = 'photo';
 | 
				
			||||||
    div.innerHTML = `
 | 
					    div.innerHTML = `
 | 
				
			||||||
      <img src="/photos/${img.src}">
 | 
					      <img src="/photos/${img.src}">
 | 
				
			||||||
      <input type="text" value="${(img.tags || []).join(', ')}"
 | 
					      <div class="tag-input" data-index="${i}"></div>
 | 
				
			||||||
        onchange="updateTags(${i}, this.value)">
 | 
					 | 
				
			||||||
      <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
					      <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div);
 | 
					    container.appendChild(div);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    renderTags(div.querySelector('.tag-input'), img.tags || [], i);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Render tags for a single image ---
 | 
				
			||||||
 | 
					function renderTags(container, tags, imgIndex) {
 | 
				
			||||||
 | 
					  container.innerHTML = '';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Render existing tags
 | 
				
			||||||
 | 
					  tags.forEach(tag => {
 | 
				
			||||||
 | 
					    const span = document.createElement('span');
 | 
				
			||||||
 | 
					    span.className = 'tag';
 | 
				
			||||||
 | 
					    span.textContent = tag;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const remove = document.createElement('span');
 | 
				
			||||||
 | 
					    remove.className = 'remove-tag';
 | 
				
			||||||
 | 
					    remove.textContent = '×';
 | 
				
			||||||
 | 
					    remove.onclick = () => {
 | 
				
			||||||
 | 
					      tags.splice(tags.indexOf(tag), 1);
 | 
				
			||||||
 | 
					      updateTags(imgIndex, tags);
 | 
				
			||||||
 | 
					      renderTags(container, tags, imgIndex);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    span.appendChild(remove);
 | 
				
			||||||
 | 
					    container.appendChild(span);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Input for new tags
 | 
				
			||||||
 | 
					  const input = document.createElement('input');
 | 
				
			||||||
 | 
					  input.type = 'text';
 | 
				
			||||||
 | 
					  input.placeholder = 'Add tag...';
 | 
				
			||||||
 | 
					  container.appendChild(input);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Suggestion dropdown
 | 
				
			||||||
 | 
					  const suggestionBox = document.createElement('ul');
 | 
				
			||||||
 | 
					  suggestionBox.className = 'suggestions';
 | 
				
			||||||
 | 
					  suggestionBox.style.fontStyle = 'italic';
 | 
				
			||||||
 | 
					  suggestionBox.style.textAlign = 'left';
 | 
				
			||||||
 | 
					  container.appendChild(suggestionBox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let selectedIndex = -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const addTag = (tag) => {
 | 
				
			||||||
 | 
					    tag = tag.trim();
 | 
				
			||||||
 | 
					    if (!tag) return;
 | 
				
			||||||
 | 
					    if (!tags.includes(tag)) tags.push(tag);
 | 
				
			||||||
 | 
					    updateAllTags();
 | 
				
			||||||
 | 
					    updateTags(imgIndex, tags);
 | 
				
			||||||
 | 
					    renderTags(container, tags, imgIndex);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateSuggestions = () => {
 | 
				
			||||||
 | 
					    const value = input.value.toLowerCase();
 | 
				
			||||||
 | 
					    const suggestions = allTags.filter(t => !tags.includes(t) && t.toLowerCase().startsWith(value));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    suggestionBox.innerHTML = '';
 | 
				
			||||||
 | 
					    selectedIndex = -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (suggestions.length) {
 | 
				
			||||||
 | 
					      suggestionBox.style.display = 'block';
 | 
				
			||||||
 | 
					      suggestions.forEach((s, idx) => {
 | 
				
			||||||
 | 
					        const li = document.createElement('li');
 | 
				
			||||||
 | 
					        const boldPart = `<b>${s.substring(0, input.value.length)}</b>`;
 | 
				
			||||||
 | 
					        const rest = s.substring(input.value.length);
 | 
				
			||||||
 | 
					        li.innerHTML = boldPart + rest;
 | 
				
			||||||
 | 
					        li.onclick = () => addTag(s);
 | 
				
			||||||
 | 
					        li.onmouseover = () => selectedIndex = idx;
 | 
				
			||||||
 | 
					        suggestionBox.appendChild(li);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      suggestionBox.style.display = 'none';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input.addEventListener('input', updateSuggestions);
 | 
				
			||||||
 | 
					  input.addEventListener('focus', updateSuggestions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Keyboard navigation
 | 
				
			||||||
 | 
					  input.addEventListener('keydown', (e) => {
 | 
				
			||||||
 | 
					    const items = suggestionBox.querySelectorAll('li');
 | 
				
			||||||
 | 
					    if (items.length) {
 | 
				
			||||||
 | 
					      if (e.key === 'ArrowDown') {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        selectedIndex = (selectedIndex + 1) % items.length;
 | 
				
			||||||
 | 
					        items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
 | 
				
			||||||
 | 
					      } else if (e.key === 'ArrowUp') {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        selectedIndex = (selectedIndex - 1 + items.length) % items.length;
 | 
				
			||||||
 | 
					        items.forEach((li, i) => li.classList.toggle('selected', i === selectedIndex));
 | 
				
			||||||
 | 
					      } else if (e.key === 'Enter') {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        if (selectedIndex >= 0) addTag(items[selectedIndex].textContent.replace(/×$/,''));
 | 
				
			||||||
 | 
					        else addTag(input.value);
 | 
				
			||||||
 | 
					      } else if (e.key === 'Escape') {
 | 
				
			||||||
 | 
					        suggestionBox.style.display = 'none';
 | 
				
			||||||
 | 
					      } else if ([' ', ','].includes(e.key)) {
 | 
				
			||||||
 | 
					        e.preventDefault();
 | 
				
			||||||
 | 
					        addTag(input.value);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else if (['Enter', ' ', ','].includes(e.key)) {
 | 
				
			||||||
 | 
					      e.preventDefault();
 | 
				
			||||||
 | 
					      addTag(input.value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input.addEventListener('blur', () => {
 | 
				
			||||||
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      if (input.value.trim()) addTag(input.value);
 | 
				
			||||||
 | 
					      suggestionBox.style.display = 'none';
 | 
				
			||||||
 | 
					    }, 100);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  input.focus();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Update tags in galleryImages array ---
 | 
				
			||||||
 | 
					function updateTags(index, tags) {
 | 
				
			||||||
 | 
					  galleryImages[index].tags = tags;
 | 
				
			||||||
 | 
					  saveGallery();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Render hero images with delete buttons ---
 | 
					// --- Render hero images with delete buttons ---
 | 
				
			||||||
function renderHero() {
 | 
					function renderHero() {
 | 
				
			||||||
  const container = document.getElementById('hero');
 | 
					  const container = document.getElementById('hero');
 | 
				
			||||||
@@ -50,11 +180,6 @@ function renderHero() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Update tags for gallery image ---
 | 
					 | 
				
			||||||
function updateTags(index, value) {
 | 
					 | 
				
			||||||
  galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Delete gallery image ---
 | 
					// --- Delete gallery image ---
 | 
				
			||||||
async function deleteGalleryImage(index) {
 | 
					async function deleteGalleryImage(index) {
 | 
				
			||||||
  const img = galleryImages[index];
 | 
					  const img = galleryImages[index];
 | 
				
			||||||
@@ -69,9 +194,11 @@ async function deleteGalleryImage(index) {
 | 
				
			|||||||
      galleryImages.splice(index, 1);
 | 
					      galleryImages.splice(index, 1);
 | 
				
			||||||
      renderGallery();
 | 
					      renderGallery();
 | 
				
			||||||
      await saveGallery();
 | 
					      await saveGallery();
 | 
				
			||||||
    } else alert("Error: " + data.error);
 | 
					      showToast("✅ Gallery image deleted!", "success");
 | 
				
			||||||
 | 
					    } else showToast("Error: " + data.error, "error");
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err); alert("Server error!");
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    showToast("Server error!", "error");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -89,9 +216,11 @@ async function deleteHeroImage(index) {
 | 
				
			|||||||
      heroImages.splice(index, 1);
 | 
					      heroImages.splice(index, 1);
 | 
				
			||||||
      renderHero();
 | 
					      renderHero();
 | 
				
			||||||
      await saveHero();
 | 
					      await saveHero();
 | 
				
			||||||
    } else alert("Error: " + data.error);
 | 
					      showToast("✅ Hero image deleted!", "success");
 | 
				
			||||||
 | 
					    } else showToast("Error: " + data.error, "error");
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err); alert("Server error!");
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    showToast("Server error!", "error");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,21 +246,39 @@ async function saveHero() {
 | 
				
			|||||||
async function saveChanges() {
 | 
					async function saveChanges() {
 | 
				
			||||||
  await saveGallery();
 | 
					  await saveGallery();
 | 
				
			||||||
  await saveHero();
 | 
					  await saveHero();
 | 
				
			||||||
  alert('✅ Changes saved!');
 | 
					  showToast('✅ Changes saved!', "success");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Refresh gallery from folder ---
 | 
					// --- Refresh gallery from folder ---
 | 
				
			||||||
async function refreshGallery() {
 | 
					async function refreshGallery() {
 | 
				
			||||||
  await fetch('/api/gallery/refresh', { method: 'POST' });
 | 
					  await fetch('/api/gallery/refresh', { method: 'POST' });
 | 
				
			||||||
  await loadData();
 | 
					  await loadData();
 | 
				
			||||||
  alert('🔄 Gallery updated from photos/gallery folder');
 | 
					  showToast('🔄 Gallery updated from photos/gallery folder', "success");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Refresh hero from folder ---
 | 
					// --- Refresh hero from folder ---
 | 
				
			||||||
async function refreshHero() {
 | 
					async function refreshHero() {
 | 
				
			||||||
  await fetch('/api/hero/refresh', { method: 'POST' });
 | 
					  await fetch('/api/hero/refresh', { method: 'POST' });
 | 
				
			||||||
  await loadData();
 | 
					  await loadData();
 | 
				
			||||||
  alert('🔄 Hero updated from photos/hero folder');
 | 
					  showToast('🔄 Hero updated from photos/hero folder', "success");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Show toast notification ---
 | 
				
			||||||
 | 
					function showToast(message, type = "success", duration = 3000) {
 | 
				
			||||||
 | 
					  const container = document.getElementById("toast-container");
 | 
				
			||||||
 | 
					  if (!container) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const toast = document.createElement("div");
 | 
				
			||||||
 | 
					  toast.className = `toast ${type}`;
 | 
				
			||||||
 | 
					  toast.textContent = message;
 | 
				
			||||||
 | 
					  container.appendChild(toast);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  requestAnimationFrame(() => toast.classList.add("show"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setTimeout(() => {
 | 
				
			||||||
 | 
					    toast.classList.remove("show");
 | 
				
			||||||
 | 
					    toast.addEventListener("transitionend", () => toast.remove());
 | 
				
			||||||
 | 
					  }, duration);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// --- Initialize ---
 | 
					// --- Initialize ---
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,11 +10,12 @@ document.getElementById('upload-gallery').addEventListener('change', async (e) =
 | 
				
			|||||||
    const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
					    const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
 | 
				
			||||||
    const data = await res.json();
 | 
					    const data = await res.json();
 | 
				
			||||||
    if (res.ok) {
 | 
					    if (res.ok) {
 | 
				
			||||||
      alert(`✅ ${data.uploaded.length} gallery image(s) uploaded!`);
 | 
					      showToast(`✅ ${data.uploaded.length} gallery image(s) uploaded!`, "success");
 | 
				
			||||||
      refreshGallery();
 | 
					      refreshGallery();
 | 
				
			||||||
    } else alert('Error: ' + data.error);
 | 
					    } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err); alert('Server error!');
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    showToast('Server error!', "error");
 | 
				
			||||||
  } finally { e.target.value = ''; }
 | 
					  } finally { e.target.value = ''; }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,10 +31,11 @@ document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
				
			|||||||
    const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
					    const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
 | 
				
			||||||
    const data = await res.json();
 | 
					    const data = await res.json();
 | 
				
			||||||
    if (res.ok) {
 | 
					    if (res.ok) {
 | 
				
			||||||
      alert(`✅ ${data.uploaded.length} hero image(s) uploaded!`);
 | 
					      showToast(`✅ ${data.uploaded.length} hero image(s) uploaded!`, "success");
 | 
				
			||||||
      refreshHero();
 | 
					      refreshHero();
 | 
				
			||||||
    } else alert('Error: ' + data.error);
 | 
					    } else showToast('Error: ' + data.error, "error");
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err); alert('Server error!');
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    showToast('Server error!', "error");
 | 
				
			||||||
  } finally { e.target.value = ''; }
 | 
					  } finally { e.target.value = ''; }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,3 +96,104 @@ h1, h2 {
 | 
				
			|||||||
    margin-bottom: 10px;
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Toast notifications */
 | 
				
			||||||
 | 
					#toast-container {
 | 
				
			||||||
 | 
					  position: fixed;
 | 
				
			||||||
 | 
					  top: 1rem;
 | 
				
			||||||
 | 
					  right: 1rem;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-direction: column;
 | 
				
			||||||
 | 
					  gap: 0.5rem;
 | 
				
			||||||
 | 
					  z-index: 9999;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toast {
 | 
				
			||||||
 | 
					  background: rgba(0,0,0,0.85);
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  padding: 0.75rem 1.25rem;
 | 
				
			||||||
 | 
					  border-radius: 0.5rem;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  transform: translateY(-20px);
 | 
				
			||||||
 | 
					  transition: opacity 0.5s ease, transform 0.5s ease;
 | 
				
			||||||
 | 
					  pointer-events: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toast.show {
 | 
				
			||||||
 | 
					  opacity: 1;
 | 
				
			||||||
 | 
					  transform: translateY(0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toast.success { background-color: #28a745; }
 | 
				
			||||||
 | 
					.toast.error { background-color: #dc3545; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Tags */
 | 
				
			||||||
 | 
					.tag-input {
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  flex-wrap: wrap;
 | 
				
			||||||
 | 
					  gap: 4px;
 | 
				
			||||||
 | 
					  border: 1px solid #ccc;
 | 
				
			||||||
 | 
					  padding: 4px;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  position: relative; /* ensures dropdown positions correctly */
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  z-index: 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag-input input {
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  flex: 1;
 | 
				
			||||||
 | 
					  min-width: 60px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag {
 | 
				
			||||||
 | 
					  background-color: #eee;
 | 
				
			||||||
 | 
					  padding: 2px 6px;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag .remove-tag {
 | 
				
			||||||
 | 
					  margin-left: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  font-weight: bold;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag-input ul.suggestions {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  top: 100%;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  background: white;
 | 
				
			||||||
 | 
					  border: 1px solid #ccc;
 | 
				
			||||||
 | 
					  border-top: none;
 | 
				
			||||||
 | 
					  list-style: none;
 | 
				
			||||||
 | 
					  margin: 0;
 | 
				
			||||||
 | 
					  padding: 0;
 | 
				
			||||||
 | 
					  max-height: 150px;
 | 
				
			||||||
 | 
					  overflow-y: auto;
 | 
				
			||||||
 | 
					  z-index: 999; /* ensure it displays above other elements */
 | 
				
			||||||
 | 
					  display: none;
 | 
				
			||||||
 | 
					  box-shadow: 0 4px 8px rgba(0,0,0,0.15); /* subtle shadow for visibility */
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag-input ul.suggestions li {
 | 
				
			||||||
 | 
					  padding: 6px 8px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tag-input ul.suggestions li:hover {
 | 
				
			||||||
 | 
					  background-color: #f0f0f0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.suggestions li.selected {
 | 
				
			||||||
 | 
					  background-color: #007bff;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.suggestions li {
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user