2.0 - WebUI builder ("Cielight" merge) #9
@@ -2,49 +2,64 @@ import logging
 | 
				
			|||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from flask import Blueprint, request, current_app
 | 
					from flask import Blueprint, request, current_app
 | 
				
			||||||
from werkzeug.utils import secure_filename
 | 
					from werkzeug.utils import secure_filename
 | 
				
			||||||
 | 
					from src.py.builder.gallery_builder import update_gallery, update_hero
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Create a Flask blueprint for upload routes
 | 
					# --- Create Flask blueprint for upload routes ---
 | 
				
			||||||
upload_bp = Blueprint("upload", __name__)
 | 
					upload_bp = Blueprint("upload", __name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Allowed file extensions for uploads
 | 
					# --- Allowed file types ---
 | 
				
			||||||
ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
 | 
					ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Function to check if a file has an allowed extension
 | 
					 | 
				
			||||||
def allowed_file(filename: str) -> bool:
 | 
					def allowed_file(filename: str) -> bool:
 | 
				
			||||||
 | 
					    """Check if the uploaded file has an allowed extension."""
 | 
				
			||||||
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
 | 
					    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Function to save uploaded file to a given folder
 | 
					 | 
				
			||||||
def save_uploaded_file(file, folder: Path):
 | 
					def save_uploaded_file(file, folder: Path):
 | 
				
			||||||
    folder.mkdir(parents=True, exist_ok=True)  # Create folder if it doesn't exist
 | 
					    """Save an uploaded file to the specified folder."""
 | 
				
			||||||
 | 
					    folder.mkdir(parents=True, exist_ok=True)  # Create folder if not exists
 | 
				
			||||||
    filename = secure_filename(file.filename)  # Sanitize filename
 | 
					    filename = secure_filename(file.filename)  # Sanitize filename
 | 
				
			||||||
    file.save(folder / filename)  # Save file to folder
 | 
					    file.save(folder / filename)  # Save to disk
 | 
				
			||||||
    logging.info(f"[✓] Uploaded {filename} to {folder}")
 | 
					    logging.info(f"[✓] Uploaded {filename} to {folder}")
 | 
				
			||||||
    return filename  # Return saved filename
 | 
					    return filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Route to handle photo uploads for gallery or hero
 | 
					 | 
				
			||||||
@upload_bp.route("/api/<section>/upload", methods=["POST"])
 | 
					@upload_bp.route("/api/<section>/upload", methods=["POST"])
 | 
				
			||||||
def upload_photo(section: str):
 | 
					def upload_photo(section: str):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Handle file uploads for gallery or hero section.
 | 
				
			||||||
 | 
					    Accepts multiple files under 'files'.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
    # Validate section
 | 
					    # Validate section
 | 
				
			||||||
    if section not in ["gallery", "hero"]:
 | 
					    if section not in ["gallery", "hero"]:
 | 
				
			||||||
        return {"error": "Invalid section"}, 400
 | 
					        return {"error": "Invalid section"}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check if the request contains a file
 | 
					    # Check if files are provided
 | 
				
			||||||
    if "file" not in request.files:
 | 
					    if "files" not in request.files:
 | 
				
			||||||
        return {"error": "No file part"}, 400
 | 
					        return {"error": "No files provided"}, 400
 | 
				
			||||||
    file = request.files["file"]
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    # Check if a file was actually selected
 | 
					    files = request.files.getlist("files")
 | 
				
			||||||
    if file.filename == "":
 | 
					    if not files:
 | 
				
			||||||
        return {"error": "No selected file"}, 400
 | 
					        return {"error": "No selected files"}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Check file type and save it
 | 
					    # Get photos directory from app config
 | 
				
			||||||
    if file and allowed_file(file.filename):
 | 
					    PHOTOS_DIR = current_app.config.get("PHOTOS_DIR")
 | 
				
			||||||
        PHOTOS_DIR = current_app.config.get("PHOTOS_DIR")  # Get base photos directory from config
 | 
					    if not PHOTOS_DIR:
 | 
				
			||||||
        if not PHOTOS_DIR:
 | 
					        return {"error": "Server misconfiguration"}, 500
 | 
				
			||||||
            return {"error": "Server misconfiguration"}, 500
 | 
					 | 
				
			||||||
        folder = PHOTOS_DIR / section  # Target folder (gallery or hero)
 | 
					 | 
				
			||||||
        filename = save_uploaded_file(file, folder)
 | 
					 | 
				
			||||||
        return {"status": "ok", "filename": filename}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # If file type is not allowed
 | 
					    folder = PHOTOS_DIR / section  # Target folder
 | 
				
			||||||
    return {"error": "File type not allowed"}, 400
 | 
					    uploaded = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Save each valid file
 | 
				
			||||||
 | 
					    for file in files:
 | 
				
			||||||
 | 
					        if file and allowed_file(file.filename):
 | 
				
			||||||
 | 
					            filename = save_uploaded_file(file, folder)
 | 
				
			||||||
 | 
					            uploaded.append(filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Update YAML if any files were uploaded
 | 
				
			||||||
 | 
					    if uploaded:
 | 
				
			||||||
 | 
					        if section == "gallery":
 | 
				
			||||||
 | 
					            update_gallery()
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            update_hero()
 | 
				
			||||||
 | 
					        return {"status": "ok", "uploaded": uploaded}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {"error": "No valid files uploaded"}, 400
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,111 +2,106 @@ import logging
 | 
				
			|||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
from flask import Flask, jsonify, request, send_from_directory, render_template
 | 
					from flask import Flask, jsonify, request, send_from_directory, render_template
 | 
				
			||||||
from src.py.builder.gallery_builder import (
 | 
					from src.py.builder.gallery_builder import (
 | 
				
			||||||
    GALLERY_YAML, GALLERY_DIR, HERO_DIR,
 | 
					    GALLERY_YAML, load_yaml, save_yaml, update_gallery, update_hero
 | 
				
			||||||
    load_yaml, save_yaml, get_all_image_paths, update_gallery, update_hero
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from src.py.webui.upload import upload_bp
 | 
					from src.py.webui.upload import upload_bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Logging configuration ---
 | 
					# --- Logging configuration ---
 | 
				
			||||||
# Logs messages to console with INFO level
 | 
					 | 
				
			||||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
					logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Flask app setup ---
 | 
					# --- Flask app setup ---
 | 
				
			||||||
# WEBUI_PATH points to the web UI folder (templates + static)
 | 
					WEBUI_PATH = Path(__file__).parents[2] / "webui"  # Path to static/templates
 | 
				
			||||||
WEBUI_PATH = Path(__file__).parents[2] / "webui"
 | 
					 | 
				
			||||||
app = Flask(
 | 
					app = Flask(
 | 
				
			||||||
    __name__,
 | 
					    __name__,
 | 
				
			||||||
    template_folder=WEBUI_PATH,  # where Flask looks for templates
 | 
					    template_folder=WEBUI_PATH,
 | 
				
			||||||
    static_folder=WEBUI_PATH,    # where Flask serves static files
 | 
					    static_folder=WEBUI_PATH,
 | 
				
			||||||
    static_url_path=""            # URL path prefix for static files
 | 
					    static_url_path=""
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Absolute photos directory ---
 | 
					# --- Photos directory (configurable) ---
 | 
				
			||||||
# Used by upload.py and deletion endpoints
 | 
					 | 
				
			||||||
PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
					PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
				
			||||||
app.config["PHOTOS_DIR"] = PHOTOS_DIR
 | 
					app.config["PHOTOS_DIR"] = PHOTOS_DIR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Register upload blueprint ---
 | 
					# --- Register upload blueprint ---
 | 
				
			||||||
# Handles /api/<section>/upload endpoints for gallery and hero images
 | 
					 | 
				
			||||||
app.register_blueprint(upload_bp)
 | 
					app.register_blueprint(upload_bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Existing API routes ---
 | 
					# --- Routes ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Serve main page
 | 
					 | 
				
			||||||
@app.route("/")
 | 
					@app.route("/")
 | 
				
			||||||
def index():
 | 
					def index():
 | 
				
			||||||
 | 
					    """Serve the main HTML page."""
 | 
				
			||||||
    return render_template("index.html")
 | 
					    return render_template("index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Get gallery images (returns JSON array)
 | 
					 | 
				
			||||||
@app.route("/api/gallery", methods=["GET"])
 | 
					@app.route("/api/gallery", methods=["GET"])
 | 
				
			||||||
def get_gallery():
 | 
					def get_gallery():
 | 
				
			||||||
 | 
					    """Return JSON list of gallery images from YAML."""
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    return jsonify(data.get("gallery", {}).get("images", []))
 | 
					    return jsonify(data.get("gallery", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Get hero images (returns JSON array)
 | 
					 | 
				
			||||||
@app.route("/api/hero", methods=["GET"])
 | 
					@app.route("/api/hero", methods=["GET"])
 | 
				
			||||||
def get_hero():
 | 
					def get_hero():
 | 
				
			||||||
 | 
					    """Return JSON list of hero images from YAML."""
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    return jsonify(data.get("hero", {}).get("images", []))
 | 
					    return jsonify(data.get("hero", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Update gallery images with new JSON data
 | 
					 | 
				
			||||||
@app.route("/api/gallery/update", methods=["POST"])
 | 
					@app.route("/api/gallery/update", methods=["POST"])
 | 
				
			||||||
def update_gallery_api():
 | 
					def update_gallery_api():
 | 
				
			||||||
 | 
					    """Update gallery images in YAML from frontend JSON."""
 | 
				
			||||||
    images = request.json
 | 
					    images = request.json
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["gallery"]["images"] = images
 | 
					    data["gallery"]["images"] = images
 | 
				
			||||||
    save_yaml(data, GALLERY_YAML)
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Update hero images with new JSON data
 | 
					 | 
				
			||||||
@app.route("/api/hero/update", methods=["POST"])
 | 
					@app.route("/api/hero/update", methods=["POST"])
 | 
				
			||||||
def update_hero_api():
 | 
					def update_hero_api():
 | 
				
			||||||
 | 
					    """Update hero images in YAML from frontend JSON."""
 | 
				
			||||||
    images = request.json
 | 
					    images = request.json
 | 
				
			||||||
    data = load_yaml(GALLERY_YAML)
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
    data["hero"]["images"] = images
 | 
					    data["hero"]["images"] = images
 | 
				
			||||||
    save_yaml(data, GALLERY_YAML)
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Refresh gallery from the folder (rebuild YAML)
 | 
					 | 
				
			||||||
@app.route("/api/gallery/refresh", methods=["POST"])
 | 
					@app.route("/api/gallery/refresh", methods=["POST"])
 | 
				
			||||||
def refresh_gallery():
 | 
					def refresh_gallery():
 | 
				
			||||||
 | 
					    """Refresh gallery YAML from photos/gallery folder."""
 | 
				
			||||||
    update_gallery()
 | 
					    update_gallery()
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Refresh hero images from the folder
 | 
					 | 
				
			||||||
@app.route("/api/hero/refresh", methods=["POST"])
 | 
					@app.route("/api/hero/refresh", methods=["POST"])
 | 
				
			||||||
def refresh_hero():
 | 
					def refresh_hero():
 | 
				
			||||||
 | 
					    """Refresh hero YAML from photos/hero folder."""
 | 
				
			||||||
    update_hero()
 | 
					    update_hero()
 | 
				
			||||||
    return jsonify({"status": "ok"})
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Delete a gallery image file
 | 
					 | 
				
			||||||
@app.route("/api/gallery/delete", methods=["POST"])
 | 
					@app.route("/api/gallery/delete", methods=["POST"])
 | 
				
			||||||
def delete_gallery_photo():
 | 
					def delete_gallery_photo():
 | 
				
			||||||
 | 
					    """Delete a gallery photo from disk and return status."""
 | 
				
			||||||
    data = request.json
 | 
					    data = request.json
 | 
				
			||||||
    src = data.get("src")  # filename only
 | 
					    src = data.get("src")
 | 
				
			||||||
    file_path = PHOTOS_DIR / "gallery" / src
 | 
					    file_path = PHOTOS_DIR / "gallery" / src
 | 
				
			||||||
    if file_path.exists():
 | 
					    if file_path.exists():
 | 
				
			||||||
        file_path.unlink()  # remove the file
 | 
					        file_path.unlink()
 | 
				
			||||||
        return {"status": "ok"}
 | 
					        return {"status": "ok"}
 | 
				
			||||||
    return {"error": "File not found"}, 404
 | 
					    return {"error": "File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Delete a hero image file
 | 
					 | 
				
			||||||
@app.route("/api/hero/delete", methods=["POST"])
 | 
					@app.route("/api/hero/delete", methods=["POST"])
 | 
				
			||||||
def delete_hero_photo():
 | 
					def delete_hero_photo():
 | 
				
			||||||
 | 
					    """Delete a hero photo from disk and return status."""
 | 
				
			||||||
    data = request.json
 | 
					    data = request.json
 | 
				
			||||||
    src = data.get("src")  # filename only
 | 
					    src = data.get("src")
 | 
				
			||||||
    file_path = PHOTOS_DIR / "hero" / src
 | 
					    file_path = PHOTOS_DIR / "hero" / src
 | 
				
			||||||
    if file_path.exists():
 | 
					    if file_path.exists():
 | 
				
			||||||
        file_path.unlink()  # remove the file
 | 
					        file_path.unlink()
 | 
				
			||||||
        return {"status": "ok"}
 | 
					        return {"status": "ok"}
 | 
				
			||||||
    return {"error": "File not found"}, 404
 | 
					    return {"error": "File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Serve photos from /photos/<section>/<filename>
 | 
					 | 
				
			||||||
@app.route("/photos/<section>/<path:filename>")
 | 
					@app.route("/photos/<section>/<path:filename>")
 | 
				
			||||||
def photos(section, filename):
 | 
					def photos(section, filename):
 | 
				
			||||||
 | 
					    """Serve uploaded photos from disk."""
 | 
				
			||||||
    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
					    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# --- Main entry point ---
 | 
					# --- Run server ---
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
					    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
				
			||||||
    app.run(debug=True)
 | 
					    app.run(debug=True)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,11 +3,14 @@
 | 
				
			|||||||
<head>
 | 
					<head>
 | 
				
			||||||
  <meta charset="UTF-8">
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
  <title>Photo WebUI</title>
 | 
					  <title>Photo WebUI</title>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Link to your CSS in the package -->
 | 
				
			||||||
  <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>
 | 
				
			||||||
  <h1>Photo WebUI</h1>
 | 
					  <h1>Photo WebUI</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Toolbar with refresh and save buttons -->
 | 
				
			||||||
  <div class="toolbar">
 | 
					  <div class="toolbar">
 | 
				
			||||||
    <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
 | 
					    <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
 | 
				
			||||||
    <button onclick="refreshHero()">🔄 Refresh Hero</button>
 | 
					    <button onclick="refreshHero()">🔄 Refresh Hero</button>
 | 
				
			||||||
@@ -19,7 +22,7 @@
 | 
				
			|||||||
    <h2>Gallery</h2>
 | 
					    <h2>Gallery</h2>
 | 
				
			||||||
    <label>
 | 
					    <label>
 | 
				
			||||||
      Upload Image:
 | 
					      Upload Image:
 | 
				
			||||||
      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp">
 | 
					      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp" multiple>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
    <div id="gallery"></div>
 | 
					    <div id="gallery"></div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
@@ -29,10 +32,12 @@
 | 
				
			|||||||
    <h2>Hero</h2>
 | 
					    <h2>Hero</h2>
 | 
				
			||||||
    <label>
 | 
					    <label>
 | 
				
			||||||
      Upload Image:
 | 
					      Upload Image:
 | 
				
			||||||
      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp">
 | 
					      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp" multiple>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
    <div id="hero"></div>
 | 
					    <div id="hero"></div>
 | 
				
			||||||
  </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/main.js') }}"></script>
 | 
				
			||||||
  <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
					  <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,29 +1,27 @@
 | 
				
			|||||||
// Arrays to store gallery and hero images
 | 
					// --- Arrays to store gallery and hero images ---
 | 
				
			||||||
let galleryImages = [];
 | 
					let galleryImages = [];
 | 
				
			||||||
let heroImages = [];
 | 
					let heroImages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Load images data from server
 | 
					// --- Load images from server on page load ---
 | 
				
			||||||
async function loadData() {
 | 
					async function loadData() {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Fetch gallery images from API
 | 
					 | 
				
			||||||
    const galleryRes = await fetch('/api/gallery');
 | 
					    const galleryRes = await fetch('/api/gallery');
 | 
				
			||||||
    galleryImages = await galleryRes.json();
 | 
					    galleryImages = await galleryRes.json();
 | 
				
			||||||
    renderGallery(); // Render gallery images
 | 
					    renderGallery();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Fetch hero images from API
 | 
					 | 
				
			||||||
    const heroRes = await fetch('/api/hero');
 | 
					    const heroRes = await fetch('/api/hero');
 | 
				
			||||||
    heroImages = await heroRes.json();
 | 
					    heroImages = await heroRes.json();
 | 
				
			||||||
    renderHero(); // Render hero images
 | 
					    renderHero();
 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err);
 | 
				
			||||||
    alert("Error while loading images!");
 | 
					    alert("Error loading images!");
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Render gallery images in the page
 | 
					// --- Render gallery images with tags and delete buttons ---
 | 
				
			||||||
function renderGallery() {
 | 
					function renderGallery() {
 | 
				
			||||||
  const container = document.getElementById('gallery');
 | 
					  const container = document.getElementById('gallery');
 | 
				
			||||||
  container.innerHTML = ''; // Clear current content
 | 
					  container.innerHTML = '';
 | 
				
			||||||
  galleryImages.forEach((img, i) => {
 | 
					  galleryImages.forEach((img, i) => {
 | 
				
			||||||
    const div = document.createElement('div');
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
    div.className = 'photo';
 | 
					    div.className = 'photo';
 | 
				
			||||||
@@ -33,14 +31,14 @@ function renderGallery() {
 | 
				
			|||||||
        onchange="updateTags(${i}, this.value)">
 | 
					        onchange="updateTags(${i}, this.value)">
 | 
				
			||||||
      <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
					      <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div); // Add image div to container
 | 
					    container.appendChild(div);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Render hero images in the page
 | 
					// --- Render hero images with delete buttons ---
 | 
				
			||||||
function renderHero() {
 | 
					function renderHero() {
 | 
				
			||||||
  const container = document.getElementById('hero');
 | 
					  const container = document.getElementById('hero');
 | 
				
			||||||
  container.innerHTML = ''; // Clear current content
 | 
					  container.innerHTML = '';
 | 
				
			||||||
  heroImages.forEach((img, i) => {
 | 
					  heroImages.forEach((img, i) => {
 | 
				
			||||||
    const div = document.createElement('div');
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
    div.className = 'photo';
 | 
					    div.className = 'photo';
 | 
				
			||||||
@@ -48,104 +46,93 @@ function renderHero() {
 | 
				
			|||||||
      <img src="/photos/${img.src}">
 | 
					      <img src="/photos/${img.src}">
 | 
				
			||||||
      <button onclick="deleteHeroImage(${i})">🗑 Delete</button>
 | 
					      <button onclick="deleteHeroImage(${i})">🗑 Delete</button>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
    container.appendChild(div); // Add image div to container
 | 
					    container.appendChild(div);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Update tags for a gallery image
 | 
					// --- Update tags for gallery image ---
 | 
				
			||||||
function updateTags(index, value) {
 | 
					function updateTags(index, value) {
 | 
				
			||||||
  // Split tags by comma, trim spaces, and remove empty values
 | 
					 | 
				
			||||||
  galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
 | 
					  galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Delete a gallery image
 | 
					// --- Delete gallery image ---
 | 
				
			||||||
async function deleteGalleryImage(index) {
 | 
					async function deleteGalleryImage(index) {
 | 
				
			||||||
  const img = galleryImages[index];
 | 
					  const img = galleryImages[index];
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Send only the filename to the server for deletion
 | 
					 | 
				
			||||||
    const res = await fetch('/api/gallery/delete', {
 | 
					    const res = await fetch('/api/gallery/delete', {
 | 
				
			||||||
      method: 'POST',
 | 
					      method: 'POST',
 | 
				
			||||||
      headers: { 'Content-Type': 'application/json' },
 | 
					      headers: {'Content-Type': 'application/json'},
 | 
				
			||||||
      body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
 | 
					      body: JSON.stringify({ src: img.src.split('/').pop() })
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const data = await res.json();
 | 
					    const data = await res.json();
 | 
				
			||||||
    if (res.ok) {
 | 
					    if (res.ok) {
 | 
				
			||||||
      // Remove image from array and re-render gallery
 | 
					 | 
				
			||||||
      galleryImages.splice(index, 1);
 | 
					      galleryImages.splice(index, 1);
 | 
				
			||||||
      renderGallery();
 | 
					      renderGallery();
 | 
				
			||||||
      await saveGallery(); // Save updated tags
 | 
					      await saveGallery();
 | 
				
			||||||
    } else {
 | 
					    } else alert("Error: " + data.error);
 | 
				
			||||||
      alert("Error: " + data.error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err); alert("Server error!");
 | 
				
			||||||
    alert("Server error!");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Delete a hero image
 | 
					// --- Delete hero image ---
 | 
				
			||||||
async function deleteHeroImage(index) {
 | 
					async function deleteHeroImage(index) {
 | 
				
			||||||
  const img = heroImages[index];
 | 
					  const img = heroImages[index];
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Send only the filename to the server for deletion
 | 
					 | 
				
			||||||
    const res = await fetch('/api/hero/delete', {
 | 
					    const res = await fetch('/api/hero/delete', {
 | 
				
			||||||
      method: 'POST',
 | 
					      method: 'POST',
 | 
				
			||||||
      headers: { 'Content-Type': 'application/json' },
 | 
					      headers: {'Content-Type': 'application/json'},
 | 
				
			||||||
      body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
 | 
					      body: JSON.stringify({ src: img.src.split('/').pop() })
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    const data = await res.json();
 | 
					    const data = await res.json();
 | 
				
			||||||
    if (res.ok) {
 | 
					    if (res.ok) {
 | 
				
			||||||
      // Remove image from array and re-render hero section
 | 
					 | 
				
			||||||
      heroImages.splice(index, 1);
 | 
					      heroImages.splice(index, 1);
 | 
				
			||||||
      renderHero();
 | 
					      renderHero();
 | 
				
			||||||
      await saveHero(); // Save updated hero images
 | 
					      await saveHero();
 | 
				
			||||||
    } else {
 | 
					    } else alert("Error: " + data.error);
 | 
				
			||||||
      alert("Error: " + data.error);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  } catch(err) {
 | 
					  } catch(err) {
 | 
				
			||||||
    console.error(err);
 | 
					    console.error(err); alert("Server error!");
 | 
				
			||||||
    alert("Server error!");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Save gallery images (with tags) to the server
 | 
					// --- Save gallery to server ---
 | 
				
			||||||
async function saveGallery() {
 | 
					async function saveGallery() {
 | 
				
			||||||
  await fetch('/api/gallery/update', {
 | 
					  await fetch('/api/gallery/update', {
 | 
				
			||||||
    method: 'POST',
 | 
					    method: 'POST',
 | 
				
			||||||
    headers: { 'Content-Type': 'application/json' },
 | 
					    headers: {'Content-Type': 'application/json'},
 | 
				
			||||||
    body: JSON.stringify(galleryImages)
 | 
					    body: JSON.stringify(galleryImages)
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Save hero images to the server
 | 
					// --- Save hero to server ---
 | 
				
			||||||
async function saveHero() {
 | 
					async function saveHero() {
 | 
				
			||||||
  await fetch('/api/hero/update', {
 | 
					  await fetch('/api/hero/update', {
 | 
				
			||||||
    method: 'POST',
 | 
					    method: 'POST',
 | 
				
			||||||
    headers: { 'Content-Type': 'application/json' },
 | 
					    headers: {'Content-Type': 'application/json'},
 | 
				
			||||||
    body: JSON.stringify(heroImages)
 | 
					    body: JSON.stringify(heroImages)
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Save both gallery and hero changes
 | 
					// --- Save all changes ---
 | 
				
			||||||
async function saveChanges() {
 | 
					async function saveChanges() {
 | 
				
			||||||
  await saveGallery();
 | 
					  await saveGallery();
 | 
				
			||||||
  await saveHero();
 | 
					  await saveHero();
 | 
				
			||||||
  alert('✅ Changes saved!');
 | 
					  alert('✅ Changes saved!');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Refresh gallery images from the server 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(); // Reload data after refresh
 | 
					  await loadData();
 | 
				
			||||||
  alert('🔄 Gallery updated from photos/gallery folder');
 | 
					  alert('🔄 Gallery updated from photos/gallery folder');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Refresh hero images from the server 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(); // Reload data after refresh
 | 
					  await loadData();
 | 
				
			||||||
  alert('🔄 Hero updated from photos/hero folder');
 | 
					  alert('🔄 Hero updated from photos/hero folder');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Initial load of images when page opens
 | 
					// --- Initialize ---
 | 
				
			||||||
loadData();
 | 
					loadData();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,61 +1,39 @@
 | 
				
			|||||||
// Upload handler for gallery images
 | 
					// --- Upload gallery images ---
 | 
				
			||||||
document.getElementById('upload-gallery').addEventListener('change', async (e) => {
 | 
					document.getElementById('upload-gallery').addEventListener('change', async (e) => {
 | 
				
			||||||
  const file = e.target.files[0];
 | 
					  const files = e.target.files;
 | 
				
			||||||
  if (!file) return; // Exit if no file is selected
 | 
					  if (!files.length) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Create a FormData object to send the file
 | 
					 | 
				
			||||||
  const formData = new FormData();
 | 
					  const formData = new FormData();
 | 
				
			||||||
  formData.append('file', file); // Key must match what upload.py expects
 | 
					  for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Send POST request to the gallery upload endpoint
 | 
					    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('✅ Gallery image uploaded!');
 | 
					      alert(`✅ ${data.uploaded.length} gallery image(s) uploaded!`);
 | 
				
			||||||
      refreshGallery(); // Refresh the gallery list from the server
 | 
					      refreshGallery();
 | 
				
			||||||
    } else {
 | 
					    } else alert('Error: ' + data.error);
 | 
				
			||||||
      alert('Error: ' + data.error); // Show server error if upload failed
 | 
					  } catch(err) {
 | 
				
			||||||
    }
 | 
					    console.error(err); alert('Server error!');
 | 
				
			||||||
  } catch (err) {
 | 
					  } finally { e.target.value = ''; }
 | 
				
			||||||
    console.error(err);
 | 
					 | 
				
			||||||
    alert('Server error!'); // Network or server failure
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    e.target.value = ''; // Reset file input so the same file can be uploaded again if needed
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Upload handler for hero images
 | 
					// --- Upload hero images ---
 | 
				
			||||||
document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
					document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
				
			||||||
  const file = e.target.files[0];
 | 
					  const files = e.target.files;
 | 
				
			||||||
  if (!file) return; // Exit if no file is selected
 | 
					  if (!files.length) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Create a FormData object to send the file
 | 
					 | 
				
			||||||
  const formData = new FormData();
 | 
					  const formData = new FormData();
 | 
				
			||||||
  formData.append('file', file); // Key must match what upload.py expects
 | 
					  for (const file of files) formData.append('files', file);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    // Send POST request to the hero upload endpoint
 | 
					    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('✅ Hero image uploaded!');
 | 
					      alert(`✅ ${data.uploaded.length} hero image(s) uploaded!`);
 | 
				
			||||||
      refreshHero(); // Refresh the hero list from the server
 | 
					      refreshHero();
 | 
				
			||||||
    } else {
 | 
					    } else alert('Error: ' + data.error);
 | 
				
			||||||
      alert('Error: ' + data.error); // Show server error if upload failed
 | 
					  } catch(err) {
 | 
				
			||||||
    }
 | 
					    console.error(err); alert('Server error!');
 | 
				
			||||||
  } catch (err) {
 | 
					  } finally { e.target.value = ''; }
 | 
				
			||||||
    console.error(err);
 | 
					 | 
				
			||||||
    alert('Server error!'); // Network or server failure
 | 
					 | 
				
			||||||
  } finally {
 | 
					 | 
				
			||||||
    e.target.value = ''; // Reset file input so the same file can be uploaded again if needed
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user