2.0 - WebUI builder ("Cielight" merge) #9
							
								
								
									
										2
									
								
								build.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								build.py
									
									
									
									
									
								
							@@ -1,5 +1,5 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
from src.py.site_builder import build
 | 
					from src.py.builder.site_builder import build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
					    logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import logging
 | 
					import logging
 | 
				
			||||||
from src.py.gallery_builder import update_gallery, update_hero
 | 
					from src.py.builder.gallery_builder import update_gallery, update_hero
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					if __name__ == "__main__":
 | 
				
			||||||
    logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
					    logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,2 +1,3 @@
 | 
				
			|||||||
pyyaml
 | 
					pyyaml
 | 
				
			||||||
pillow
 | 
					pillow
 | 
				
			||||||
 | 
					flask
 | 
				
			||||||
@@ -3,6 +3,7 @@ from pathlib import Path
 | 
				
			|||||||
from shutil import copyfile
 | 
					from shutil import copyfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_css_variables(colors_dict, output_path):
 | 
					def generate_css_variables(colors_dict, output_path):
 | 
				
			||||||
 | 
					    """Generate css variables for theme colors"""
 | 
				
			||||||
    css_lines = [":root {"]
 | 
					    css_lines = [":root {"]
 | 
				
			||||||
    for key, value in colors_dict.items():
 | 
					    for key, value in colors_dict.items():
 | 
				
			||||||
        css_lines.append(f"  --color-{key.replace('_', '-')}: {value};")
 | 
					        css_lines.append(f"  --color-{key.replace('_', '-')}: {value};")
 | 
				
			||||||
@@ -13,6 +14,7 @@ def generate_css_variables(colors_dict, output_path):
 | 
				
			|||||||
    logging.info(f"[✓] CSS variables written to {output_path}")
 | 
					    logging.info(f"[✓] CSS variables written to {output_path}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
 | 
					def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
 | 
				
			||||||
 | 
					    """Generate css variables fonts"""
 | 
				
			||||||
    font_files = list(fonts_dir.glob("*"))
 | 
					    font_files = list(fonts_dir.glob("*"))
 | 
				
			||||||
    font_faces = {}
 | 
					    font_faces = {}
 | 
				
			||||||
    preload_links = []
 | 
					    preload_links = []
 | 
				
			||||||
@@ -57,6 +59,7 @@ def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
 | 
				
			|||||||
    return preload_links
 | 
					    return preload_links
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_google_fonts_link(fonts):
 | 
					def generate_google_fonts_link(fonts):
 | 
				
			||||||
 | 
					    """Generate src link for Google fonts"""
 | 
				
			||||||
    if not fonts:
 | 
					    if not fonts:
 | 
				
			||||||
        return ""
 | 
					        return ""
 | 
				
			||||||
    families = []
 | 
					    families = []
 | 
				
			||||||
@@ -10,6 +10,7 @@ GALLERY_DIR = Path("config/photos/gallery")
 | 
				
			|||||||
HERO_DIR = Path("config/photos/hero")
 | 
					HERO_DIR = Path("config/photos/hero")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_yaml(path):
 | 
					def load_yaml(path):
 | 
				
			||||||
 | 
					    """Load gallery config .yaml file"""
 | 
				
			||||||
    print(f"[→] Loading {path}...")
 | 
					    print(f"[→] Loading {path}...")
 | 
				
			||||||
    if not os.path.exists(path):
 | 
					    if not os.path.exists(path):
 | 
				
			||||||
        print(f"[✗] File not found: {path}")
 | 
					        print(f"[✗] File not found: {path}")
 | 
				
			||||||
@@ -21,11 +22,13 @@ def load_yaml(path):
 | 
				
			|||||||
        return data
 | 
					        return data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def save_yaml(data, path):
 | 
					def save_yaml(data, path):
 | 
				
			||||||
 | 
					    """Save modified gallery config .yaml file"""
 | 
				
			||||||
    with open(path, "w", encoding="utf-8") as f:
 | 
					    with open(path, "w", encoding="utf-8") as f:
 | 
				
			||||||
        yaml.dump(data, f, sort_keys=False, allow_unicode=True)
 | 
					        yaml.dump(data, f, sort_keys=False, allow_unicode=True)
 | 
				
			||||||
    print(f"[✓] Saved updated YAML to {path}")
 | 
					    print(f"[✓] Saved updated YAML to {path}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_all_image_paths(directory):
 | 
					def get_all_image_paths(directory):
 | 
				
			||||||
 | 
					    """Get the path to record for builded site"""
 | 
				
			||||||
    return sorted([
 | 
					    return sorted([
 | 
				
			||||||
        str(p.relative_to(directory.parent)).replace("\\", "/")
 | 
					        str(p.relative_to(directory.parent)).replace("\\", "/")
 | 
				
			||||||
        for p in directory.rglob("*")
 | 
					        for p in directory.rglob("*")
 | 
				
			||||||
@@ -33,6 +36,7 @@ def get_all_image_paths(directory):
 | 
				
			|||||||
    ])
 | 
					    ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def update_gallery():
 | 
					def update_gallery():
 | 
				
			||||||
 | 
					    """Update the gallery photo list"""
 | 
				
			||||||
    print("\n=== Updating gallery.yaml (gallery section) ===")
 | 
					    print("\n=== Updating gallery.yaml (gallery section) ===")
 | 
				
			||||||
    gallery = load_yaml(GALLERY_YAML)
 | 
					    gallery = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -71,6 +75,7 @@ def update_gallery():
 | 
				
			|||||||
        print("[✓] No changes to gallery.yaml (gallery)")
 | 
					        print("[✓] No changes to gallery.yaml (gallery)")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def update_hero():
 | 
					def update_hero():
 | 
				
			||||||
 | 
					    """Update the hero photo list"""
 | 
				
			||||||
    print("\n=== Updating gallery.yaml (hero section) ===")
 | 
					    print("\n=== Updating gallery.yaml (hero section) ===")
 | 
				
			||||||
    gallery = load_yaml(GALLERY_YAML)
 | 
					    gallery = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3,6 +3,7 @@ import logging
 | 
				
			|||||||
from pathlib import Path
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_template(template_path, context):
 | 
					def render_template(template_path, context):
 | 
				
			||||||
 | 
					    """Render html templates"""
 | 
				
			||||||
    with open(template_path, encoding="utf-8") as f:
 | 
					    with open(template_path, encoding="utf-8") as f:
 | 
				
			||||||
        content = f.read()
 | 
					        content = f.read()
 | 
				
			||||||
    for key, value in context.items():
 | 
					    for key, value in context.items():
 | 
				
			||||||
@@ -11,6 +12,7 @@ def render_template(template_path, context):
 | 
				
			|||||||
    return content
 | 
					    return content
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def render_gallery_images(images):
 | 
					def render_gallery_images(images):
 | 
				
			||||||
 | 
					    """Render the photo gallery"""
 | 
				
			||||||
    html = ""
 | 
					    html = ""
 | 
				
			||||||
    for img in images:
 | 
					    for img in images:
 | 
				
			||||||
        tags = " ".join(img.get("tags", []))
 | 
					        tags = " ".join(img.get("tags", []))
 | 
				
			||||||
@@ -24,6 +26,7 @@ def render_gallery_images(images):
 | 
				
			|||||||
    return html
 | 
					    return html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_gallery_json_from_images(images, output_dir):
 | 
					def generate_gallery_json_from_images(images, output_dir):
 | 
				
			||||||
 | 
					    """Generte the hero carrousel photo list"""
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        img_list = [img["src"] for img in images]
 | 
					        img_list = [img["src"] for img in images]
 | 
				
			||||||
        output_path = output_dir / "data" / "gallery.json"
 | 
					        output_path = output_dir / "data" / "gallery.json"
 | 
				
			||||||
@@ -35,6 +38,7 @@ def generate_gallery_json_from_images(images, output_dir):
 | 
				
			|||||||
        logging.error(f"[✗] Error generating gallery JSON: {e}")
 | 
					        logging.error(f"[✗] Error generating gallery JSON: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
 | 
					def generate_robots_txt(canonical_url, allowed_paths, output_dir):
 | 
				
			||||||
 | 
					    """Generate the robot.txt"""
 | 
				
			||||||
    robots_lines = ["User-agent: *"]
 | 
					    robots_lines = ["User-agent: *"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Block everything by default
 | 
					    # Block everything by default
 | 
				
			||||||
@@ -62,6 +66,7 @@ def generate_robots_txt(canonical_url, allowed_paths, output_dir):
 | 
				
			|||||||
        logging.error(f"[✗] Failed to write robots.txt: {e}")
 | 
					        logging.error(f"[✗] Failed to write robots.txt: {e}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
 | 
					def generate_sitemap_xml(canonical_url, allowed_paths, output_dir):
 | 
				
			||||||
 | 
					    """Generate the sitemap"""
 | 
				
			||||||
    urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
 | 
					    urlset_start = '<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
 | 
				
			||||||
    urlset_end = '</urlset>\n'
 | 
					    urlset_end = '</urlset>\n'
 | 
				
			||||||
    urls = ""
 | 
					    urls = ""
 | 
				
			||||||
@@ -4,6 +4,7 @@ from pathlib import Path
 | 
				
			|||||||
from shutil import copytree, rmtree, copyfile
 | 
					from shutil import copytree, rmtree, copyfile
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_yaml(path):
 | 
					def load_yaml(path):
 | 
				
			||||||
 | 
					    """Load gallery and site .yaml conf"""
 | 
				
			||||||
    if not path.exists():
 | 
					    if not path.exists():
 | 
				
			||||||
        logging.warning(f"[!] YAML file not found: {path}")
 | 
					        logging.warning(f"[!] YAML file not found: {path}")
 | 
				
			||||||
        return {}
 | 
					        return {}
 | 
				
			||||||
@@ -11,6 +12,7 @@ def load_yaml(path):
 | 
				
			|||||||
        return yaml.safe_load(f)
 | 
					        return yaml.safe_load(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def load_theme_config(theme_name, themes_dir):
 | 
					def load_theme_config(theme_name, themes_dir):
 | 
				
			||||||
 | 
					    """Load theme.yaml"""
 | 
				
			||||||
    theme_dir = themes_dir / theme_name
 | 
					    theme_dir = themes_dir / theme_name
 | 
				
			||||||
    theme_config_path = theme_dir / "theme.yaml"
 | 
					    theme_config_path = theme_dir / "theme.yaml"
 | 
				
			||||||
    if not theme_config_path.exists():
 | 
					    if not theme_config_path.exists():
 | 
				
			||||||
@@ -20,26 +22,25 @@ def load_theme_config(theme_name, themes_dir):
 | 
				
			|||||||
    return theme_vars, theme_dir
 | 
					    return theme_vars, theme_dir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def clear_dir(path: Path):
 | 
					def clear_dir(path: Path):
 | 
				
			||||||
 | 
					    """Clear the output dir"""
 | 
				
			||||||
    if not path.exists():
 | 
					    if not path.exists():
 | 
				
			||||||
        path.mkdir(parents=True)
 | 
					        path.mkdir(parents=True)
 | 
				
			||||||
        return
 | 
					        return
 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Remove all files and subdirectories inside path, but not path itself
 | 
					 | 
				
			||||||
    for child in path.iterdir():
 | 
					    for child in path.iterdir():
 | 
				
			||||||
        if child.is_file() or child.is_symlink():
 | 
					        if child.is_file() or child.is_symlink():
 | 
				
			||||||
            child.unlink()  # delete file or symlink
 | 
					            child.unlink()
 | 
				
			||||||
        elif child.is_dir():
 | 
					        elif child.is_dir():
 | 
				
			||||||
            rmtree(child)  # delete directory and contents
 | 
					            rmtree(child)
 | 
				
			||||||
 | 
					 | 
				
			||||||
# Then replace your ensure_dir with this:
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def ensure_dir(path: Path):
 | 
					def ensure_dir(path: Path):
 | 
				
			||||||
 | 
					    """Create the output dir if it does not exist"""
 | 
				
			||||||
    if not path.exists():
 | 
					    if not path.exists():
 | 
				
			||||||
        path.mkdir(parents=True)
 | 
					        path.mkdir(parents=True)
 | 
				
			||||||
    else:
 | 
					    else:
 | 
				
			||||||
        clear_dir(path)
 | 
					        clear_dir(path)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def copy_assets(js_dir, style_dir, build_dir):
 | 
					def copy_assets(js_dir, style_dir, build_dir):
 | 
				
			||||||
 | 
					    """Copy public assets to output dir"""
 | 
				
			||||||
    for folder in [js_dir, style_dir]:
 | 
					    for folder in [js_dir, style_dir]:
 | 
				
			||||||
        if folder.exists():
 | 
					        if folder.exists():
 | 
				
			||||||
            dest = build_dir / folder.name
 | 
					            dest = build_dir / folder.name
 | 
				
			||||||
							
								
								
									
										0
									
								
								src/py/webui/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/py/webui/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										50
									
								
								src/py/webui/upload.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/py/webui/upload.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from flask import Blueprint, request, current_app
 | 
				
			||||||
 | 
					from werkzeug.utils import secure_filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Create a Flask blueprint for upload routes
 | 
				
			||||||
 | 
					upload_bp = Blueprint("upload", __name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Allowed file extensions for uploads
 | 
				
			||||||
 | 
					ALLOWED_EXTENSIONS = {"png", "jpg", "jpeg", "webp"}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Function to check if a file has an allowed extension
 | 
				
			||||||
 | 
					def allowed_file(filename: str) -> bool:
 | 
				
			||||||
 | 
					    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):
 | 
				
			||||||
 | 
					    folder.mkdir(parents=True, exist_ok=True)  # Create folder if it doesn't exist
 | 
				
			||||||
 | 
					    filename = secure_filename(file.filename)  # Sanitize filename
 | 
				
			||||||
 | 
					    file.save(folder / filename)  # Save file to folder
 | 
				
			||||||
 | 
					    logging.info(f"[✓] Uploaded {filename} to {folder}")
 | 
				
			||||||
 | 
					    return filename  # Return saved filename
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Route to handle photo uploads for gallery or hero
 | 
				
			||||||
 | 
					@upload_bp.route("/api/<section>/upload", methods=["POST"])
 | 
				
			||||||
 | 
					def upload_photo(section: str):
 | 
				
			||||||
 | 
					    # Validate section
 | 
				
			||||||
 | 
					    if section not in ["gallery", "hero"]:
 | 
				
			||||||
 | 
					        return {"error": "Invalid section"}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check if the request contains a file
 | 
				
			||||||
 | 
					    if "file" not in request.files:
 | 
				
			||||||
 | 
					        return {"error": "No file part"}, 400
 | 
				
			||||||
 | 
					    file = request.files["file"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check if a file was actually selected
 | 
				
			||||||
 | 
					    if file.filename == "":
 | 
				
			||||||
 | 
					        return {"error": "No selected file"}, 400
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Check file type and save it
 | 
				
			||||||
 | 
					    if file and allowed_file(file.filename):
 | 
				
			||||||
 | 
					        PHOTOS_DIR = current_app.config.get("PHOTOS_DIR")  # Get base photos directory from config
 | 
				
			||||||
 | 
					        if not PHOTOS_DIR:
 | 
				
			||||||
 | 
					            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
 | 
				
			||||||
 | 
					    return {"error": "File type not allowed"}, 400
 | 
				
			||||||
							
								
								
									
										112
									
								
								src/py/webui/webui.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/py/webui/webui.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					import logging
 | 
				
			||||||
 | 
					from pathlib import Path
 | 
				
			||||||
 | 
					from flask import Flask, jsonify, request, send_from_directory, render_template
 | 
				
			||||||
 | 
					from src.py.builder.gallery_builder import (
 | 
				
			||||||
 | 
					    GALLERY_YAML, GALLERY_DIR, HERO_DIR,
 | 
				
			||||||
 | 
					    load_yaml, save_yaml, get_all_image_paths, update_gallery, update_hero
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					from src.py.webui.upload import upload_bp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Logging configuration ---
 | 
				
			||||||
 | 
					# Logs messages to console with INFO level
 | 
				
			||||||
 | 
					logging.basicConfig(level=logging.INFO, format="%(message)s")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Flask app setup ---
 | 
				
			||||||
 | 
					# WEBUI_PATH points to the web UI folder (templates + static)
 | 
				
			||||||
 | 
					WEBUI_PATH = Path(__file__).parents[2] / "webui"
 | 
				
			||||||
 | 
					app = Flask(
 | 
				
			||||||
 | 
					    __name__,
 | 
				
			||||||
 | 
					    template_folder=WEBUI_PATH,  # where Flask looks for templates
 | 
				
			||||||
 | 
					    static_folder=WEBUI_PATH,    # where Flask serves static files
 | 
				
			||||||
 | 
					    static_url_path=""            # URL path prefix for static files
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Absolute photos directory ---
 | 
				
			||||||
 | 
					# Used by upload.py and deletion endpoints
 | 
				
			||||||
 | 
					PHOTOS_DIR = Path(__file__).resolve().parents[3] / "config" / "photos"
 | 
				
			||||||
 | 
					app.config["PHOTOS_DIR"] = PHOTOS_DIR
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Register upload blueprint ---
 | 
				
			||||||
 | 
					# Handles /api/<section>/upload endpoints for gallery and hero images
 | 
				
			||||||
 | 
					app.register_blueprint(upload_bp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Existing API routes ---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Serve main page
 | 
				
			||||||
 | 
					@app.route("/")
 | 
				
			||||||
 | 
					def index():
 | 
				
			||||||
 | 
					    return render_template("index.html")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Get gallery images (returns JSON array)
 | 
				
			||||||
 | 
					@app.route("/api/gallery", methods=["GET"])
 | 
				
			||||||
 | 
					def get_gallery():
 | 
				
			||||||
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					    return jsonify(data.get("gallery", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Get hero images (returns JSON array)
 | 
				
			||||||
 | 
					@app.route("/api/hero", methods=["GET"])
 | 
				
			||||||
 | 
					def get_hero():
 | 
				
			||||||
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					    return jsonify(data.get("hero", {}).get("images", []))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Update gallery images with new JSON data
 | 
				
			||||||
 | 
					@app.route("/api/gallery/update", methods=["POST"])
 | 
				
			||||||
 | 
					def update_gallery_api():
 | 
				
			||||||
 | 
					    images = request.json
 | 
				
			||||||
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					    data["gallery"]["images"] = images
 | 
				
			||||||
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Update hero images with new JSON data
 | 
				
			||||||
 | 
					@app.route("/api/hero/update", methods=["POST"])
 | 
				
			||||||
 | 
					def update_hero_api():
 | 
				
			||||||
 | 
					    images = request.json
 | 
				
			||||||
 | 
					    data = load_yaml(GALLERY_YAML)
 | 
				
			||||||
 | 
					    data["hero"]["images"] = images
 | 
				
			||||||
 | 
					    save_yaml(data, GALLERY_YAML)
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Refresh gallery from the folder (rebuild YAML)
 | 
				
			||||||
 | 
					@app.route("/api/gallery/refresh", methods=["POST"])
 | 
				
			||||||
 | 
					def refresh_gallery():
 | 
				
			||||||
 | 
					    update_gallery()
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Refresh hero images from the folder
 | 
				
			||||||
 | 
					@app.route("/api/hero/refresh", methods=["POST"])
 | 
				
			||||||
 | 
					def refresh_hero():
 | 
				
			||||||
 | 
					    update_hero()
 | 
				
			||||||
 | 
					    return jsonify({"status": "ok"})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Delete a gallery image file
 | 
				
			||||||
 | 
					@app.route("/api/gallery/delete", methods=["POST"])
 | 
				
			||||||
 | 
					def delete_gallery_photo():
 | 
				
			||||||
 | 
					    data = request.json
 | 
				
			||||||
 | 
					    src = data.get("src")  # filename only
 | 
				
			||||||
 | 
					    file_path = PHOTOS_DIR / "gallery" / src
 | 
				
			||||||
 | 
					    if file_path.exists():
 | 
				
			||||||
 | 
					        file_path.unlink()  # remove the file
 | 
				
			||||||
 | 
					        return {"status": "ok"}
 | 
				
			||||||
 | 
					    return {"error": "File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Delete a hero image file
 | 
				
			||||||
 | 
					@app.route("/api/hero/delete", methods=["POST"])
 | 
				
			||||||
 | 
					def delete_hero_photo():
 | 
				
			||||||
 | 
					    data = request.json
 | 
				
			||||||
 | 
					    src = data.get("src")  # filename only
 | 
				
			||||||
 | 
					    file_path = PHOTOS_DIR / "hero" / src
 | 
				
			||||||
 | 
					    if file_path.exists():
 | 
				
			||||||
 | 
					        file_path.unlink()  # remove the file
 | 
				
			||||||
 | 
					        return {"status": "ok"}
 | 
				
			||||||
 | 
					    return {"error": "File not found"}, 404
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Serve photos from /photos/<section>/<filename>
 | 
				
			||||||
 | 
					@app.route("/photos/<section>/<path:filename>")
 | 
				
			||||||
 | 
					def photos(section, filename):
 | 
				
			||||||
 | 
					    return send_from_directory(PHOTOS_DIR / section, filename)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# --- Main entry point ---
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    logging.info("Starting WebUI at http://127.0.0.1:5000")
 | 
				
			||||||
 | 
					    app.run(debug=True)
 | 
				
			||||||
							
								
								
									
										39
									
								
								src/webui/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/webui/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					  <meta charset="UTF-8">
 | 
				
			||||||
 | 
					  <title>Photo WebUI</title>
 | 
				
			||||||
 | 
					  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					  <h1>Photo WebUI</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="toolbar">
 | 
				
			||||||
 | 
					    <button onclick="refreshGallery()">🔄 Refresh Gallery</button>
 | 
				
			||||||
 | 
					    <button onclick="refreshHero()">🔄 Refresh Hero</button>
 | 
				
			||||||
 | 
					    <button onclick="saveChanges()">💾 Save Changes</button>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Gallery Upload Section -->
 | 
				
			||||||
 | 
					  <div class="upload-section">
 | 
				
			||||||
 | 
					    <h2>Gallery</h2>
 | 
				
			||||||
 | 
					    <label>
 | 
				
			||||||
 | 
					      Upload Image:
 | 
				
			||||||
 | 
					      <input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp">
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					    <div id="gallery"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <!-- Hero Upload Section -->
 | 
				
			||||||
 | 
					  <div class="upload-section">
 | 
				
			||||||
 | 
					    <h2>Hero</h2>
 | 
				
			||||||
 | 
					    <label>
 | 
				
			||||||
 | 
					      Upload Image:
 | 
				
			||||||
 | 
					      <input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp">
 | 
				
			||||||
 | 
					    </label>
 | 
				
			||||||
 | 
					    <div id="hero"></div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <script src="{{ url_for('static', filename='js/main.js') }}"></script>
 | 
				
			||||||
 | 
					  <script src="{{ url_for('static', filename='js/upload.js') }}"></script>
 | 
				
			||||||
 | 
					</body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
							
								
								
									
										151
									
								
								src/webui/js/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								src/webui/js/main.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					// Arrays to store gallery and hero images
 | 
				
			||||||
 | 
					let galleryImages = [];
 | 
				
			||||||
 | 
					let heroImages = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Load images data from server
 | 
				
			||||||
 | 
					async function loadData() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Fetch gallery images from API
 | 
				
			||||||
 | 
					    const galleryRes = await fetch('/api/gallery');
 | 
				
			||||||
 | 
					    galleryImages = await galleryRes.json();
 | 
				
			||||||
 | 
					    renderGallery(); // Render gallery images
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Fetch hero images from API
 | 
				
			||||||
 | 
					    const heroRes = await fetch('/api/hero');
 | 
				
			||||||
 | 
					    heroImages = await heroRes.json();
 | 
				
			||||||
 | 
					    renderHero(); // Render hero images
 | 
				
			||||||
 | 
					  } catch(err) {
 | 
				
			||||||
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    alert("Error while loading images!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Render gallery images in the page
 | 
				
			||||||
 | 
					function renderGallery() {
 | 
				
			||||||
 | 
					  const container = document.getElementById('gallery');
 | 
				
			||||||
 | 
					  container.innerHTML = ''; // Clear current content
 | 
				
			||||||
 | 
					  galleryImages.forEach((img, i) => {
 | 
				
			||||||
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
 | 
					    div.className = 'photo';
 | 
				
			||||||
 | 
					    div.innerHTML = `
 | 
				
			||||||
 | 
					      <img src="/photos/${img.src}">
 | 
				
			||||||
 | 
					      <input type="text" value="${(img.tags || []).join(', ')}"
 | 
				
			||||||
 | 
					        onchange="updateTags(${i}, this.value)">
 | 
				
			||||||
 | 
					      <button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    container.appendChild(div); // Add image div to container
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Render hero images in the page
 | 
				
			||||||
 | 
					function renderHero() {
 | 
				
			||||||
 | 
					  const container = document.getElementById('hero');
 | 
				
			||||||
 | 
					  container.innerHTML = ''; // Clear current content
 | 
				
			||||||
 | 
					  heroImages.forEach((img, i) => {
 | 
				
			||||||
 | 
					    const div = document.createElement('div');
 | 
				
			||||||
 | 
					    div.className = 'photo';
 | 
				
			||||||
 | 
					    div.innerHTML = `
 | 
				
			||||||
 | 
					      <img src="/photos/${img.src}">
 | 
				
			||||||
 | 
					      <button onclick="deleteHeroImage(${i})">🗑 Delete</button>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					    container.appendChild(div); // Add image div to container
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Update tags for a gallery image
 | 
				
			||||||
 | 
					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);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Delete a gallery image
 | 
				
			||||||
 | 
					async function deleteGalleryImage(index) {
 | 
				
			||||||
 | 
					  const img = galleryImages[index];
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Send only the filename to the server for deletion
 | 
				
			||||||
 | 
					    const res = await fetch('/api/gallery/delete', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					      body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
 | 
					    if (res.ok) {
 | 
				
			||||||
 | 
					      // Remove image from array and re-render gallery
 | 
				
			||||||
 | 
					      galleryImages.splice(index, 1);
 | 
				
			||||||
 | 
					      renderGallery();
 | 
				
			||||||
 | 
					      await saveGallery(); // Save updated tags
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      alert("Error: " + data.error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch(err) {
 | 
				
			||||||
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    alert("Server error!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Delete a hero image
 | 
				
			||||||
 | 
					async function deleteHeroImage(index) {
 | 
				
			||||||
 | 
					  const img = heroImages[index];
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Send only the filename to the server for deletion
 | 
				
			||||||
 | 
					    const res = await fetch('/api/hero/delete', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					      body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
 | 
					    if (res.ok) {
 | 
				
			||||||
 | 
					      // Remove image from array and re-render hero section
 | 
				
			||||||
 | 
					      heroImages.splice(index, 1);
 | 
				
			||||||
 | 
					      renderHero();
 | 
				
			||||||
 | 
					      await saveHero(); // Save updated hero images
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      alert("Error: " + data.error);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch(err) {
 | 
				
			||||||
 | 
					    console.error(err);
 | 
				
			||||||
 | 
					    alert("Server error!");
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Save gallery images (with tags) to the server
 | 
				
			||||||
 | 
					async function saveGallery() {
 | 
				
			||||||
 | 
					  await fetch('/api/gallery/update', {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					    body: JSON.stringify(galleryImages)
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Save hero images to the server
 | 
				
			||||||
 | 
					async function saveHero() {
 | 
				
			||||||
 | 
					  await fetch('/api/hero/update', {
 | 
				
			||||||
 | 
					    method: 'POST',
 | 
				
			||||||
 | 
					    headers: { 'Content-Type': 'application/json' },
 | 
				
			||||||
 | 
					    body: JSON.stringify(heroImages)
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Save both gallery and hero changes
 | 
				
			||||||
 | 
					async function saveChanges() {
 | 
				
			||||||
 | 
					  await saveGallery();
 | 
				
			||||||
 | 
					  await saveHero();
 | 
				
			||||||
 | 
					  alert('✅ Changes saved!');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Refresh gallery images from the server folder
 | 
				
			||||||
 | 
					async function refreshGallery() {
 | 
				
			||||||
 | 
					  await fetch('/api/gallery/refresh', { method: 'POST' });
 | 
				
			||||||
 | 
					  await loadData(); // Reload data after refresh
 | 
				
			||||||
 | 
					  alert('🔄 Gallery updated from photos/gallery folder');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Refresh hero images from the server folder
 | 
				
			||||||
 | 
					async function refreshHero() {
 | 
				
			||||||
 | 
					  await fetch('/api/hero/refresh', { method: 'POST' });
 | 
				
			||||||
 | 
					  await loadData(); // Reload data after refresh
 | 
				
			||||||
 | 
					  alert('🔄 Hero updated from photos/hero folder');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Initial load of images when page opens
 | 
				
			||||||
 | 
					loadData();
 | 
				
			||||||
							
								
								
									
										61
									
								
								src/webui/js/upload.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/webui/js/upload.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
				
			|||||||
 | 
					// Upload handler for gallery images
 | 
				
			||||||
 | 
					document.getElementById('upload-gallery').addEventListener('change', async (e) => {
 | 
				
			||||||
 | 
					  const file = e.target.files[0];
 | 
				
			||||||
 | 
					  if (!file) return; // Exit if no file is selected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create a FormData object to send the file
 | 
				
			||||||
 | 
					  const formData = new FormData();
 | 
				
			||||||
 | 
					  formData.append('file', file); // Key must match what upload.py expects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Send POST request to the gallery upload endpoint
 | 
				
			||||||
 | 
					    const res = await fetch('/api/gallery/upload', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      body: formData
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
 | 
					    if (res.ok) {
 | 
				
			||||||
 | 
					      alert('✅ Gallery image uploaded!');
 | 
				
			||||||
 | 
					      refreshGallery(); // Refresh the gallery list from the server
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      alert('Error: ' + data.error); // Show server error if upload failed
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					document.getElementById('upload-hero').addEventListener('change', async (e) => {
 | 
				
			||||||
 | 
					  const file = e.target.files[0];
 | 
				
			||||||
 | 
					  if (!file) return; // Exit if no file is selected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Create a FormData object to send the file
 | 
				
			||||||
 | 
					  const formData = new FormData();
 | 
				
			||||||
 | 
					  formData.append('file', file); // Key must match what upload.py expects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    // Send POST request to the hero upload endpoint
 | 
				
			||||||
 | 
					    const res = await fetch('/api/hero/upload', {
 | 
				
			||||||
 | 
					      method: 'POST',
 | 
				
			||||||
 | 
					      body: formData
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const data = await res.json();
 | 
				
			||||||
 | 
					    if (res.ok) {
 | 
				
			||||||
 | 
					      alert('✅ Hero image uploaded!');
 | 
				
			||||||
 | 
					      refreshHero(); // Refresh the hero list from the server
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      alert('Error: ' + data.error); // Show server error if upload failed
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  } catch (err) {
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										98
									
								
								src/webui/style/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/webui/style/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					body {
 | 
				
			||||||
 | 
					  font-family: Arial, sans-serif;
 | 
				
			||||||
 | 
					  margin: 20px;
 | 
				
			||||||
 | 
					  background-color: #f9f9f9;
 | 
				
			||||||
 | 
					  color: #333;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					h1, h2 {
 | 
				
			||||||
 | 
					  color: #222;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toolbar {
 | 
				
			||||||
 | 
					  margin-bottom: 20px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toolbar button {
 | 
				
			||||||
 | 
					  margin-right: 10px;
 | 
				
			||||||
 | 
					  padding: 8px 12px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  background-color: #4CAF50;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toolbar button:hover {
 | 
				
			||||||
 | 
					  background-color: #45a049;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-section {
 | 
				
			||||||
 | 
					  margin-bottom: 30px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.upload-section label {
 | 
				
			||||||
 | 
					  margin-right: 20px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#gallery, #hero {
 | 
				
			||||||
 | 
					  display: grid;
 | 
				
			||||||
 | 
					  grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
 | 
				
			||||||
 | 
					  gap: 15px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo {
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  border: 1px solid #ddd;
 | 
				
			||||||
 | 
					  border-radius: 6px;
 | 
				
			||||||
 | 
					  padding: 10px;
 | 
				
			||||||
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo img {
 | 
				
			||||||
 | 
					  max-width: 100%;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  margin-bottom: 8px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo input[type="text"] {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					  padding: 4px 6px;
 | 
				
			||||||
 | 
					  margin-bottom: 6px;
 | 
				
			||||||
 | 
					  border: 1px solid #ccc;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo button {
 | 
				
			||||||
 | 
					  padding: 4px 8px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  background-color: #f44336;
 | 
				
			||||||
 | 
					  color: white;
 | 
				
			||||||
 | 
					  border-radius: 4px;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  transition: background-color 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photo button:hover {
 | 
				
			||||||
 | 
					  background-color: #d32f2f;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Responsive adjustments */
 | 
				
			||||||
 | 
					@media (max-width: 500px) {
 | 
				
			||||||
 | 
					  body {
 | 
				
			||||||
 | 
					    margin: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .toolbar button {
 | 
				
			||||||
 | 
					    margin-bottom: 8px;
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  .upload-section label {
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin-bottom: 10px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user