Compare commits
	
		
			4 Commits
		
	
	
		
			d3484a4b50
			...
			b74f1bb350
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					b74f1bb350 | ||
| 
						 | 
					5a6f08644a | ||
| 
						 | 
					031ff62168 | ||
| 
						 | 
					b56d03303e | 
@@ -24,6 +24,8 @@ footer:
 | 
			
		||||
# Build parameters
 | 
			
		||||
build:
 | 
			
		||||
  theme: modern # choose a theme in config/theme folder
 | 
			
		||||
  convert_images: true # true to enable image conversion
 | 
			
		||||
  resize_images: true # true to enable image resizing
 | 
			
		||||
 | 
			
		||||
# Change this by your legals
 | 
			
		||||
legals:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
import logging
 | 
			
		||||
import yaml
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from flask import Flask, jsonify, request, send_from_directory, render_template
 | 
			
		||||
from src.py.builder.gallery_builder import (
 | 
			
		||||
@@ -18,6 +19,8 @@ app = Flask(
 | 
			
		||||
    static_url_path=""
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
 | 
			
		||||
 | 
			
		||||
# --- Photos directory (configurable) ---
 | 
			
		||||
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
			
		||||
app.config["PHOTOS_DIR"] = PHOTOS_DIR
 | 
			
		||||
@@ -96,11 +99,62 @@ def delete_hero_photo():
 | 
			
		||||
        return {"status": "ok"}
 | 
			
		||||
    return {"error": "File not found"}, 404
 | 
			
		||||
 | 
			
		||||
@app.route("/api/gallery/delete_all", methods=["POST"])
 | 
			
		||||
def delete_all_gallery_photos():
 | 
			
		||||
    """Delete all gallery photos from disk and YAML."""
 | 
			
		||||
    gallery_dir = PHOTOS_DIR / "gallery"
 | 
			
		||||
    deleted = 0
 | 
			
		||||
    # Remove all files in gallery folder
 | 
			
		||||
    for file in gallery_dir.glob("*"):
 | 
			
		||||
        if file.is_file():
 | 
			
		||||
            file.unlink()
 | 
			
		||||
            deleted += 1
 | 
			
		||||
    # Clear YAML gallery images
 | 
			
		||||
    data = load_yaml(GALLERY_YAML)
 | 
			
		||||
    data["gallery"]["images"] = []
 | 
			
		||||
    save_yaml(data, GALLERY_YAML)
 | 
			
		||||
    return jsonify({"status": "ok", "deleted": deleted})
 | 
			
		||||
 | 
			
		||||
@app.route("/api/hero/delete_all", methods=["POST"])
 | 
			
		||||
def delete_all_hero_photos():
 | 
			
		||||
    """Delete all hero photos from disk and YAML."""
 | 
			
		||||
    hero_dir = PHOTOS_DIR / "hero"
 | 
			
		||||
    deleted = 0
 | 
			
		||||
    # Remove all files in hero folder
 | 
			
		||||
    for file in hero_dir.glob("*"):
 | 
			
		||||
        if file.is_file():
 | 
			
		||||
            file.unlink()
 | 
			
		||||
            deleted += 1
 | 
			
		||||
    # Clear YAML hero images
 | 
			
		||||
    data = load_yaml(GALLERY_YAML)
 | 
			
		||||
    data["hero"]["images"] = []
 | 
			
		||||
    save_yaml(data, GALLERY_YAML)
 | 
			
		||||
    return jsonify({"status": "ok", "deleted": deleted})
 | 
			
		||||
 | 
			
		||||
@app.route("/photos/<section>/<path:filename>")
 | 
			
		||||
def photos(section, filename):
 | 
			
		||||
    """Serve uploaded photos from disk."""
 | 
			
		||||
    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@app.route("/site-info")
 | 
			
		||||
def site_info():
 | 
			
		||||
    return render_template("site-info/index.html")
 | 
			
		||||
 | 
			
		||||
@app.route("/api/site-info", methods=["GET"])
 | 
			
		||||
def get_site_info():
 | 
			
		||||
    with open(SITE_YAML, "r") as f:
 | 
			
		||||
        data = yaml.safe_load(f)
 | 
			
		||||
    return jsonify(data)
 | 
			
		||||
 | 
			
		||||
@app.route("/api/site-info", methods=["POST"])
 | 
			
		||||
def update_site_info():
 | 
			
		||||
    data = request.json
 | 
			
		||||
    with open(SITE_YAML, "w") as f:
 | 
			
		||||
        yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
 | 
			
		||||
    return jsonify({"status": "ok"})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# --- Run server ---
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <title>Photo WebUI</title>
 | 
			
		||||
 | 
			
		||||
  <!-- Link to your CSS in the package -->
 | 
			
		||||
  <title>Lumeex</title>
 | 
			
		||||
  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
@@ -12,10 +11,10 @@
 | 
			
		||||
  <div class="nav-bar">
 | 
			
		||||
    <div class="content-inner nav">
 | 
			
		||||
      <div class="nav-cta">
 | 
			
		||||
				  	<div class="arrow">
 | 
			
		||||
             →
 | 
			
		||||
            </div>
 | 
			
		||||
              <a class="button" href="#" target="_blank"><span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span></a>
 | 
			
		||||
        <div class="arrow">→</div>
 | 
			
		||||
        <a class="button" href="#" target="_blank">
 | 
			
		||||
          <span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input type="checkbox" id="nav-check">
 | 
			
		||||
      <div class="nav-header">
 | 
			
		||||
@@ -23,7 +22,6 @@
 | 
			
		||||
          <img src="{{ url_for('static', filename='img/logo.svg') }}">
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="nav-btn">
 | 
			
		||||
        <label for="nav-check">
 | 
			
		||||
          <span></span>
 | 
			
		||||
@@ -31,16 +29,13 @@
 | 
			
		||||
          <span></span>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
			  
 | 
			
		||||
      <div class="nav-links">
 | 
			
		||||
        <ul class="nav-list">
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Site info</a>
 | 
			
		||||
				    <li class="nav-item appear2"><a href="#qui-suis-je">Theme info</a>
 | 
			
		||||
				    <li class="nav-item appear2"><a href="#mariages">Gallery</a>
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Theme info</a>
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Gallery</a>
 | 
			
		||||
        </ul>
 | 
			
		||||
				  
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Toast container for notifications -->
 | 
			
		||||
@@ -53,9 +48,12 @@
 | 
			
		||||
    <div class="upload-section">
 | 
			
		||||
      <h2>Title Carrousel</h2>
 | 
			
		||||
      <p> Select photos to display in the Title Carrousel</p>
 | 
			
		||||
      <label for="upload-hero" class="custom-upload-btn">
 | 
			
		||||
      <div class="upload-actions-row">
 | 
			
		||||
        <label for="upload-hero" class="up-btn">
 | 
			
		||||
          📸 Upload photos
 | 
			
		||||
        </label>
 | 
			
		||||
        <button id="remove-all-hero" class="up-btn">🗑 Delete all</button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
			
		||||
      <div id="hero"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -64,16 +62,17 @@
 | 
			
		||||
    <div class="upload-section">
 | 
			
		||||
      <h2>Gallery</h2>
 | 
			
		||||
      <p> Select and tags photos to display in the Gallery</p>
 | 
			
		||||
      <label for="upload-gallery" class="custom-upload-btn">
 | 
			
		||||
      <div class="upload-actions-row">
 | 
			
		||||
        <label for="upload-gallery" class="up-btn">
 | 
			
		||||
          📸 Upload photos
 | 
			
		||||
        </label>
 | 
			
		||||
        <button id="remove-all-gallery" class="up-btn">🗑 Delete all</button>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple hidden>
 | 
			
		||||
      <div id="gallery"></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- JS files for rendering, uploading, and actions -->
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/main.js') }}" defer></script>
 | 
			
		||||
    <script src="{{ url_for('static', filename='js/upload.js') }}" defer></script>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Delete confirmation modal -->
 | 
			
		||||
  <div id="delete-modal" class="modal" style="display:none;">
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,12 @@ function renderGallery() {
 | 
			
		||||
 | 
			
		||||
    renderTags(i, img.tags || []);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Show/hide Remove All button
 | 
			
		||||
  const removeAllBtn = document.getElementById('remove-all-gallery');
 | 
			
		||||
  if (removeAllBtn) {
 | 
			
		||||
    removeAllBtn.style.display = galleryImages.length > 0 ? 'inline-block' : 'none';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Render tags for a single image ---
 | 
			
		||||
@@ -60,11 +66,9 @@ function renderTags(imgIndex, tags) {
 | 
			
		||||
  const tagsDisplay = document.querySelector(`.tags-display[data-index="${imgIndex}"]`);
 | 
			
		||||
  const inputContainer = document.querySelector(`.tag-input[data-index="${imgIndex}"]`);
 | 
			
		||||
 | 
			
		||||
  // vider
 | 
			
		||||
  tagsDisplay.innerHTML = '';
 | 
			
		||||
  inputContainer.innerHTML = '';
 | 
			
		||||
 | 
			
		||||
  // --- rendre les tags (en haut) ---
 | 
			
		||||
  tags.forEach(tag => {
 | 
			
		||||
    const span = document.createElement('span');
 | 
			
		||||
    span.className = 'tag';
 | 
			
		||||
@@ -83,13 +87,11 @@ function renderTags(imgIndex, tags) {
 | 
			
		||||
    tagsDisplay.appendChild(span);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // --- input (en bas) ---
 | 
			
		||||
  const input = document.createElement('input');
 | 
			
		||||
  input.type = 'text';
 | 
			
		||||
  input.placeholder = 'Add tag...';
 | 
			
		||||
  inputContainer.appendChild(input);
 | 
			
		||||
 | 
			
		||||
  // suggestion box
 | 
			
		||||
  const suggestionBox = document.createElement('ul');
 | 
			
		||||
  suggestionBox.className = 'suggestions';
 | 
			
		||||
  inputContainer.appendChild(suggestionBox);
 | 
			
		||||
@@ -210,11 +212,17 @@ function renderHero() {
 | 
			
		||||
        <div class="flex-item flex-end">
 | 
			
		||||
          <button onclick="showDeleteModal('hero', ${i})">🗑 Delete</button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
    container.appendChild(div);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  // Show/hide Remove All button
 | 
			
		||||
  const removeAllBtn = document.getElementById('remove-all-hero');
 | 
			
		||||
  if (removeAllBtn) {
 | 
			
		||||
    removeAllBtn.style.display = heroImages.length > 0 ? 'inline-block' : 'none';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Save gallery to server ---
 | 
			
		||||
async function saveGallery() {
 | 
			
		||||
@@ -273,11 +281,19 @@ function showToast(message, type = "success", duration = 3000) {
 | 
			
		||||
  }, duration);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let pendingDelete = null; // { type: 'gallery'|'hero', index: number }
 | 
			
		||||
let pendingDelete = null; // { type: 'gallery'|'hero'|'gallery-all'|'hero-all', index: number|null }
 | 
			
		||||
 | 
			
		||||
// --- Show delete confirmation modal ---
 | 
			
		||||
function showDeleteModal(type, index) {
 | 
			
		||||
function showDeleteModal(type, index = null) {
 | 
			
		||||
  pendingDelete = { type, index };
 | 
			
		||||
  const modalText = document.getElementById('delete-modal-text');
 | 
			
		||||
  if (type === 'gallery-all') {
 | 
			
		||||
    modalText.textContent = "Are you sure you want to delete ALL gallery images?";
 | 
			
		||||
  } else if (type === 'hero-all') {
 | 
			
		||||
    modalText.textContent = "Are you sure you want to delete ALL hero images?";
 | 
			
		||||
  } else {
 | 
			
		||||
    modalText.textContent = "Are you sure you want to delete this image?";
 | 
			
		||||
  }
 | 
			
		||||
  document.getElementById('delete-modal').style.display = 'flex';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -294,18 +310,14 @@ async function confirmDelete() {
 | 
			
		||||
    await actuallyDeleteGalleryImage(pendingDelete.index);
 | 
			
		||||
  } else if (pendingDelete.type === 'hero') {
 | 
			
		||||
    await actuallyDeleteHeroImage(pendingDelete.index);
 | 
			
		||||
  } else if (pendingDelete.type === 'gallery-all') {
 | 
			
		||||
    await actuallyDeleteAllGalleryImages();
 | 
			
		||||
  } else if (pendingDelete.type === 'hero-all') {
 | 
			
		||||
    await actuallyDeleteAllHeroImages();
 | 
			
		||||
  }
 | 
			
		||||
  hideDeleteModal();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// --- Modal event listeners ---
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  document.getElementById('delete-modal-close').onclick = hideDeleteModal;
 | 
			
		||||
  document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
 | 
			
		||||
  document.getElementById('delete-modal-confirm').onclick = confirmDelete;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- Actual delete functions ---
 | 
			
		||||
async function actuallyDeleteGalleryImage(index) {
 | 
			
		||||
  const img = galleryImages[index];
 | 
			
		||||
@@ -349,14 +361,51 @@ async function actuallyDeleteHeroImage(index) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Modal event listeners ---
 | 
			
		||||
// --- Bulk delete functions ---
 | 
			
		||||
async function actuallyDeleteAllGalleryImages() {
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch('/api/gallery/delete_all', { method: 'POST' });
 | 
			
		||||
    const data = await res.json();
 | 
			
		||||
    if (res.ok) {
 | 
			
		||||
      galleryImages = [];
 | 
			
		||||
      renderGallery();
 | 
			
		||||
      await saveGallery();
 | 
			
		||||
      showToast("✅ All gallery images removed!", "success");
 | 
			
		||||
    } else showToast("Error: " + data.error, "error");
 | 
			
		||||
  } catch(err) {
 | 
			
		||||
    console.error(err);
 | 
			
		||||
    showToast("Server error!", "error");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function actuallyDeleteAllHeroImages() {
 | 
			
		||||
  try {
 | 
			
		||||
    const res = await fetch('/api/hero/delete_all', { method: 'POST' });
 | 
			
		||||
    const data = await res.json();
 | 
			
		||||
    if (res.ok) {
 | 
			
		||||
      heroImages = [];
 | 
			
		||||
      renderHero();
 | 
			
		||||
      await saveHero();
 | 
			
		||||
      showToast("✅ All hero images removed!", "success");
 | 
			
		||||
    } else showToast("Error: " + data.error, "error");
 | 
			
		||||
  } catch(err) {
 | 
			
		||||
    console.error(err);
 | 
			
		||||
    showToast("Server error!", "error");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- Modal event listeners and bulk delete buttons ---
 | 
			
		||||
document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
  document.getElementById('delete-modal-close').onclick = hideDeleteModal;
 | 
			
		||||
  document.getElementById('delete-modal-cancel').onclick = hideDeleteModal;
 | 
			
		||||
  document.getElementById('delete-modal-confirm').onclick = confirmDelete;
 | 
			
		||||
 | 
			
		||||
  // Bulk delete buttons
 | 
			
		||||
  const removeAllGalleryBtn = document.getElementById('remove-all-gallery');
 | 
			
		||||
  const removeAllHeroBtn = document.getElementById('remove-all-hero');
 | 
			
		||||
  if (removeAllGalleryBtn) removeAllGalleryBtn.onclick = () => showDeleteModal('gallery-all');
 | 
			
		||||
  if (removeAllHeroBtn) removeAllHeroBtn.onclick = () => showDeleteModal('hero-all');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// --- Initialize ---
 | 
			
		||||
loadData();
 | 
			
		||||
							
								
								
									
										197
									
								
								src/webui/js/site-info.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/webui/js/site-info.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,197 @@
 | 
			
		||||
document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
  const form = document.getElementById("site-info-form");
 | 
			
		||||
  const status = document.getElementById("site-info-status");
 | 
			
		||||
  const menuList = document.getElementById("menu-items-list");
 | 
			
		||||
  const addMenuBtn = document.getElementById("add-menu-item");
 | 
			
		||||
 | 
			
		||||
  let menuItems = [];
 | 
			
		||||
 | 
			
		||||
  function renderMenuItems() {
 | 
			
		||||
    menuList.innerHTML = "";
 | 
			
		||||
    menuItems.forEach((item, idx) => {
 | 
			
		||||
      const div = document.createElement("div");
 | 
			
		||||
      div.style.display = "flex";
 | 
			
		||||
      div.style.gap = "8px";
 | 
			
		||||
      div.style.marginBottom = "6px";
 | 
			
		||||
      div.innerHTML = `
 | 
			
		||||
        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label">
 | 
			
		||||
        <input type="text" placeholder="URL" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href">
 | 
			
		||||
        <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
			
		||||
      `;
 | 
			
		||||
      menuList.appendChild(div);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateMenuItemsFromInputs() {
 | 
			
		||||
    const inputs = menuList.querySelectorAll("input");
 | 
			
		||||
    const items = [];
 | 
			
		||||
    for (let i = 0; i < inputs.length; i += 2) {
 | 
			
		||||
      const label = inputs[i].value.trim();
 | 
			
		||||
      const href = inputs[i + 1].value.trim();
 | 
			
		||||
      if (label || href) items.push({ label, href });
 | 
			
		||||
    }
 | 
			
		||||
    menuItems = items;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const ipList = document.getElementById("ip-list");
 | 
			
		||||
  const addIpBtn = document.getElementById("add-ip-paragraph");
 | 
			
		||||
  let ipParagraphs = [];
 | 
			
		||||
 | 
			
		||||
  function renderIpParagraphs() {
 | 
			
		||||
    ipList.innerHTML = "";
 | 
			
		||||
    ipParagraphs.forEach((item, idx) => {
 | 
			
		||||
      const div = document.createElement("div");
 | 
			
		||||
      div.style.display = "flex";
 | 
			
		||||
      div.style.gap = "8px";
 | 
			
		||||
      div.style.marginBottom = "6px";
 | 
			
		||||
      div.innerHTML = `
 | 
			
		||||
        <input type="text" placeholder="Paragraph" value="${item.paragraph || ""}" style="flex:1;" data-idx="${idx}">
 | 
			
		||||
        <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
 | 
			
		||||
      `;
 | 
			
		||||
      ipList.appendChild(div);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function updateIpParagraphsFromInputs() {
 | 
			
		||||
    const inputs = ipList.querySelectorAll("input");
 | 
			
		||||
    ipParagraphs = Array.from(inputs).map(input => ({
 | 
			
		||||
      paragraph: input.value.trim()
 | 
			
		||||
    })).filter(item => item.paragraph !== "");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // --- Build checkboxes ---
 | 
			
		||||
  const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
 | 
			
		||||
  const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
 | 
			
		||||
 | 
			
		||||
  // Load config
 | 
			
		||||
  if (form) {
 | 
			
		||||
    fetch("/api/site-info")
 | 
			
		||||
      .then(res => res.json())
 | 
			
		||||
      .then(data => {
 | 
			
		||||
        ipParagraphs = Array.isArray(data.legals?.intellectual_property)
 | 
			
		||||
          ? data.legals.intellectual_property
 | 
			
		||||
          : [];
 | 
			
		||||
        renderIpParagraphs();
 | 
			
		||||
        menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
 | 
			
		||||
        renderMenuItems();
 | 
			
		||||
        form.elements["info.title"].value = data.info?.title || "";
 | 
			
		||||
        form.elements["info.subtitle"].value = data.info?.subtitle || "";
 | 
			
		||||
        form.elements["info.description"].value = data.info?.description || "";
 | 
			
		||||
        form.elements["info.canonical"].value = data.info?.canonical || "";
 | 
			
		||||
        form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
 | 
			
		||||
        form.elements["info.author"].value = data.info?.author || "";
 | 
			
		||||
        form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
 | 
			
		||||
        form.elements["social.thumbnail"].value = data.social?.thumbnail || "";
 | 
			
		||||
        form.elements["footer.copyright"].value = data.footer?.copyright || "";
 | 
			
		||||
        form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
 | 
			
		||||
        form.elements["build.theme"].value = data.build?.theme || "";
 | 
			
		||||
        form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
 | 
			
		||||
        form.elements["legals.hoster_adress"].value = data.legals?.hoster_adress || "";
 | 
			
		||||
        form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
 | 
			
		||||
        // --- Build checkboxes ---
 | 
			
		||||
        if (convertImagesCheckbox) {
 | 
			
		||||
          convertImagesCheckbox.checked = !!data.build?.convert_images;
 | 
			
		||||
        }
 | 
			
		||||
        if (resizeImagesCheckbox) {
 | 
			
		||||
          resizeImagesCheckbox.checked = !!data.build?.resize_images;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Add menu item
 | 
			
		||||
  if (addMenuBtn) {
 | 
			
		||||
    addMenuBtn.addEventListener("click", () => {
 | 
			
		||||
      menuItems.push({ label: "", href: "" });
 | 
			
		||||
      renderMenuItems();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Remove menu item
 | 
			
		||||
  menuList.addEventListener("click", (e) => {
 | 
			
		||||
    if (e.target.classList.contains("remove-menu-item")) {
 | 
			
		||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
			
		||||
      menuItems.splice(idx, 1);
 | 
			
		||||
      renderMenuItems();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Update menuItems on input change
 | 
			
		||||
  menuList.addEventListener("input", () => {
 | 
			
		||||
    updateMenuItemsFromInputs();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Add paragraph
 | 
			
		||||
  if (addIpBtn) {
 | 
			
		||||
    addIpBtn.addEventListener("click", () => {
 | 
			
		||||
      ipParagraphs.push({ paragraph: "" });
 | 
			
		||||
      renderIpParagraphs();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Remove paragraph
 | 
			
		||||
  ipList.addEventListener("click", (e) => {
 | 
			
		||||
    if (e.target.classList.contains("remove-ip-paragraph")) {
 | 
			
		||||
      const idx = parseInt(e.target.getAttribute("data-idx"));
 | 
			
		||||
      ipParagraphs.splice(idx, 1);
 | 
			
		||||
      renderIpParagraphs();
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Update ipParagraphs on input change
 | 
			
		||||
  ipList.addEventListener("input", () => {
 | 
			
		||||
    updateIpParagraphsFromInputs();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Save config
 | 
			
		||||
  if (form) {
 | 
			
		||||
    form.addEventListener("submit", async (e) => {
 | 
			
		||||
      e.preventDefault();
 | 
			
		||||
      updateMenuItemsFromInputs();
 | 
			
		||||
      updateIpParagraphsFromInputs();
 | 
			
		||||
 | 
			
		||||
      // --- Build object with checkboxes ---
 | 
			
		||||
      const build = {
 | 
			
		||||
        theme: form.elements["build.theme"].value,
 | 
			
		||||
        convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
 | 
			
		||||
        resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const payload = {
 | 
			
		||||
        info: {
 | 
			
		||||
          title: form.elements["info.title"].value,
 | 
			
		||||
          subtitle: form.elements["info.subtitle"].value,
 | 
			
		||||
          description: form.elements["info.description"].value,
 | 
			
		||||
          canonical: form.elements["info.canonical"].value,
 | 
			
		||||
          keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
 | 
			
		||||
          author: form.elements["info.author"].value
 | 
			
		||||
        },
 | 
			
		||||
        social: {
 | 
			
		||||
          instagram_url: form.elements["social.instagram_url"].value,
 | 
			
		||||
          thumbnail: form.elements["social.thumbnail"].value
 | 
			
		||||
        },
 | 
			
		||||
        menu: {
 | 
			
		||||
          items: menuItems
 | 
			
		||||
        },
 | 
			
		||||
        footer: {
 | 
			
		||||
          copyright: form.elements["footer.copyright"].value,
 | 
			
		||||
          legal_label: form.elements["footer.legal_label"].value
 | 
			
		||||
        },
 | 
			
		||||
        build,
 | 
			
		||||
        legals: {
 | 
			
		||||
          hoster_name: form.elements["legals.hoster_name"].value,
 | 
			
		||||
          hoster_adress: form.elements["legals.hoster_adress"].value,
 | 
			
		||||
          hoster_contact: form.elements["legals.hoster_contact"].value,
 | 
			
		||||
          intellectual_property: ipParagraphs
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      const res = await fetch("/api/site-info", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: { "Content-Type": "application/json" },
 | 
			
		||||
        body: JSON.stringify(payload)
 | 
			
		||||
      });
 | 
			
		||||
      const result = await res.json();
 | 
			
		||||
      status.textContent = result.status === "ok" ? "✅ Saved!" : "❌ Error saving";
 | 
			
		||||
      setTimeout(() => status.textContent = "", 2000);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										109
									
								
								src/webui/site-info/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/webui/site-info/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <title>Lumeex</title>
 | 
			
		||||
  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <!-- Top bar -->	
 | 
			
		||||
  <div class="nav-bar">
 | 
			
		||||
    <div class="content-inner nav">
 | 
			
		||||
      <div class="nav-cta">
 | 
			
		||||
        <div class="arrow">→</div>
 | 
			
		||||
        <a class="button" href="#" target="_blank">
 | 
			
		||||
          <span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input type="checkbox" id="nav-check">
 | 
			
		||||
      <div class="nav-header">
 | 
			
		||||
        <div class="nav-title">
 | 
			
		||||
          <img src="{{ url_for('static', filename='img/logo.svg') }}">
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-btn">
 | 
			
		||||
        <label for="nav-check">
 | 
			
		||||
          <span></span>
 | 
			
		||||
          <span></span>
 | 
			
		||||
          <span></span>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-links">
 | 
			
		||||
        <ul class="nav-list">
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Site info</a>
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Theme info</a>
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Gallery</a>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Toast container for notifications -->
 | 
			
		||||
  <div class="content-inner">
 | 
			
		||||
    <div id="toast-container"></div>
 | 
			
		||||
    <h1>Edit Site Info</h1>
 | 
			
		||||
    <form id="site-info-form">
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Info</legend>
 | 
			
		||||
        <label>Title: <input type="text" name="info.title"></label><br>
 | 
			
		||||
        <label>Subtitle: <input type="text" name="info.subtitle"></label><br>
 | 
			
		||||
        <label>Description: <textarea name="info.description"></textarea></label><br>
 | 
			
		||||
        <label>Canonical URL: <input type="text" name="info.canonical"></label><br>
 | 
			
		||||
        <label>Keywords (comma separated): <input type="text" name="info.keywords"></label><br>
 | 
			
		||||
        <label>Author: <input type="text" name="info.author"></label><br>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Social</legend>
 | 
			
		||||
        <label>Instagram URL: <input type="text" name="social.instagram_url"></label><br>
 | 
			
		||||
        <label>Thumbnail: <input type="text" name="social.thumbnail"></label><br>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Menu</legend>
 | 
			
		||||
        <div id="menu-items-list"></div>
 | 
			
		||||
        <button type="button" id="add-menu-item">+ Add menu item</button>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Footer</legend>
 | 
			
		||||
        <label>Copyright: <input type="text" name="footer.copyright"></label><br>
 | 
			
		||||
        <label>Legal Label: <input type="text" name="footer.legal_label"></label><br>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Build</legend>
 | 
			
		||||
        <label>Theme: <input type="text" name="build.theme"></label><br>
 | 
			
		||||
        <label>
 | 
			
		||||
            <input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
 | 
			
		||||
            Convert images
 | 
			
		||||
        </label><br>
 | 
			
		||||
        <label>
 | 
			
		||||
            <input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
 | 
			
		||||
            Resize images
 | 
			
		||||
        </label><br>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <legend>Legals</legend>
 | 
			
		||||
        <label>Hoster Name: <input type="text" name="legals.hoster_name"></label><br>
 | 
			
		||||
        <label>Hoster Address: <input type="text" name="legals.hoster_adress"></label><br>
 | 
			
		||||
        <label>Hoster Contact: <input type="text" name="legals.hoster_contact"></label><br>
 | 
			
		||||
        <div>
 | 
			
		||||
            <label>Intellectual Property:</label>
 | 
			
		||||
            <div id="ip-list"></div>
 | 
			
		||||
            <button type="button" id="add-ip-paragraph">+ Add paragraph</button>
 | 
			
		||||
        </div>
 | 
			
		||||
    </fieldset>
 | 
			
		||||
      <button type="submit">Save</button>
 | 
			
		||||
    </form>
 | 
			
		||||
    <div id="site-info-status"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div id="delete-modal" class="modal" style="display:none;">
 | 
			
		||||
    <div class="modal-content">
 | 
			
		||||
      <span id="delete-modal-close" class="modal-close">×</span>
 | 
			
		||||
      <h3>Confirm Deletion</h3>
 | 
			
		||||
      <p id="delete-modal-text">Are you sure you want to delete this image?</p>
 | 
			
		||||
      <div class="modal-actions">
 | 
			
		||||
        <button id="delete-modal-confirm" class="modal-btn danger">Delete</button>
 | 
			
		||||
        <button id="delete-modal-cancel" class="modal-btn">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  <script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
/* --- Base Styles --- */
 | 
			
		||||
body {
 | 
			
		||||
  font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
 | 
			
		||||
  margin: 20px;
 | 
			
		||||
@@ -11,10 +12,12 @@ body {
 | 
			
		||||
  max-width: 90%;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
h1, h2 {
 | 
			
		||||
  color: #FBFBFB;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Toolbar --- */
 | 
			
		||||
.toolbar {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
@@ -34,15 +37,16 @@ h1, h2 {
 | 
			
		||||
  background-color: #45a049;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Upload Section --- */
 | 
			
		||||
.upload-section {
 | 
			
		||||
  margin-bottom: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.upload-section label {
 | 
			
		||||
  margin-right: 20px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Gallery & Hero Grid --- */
 | 
			
		||||
#gallery, #hero {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
			
		||||
@@ -50,6 +54,7 @@ h1, h2 {
 | 
			
		||||
  margin-top: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Photo Card --- */
 | 
			
		||||
.photo {
 | 
			
		||||
  background-color: rgb(67 67 67 / 26%);
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
@@ -69,7 +74,6 @@ h1, h2 {
 | 
			
		||||
  padding: 4px 6px;
 | 
			
		||||
  border-radius: 30px;
 | 
			
		||||
  color: rgb(221, 221, 221);
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.photo button {
 | 
			
		||||
@@ -88,7 +92,7 @@ h1, h2 {
 | 
			
		||||
  background-color: #d32f2f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive adjustments */
 | 
			
		||||
/* --- Responsive Adjustments --- */
 | 
			
		||||
@media (max-width: 500px) {
 | 
			
		||||
  body {
 | 
			
		||||
    margin: 10px;
 | 
			
		||||
@@ -105,10 +109,10 @@ h1, h2 {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Toast notifications */
 | 
			
		||||
/* --- Toast Notifications --- */
 | 
			
		||||
#toast-container {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 1rem;
 | 
			
		||||
  bottom: 1rem;
 | 
			
		||||
  right: 1rem;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
@@ -120,12 +124,13 @@ h1, h2 {
 | 
			
		||||
  background: rgba(0,0,0,0.85);
 | 
			
		||||
  color: white;
 | 
			
		||||
  padding: 0.75rem 1.25rem;
 | 
			
		||||
  border-radius: 0.5rem;
 | 
			
		||||
  border-radius: 30px;
 | 
			
		||||
  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
  transform: translateY(-20px);
 | 
			
		||||
  transform: translateY(20px);
 | 
			
		||||
  transition: opacity 0.5s ease, transform 0.5s ease;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  backdrop-filter: blur(20px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toast.show {
 | 
			
		||||
@@ -133,10 +138,10 @@ h1, h2 {
 | 
			
		||||
  transform: translateY(0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.toast.success { background-color: #28a745; }
 | 
			
		||||
.toast.success { background-color: #28a7468c; }
 | 
			
		||||
.toast.error { background-color: #dc3545; }
 | 
			
		||||
 | 
			
		||||
/* Tags */
 | 
			
		||||
/* --- Tags --- */
 | 
			
		||||
.tag-input {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap-reverse;
 | 
			
		||||
@@ -184,9 +189,9 @@ h1, h2 {
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  max-height: 150px;
 | 
			
		||||
  overflow-y: auto;
 | 
			
		||||
  z-index: 999; /* ensure it displays above other elements */
 | 
			
		||||
  z-index: 999;
 | 
			
		||||
  display: none;
 | 
			
		||||
  box-shadow: 0 4px 8px rgba(0,0,0,0.15); /* subtle shadow for visibility */
 | 
			
		||||
  box-shadow: 0 4px 8px rgba(0,0,0,0.15);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag-input ul.suggestions li {
 | 
			
		||||
@@ -214,6 +219,7 @@ h1, h2 {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Flex Utilities --- */
 | 
			
		||||
.flex-item {
 | 
			
		||||
  display: flex;
 | 
			
		||||
}
 | 
			
		||||
@@ -232,8 +238,7 @@ h1, h2 {
 | 
			
		||||
  width: 100%
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Top bar */
 | 
			
		||||
 | 
			
		||||
/* --- Top Bar & Navigation --- */
 | 
			
		||||
.nav {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
@@ -246,7 +251,6 @@ h1, h2 {
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  backdrop-filter:  blur(20px);
 | 
			
		||||
  border-bottom: 1px solid #21212157;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav img {
 | 
			
		||||
@@ -281,7 +285,6 @@ h1, h2 {
 | 
			
		||||
 | 
			
		||||
.nav-item {
 | 
			
		||||
  display: inline;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-list {
 | 
			
		||||
@@ -297,7 +300,6 @@ h1, h2 {
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  color:#fff
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav > .nav-links > .nav-list > .nav-item > a:hover {
 | 
			
		||||
@@ -308,7 +310,6 @@ h1, h2 {
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.nav-list > li + li::before{
 | 
			
		||||
  content: " → ";
 | 
			
		||||
  color: #ffc700;
 | 
			
		||||
@@ -350,8 +351,8 @@ h1, h2 {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Custom upload buttons */
 | 
			
		||||
.custom-upload-btn {
 | 
			
		||||
/* --- Custom Upload Buttons --- */
 | 
			
		||||
.up-btn {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  background: #09A0C1;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
@@ -363,13 +364,15 @@ h1, h2 {
 | 
			
		||||
  transition: all 0.1s ease;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  box-shadow: 0 4px 10px rgba(0,0,0,0.25);
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  border: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-upload-btn:hover {
 | 
			
		||||
.up-btn:hover {
 | 
			
		||||
  background: #55c3ec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Modal styles */
 | 
			
		||||
/* --- Modal Styles --- */
 | 
			
		||||
.modal {
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0; left: 0; right: 0; bottom: 0;
 | 
			
		||||
@@ -377,8 +380,8 @@ h1, h2 {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-content {
 | 
			
		||||
  background: #ffffff29;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
@@ -391,6 +394,7 @@ h1, h2 {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  backdrop-filter: blur(20px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-close {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 12px; right: 18px;
 | 
			
		||||
@@ -400,12 +404,14 @@ h1, h2 {
 | 
			
		||||
  opacity: 0.7;
 | 
			
		||||
}
 | 
			
		||||
.modal-close:hover { opacity: 1; }
 | 
			
		||||
 | 
			
		||||
.modal-actions {
 | 
			
		||||
  margin-top: 2rem;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 1rem;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-btn {
 | 
			
		||||
  padding: 0.5em 1.5em;
 | 
			
		||||
  border-radius: 30px;
 | 
			
		||||
@@ -417,12 +423,44 @@ h1, h2 {
 | 
			
		||||
  font-size: 1rem;
 | 
			
		||||
  transition: background 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-btn.danger {
 | 
			
		||||
  background: #c62828;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-btn:hover {
 | 
			
		||||
  background: #55c3ec;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.modal-btn.danger:hover {
 | 
			
		||||
  background: #d32f2f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* --- Upload Actions Row --- */
 | 
			
		||||
.upload-actions-row {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 16px;
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Remove All Buttons */
 | 
			
		||||
#remove-all-hero, #remove-all-gallery {
 | 
			
		||||
  background: rgb(121, 26, 19);
 | 
			
		||||
  color: white;
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#remove-all-gallery:hover,
 | 
			
		||||
#remove-all-hero:hover {
 | 
			
		||||
  background: #d32f2f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Responsive: stack buttons vertically on small screens */
 | 
			
		||||
@media (max-width: 500px) {
 | 
			
		||||
  .upload-actions-row {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: stretch;
 | 
			
		||||
    gap: 8px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user