Gallery front
This commit is contained in:
2
build.py
2
build.py
@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from src.py.site_builder import build
|
||||
from src.py.builder.site_builder import build
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
@ -1,5 +1,5 @@
|
||||
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__":
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
|
@ -1,2 +1,3 @@
|
||||
pyyaml
|
||||
pillow
|
||||
pillow
|
||||
flask
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||
from shutil import copyfile
|
||||
|
||||
def generate_css_variables(colors_dict, output_path):
|
||||
"""Generate css variables for theme colors"""
|
||||
css_lines = [":root {"]
|
||||
for key, value in colors_dict.items():
|
||||
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}")
|
||||
|
||||
def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
|
||||
"""Generate css variables fonts"""
|
||||
font_files = list(fonts_dir.glob("*"))
|
||||
font_faces = {}
|
||||
preload_links = []
|
||||
@ -57,6 +59,7 @@ def generate_fonts_css(fonts_dir, output_path, fonts_cfg=None):
|
||||
return preload_links
|
||||
|
||||
def generate_google_fonts_link(fonts):
|
||||
"""Generate src link for Google fonts"""
|
||||
if not fonts:
|
||||
return ""
|
||||
families = []
|
@ -10,6 +10,7 @@ GALLERY_DIR = Path("config/photos/gallery")
|
||||
HERO_DIR = Path("config/photos/hero")
|
||||
|
||||
def load_yaml(path):
|
||||
"""Load gallery config .yaml file"""
|
||||
print(f"[→] Loading {path}...")
|
||||
if not os.path.exists(path):
|
||||
print(f"[✗] File not found: {path}")
|
||||
@ -21,11 +22,13 @@ def load_yaml(path):
|
||||
return data
|
||||
|
||||
def save_yaml(data, path):
|
||||
"""Save modified gallery config .yaml file"""
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(data, f, sort_keys=False, allow_unicode=True)
|
||||
print(f"[✓] Saved updated YAML to {path}")
|
||||
|
||||
def get_all_image_paths(directory):
|
||||
"""Get the path to record for builded site"""
|
||||
return sorted([
|
||||
str(p.relative_to(directory.parent)).replace("\\", "/")
|
||||
for p in directory.rglob("*")
|
||||
@ -33,6 +36,7 @@ def get_all_image_paths(directory):
|
||||
])
|
||||
|
||||
def update_gallery():
|
||||
"""Update the gallery photo list"""
|
||||
print("\n=== Updating gallery.yaml (gallery section) ===")
|
||||
gallery = load_yaml(GALLERY_YAML)
|
||||
|
||||
@ -71,6 +75,7 @@ def update_gallery():
|
||||
print("[✓] No changes to gallery.yaml (gallery)")
|
||||
|
||||
def update_hero():
|
||||
"""Update the hero photo list"""
|
||||
print("\n=== Updating gallery.yaml (hero section) ===")
|
||||
gallery = load_yaml(GALLERY_YAML)
|
||||
|
@ -3,6 +3,7 @@ import logging
|
||||
from pathlib import Path
|
||||
|
||||
def render_template(template_path, context):
|
||||
"""Render html templates"""
|
||||
with open(template_path, encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
for key, value in context.items():
|
||||
@ -11,6 +12,7 @@ def render_template(template_path, context):
|
||||
return content
|
||||
|
||||
def render_gallery_images(images):
|
||||
"""Render the photo gallery"""
|
||||
html = ""
|
||||
for img in images:
|
||||
tags = " ".join(img.get("tags", []))
|
||||
@ -24,6 +26,7 @@ def render_gallery_images(images):
|
||||
return html
|
||||
|
||||
def generate_gallery_json_from_images(images, output_dir):
|
||||
"""Generte the hero carrousel photo list"""
|
||||
try:
|
||||
img_list = [img["src"] for img in images]
|
||||
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}")
|
||||
|
||||
def generate_robots_txt(canonical_url, allowed_paths, output_dir):
|
||||
"""Generate the robot.txt"""
|
||||
robots_lines = ["User-agent: *"]
|
||||
|
||||
# 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}")
|
||||
|
||||
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_end = '</urlset>\n'
|
||||
urls = ""
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||
from shutil import copytree, rmtree, copyfile
|
||||
|
||||
def load_yaml(path):
|
||||
"""Load gallery and site .yaml conf"""
|
||||
if not path.exists():
|
||||
logging.warning(f"[!] YAML file not found: {path}")
|
||||
return {}
|
||||
@ -11,6 +12,7 @@ def load_yaml(path):
|
||||
return yaml.safe_load(f)
|
||||
|
||||
def load_theme_config(theme_name, themes_dir):
|
||||
"""Load theme.yaml"""
|
||||
theme_dir = themes_dir / theme_name
|
||||
theme_config_path = theme_dir / "theme.yaml"
|
||||
if not theme_config_path.exists():
|
||||
@ -20,26 +22,25 @@ def load_theme_config(theme_name, themes_dir):
|
||||
return theme_vars, theme_dir
|
||||
|
||||
def clear_dir(path: Path):
|
||||
"""Clear the output dir"""
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
return
|
||||
|
||||
# Remove all files and subdirectories inside path, but not path itself
|
||||
for child in path.iterdir():
|
||||
if child.is_file() or child.is_symlink():
|
||||
child.unlink() # delete file or symlink
|
||||
child.unlink()
|
||||
elif child.is_dir():
|
||||
rmtree(child) # delete directory and contents
|
||||
|
||||
# Then replace your ensure_dir with this:
|
||||
rmtree(child)
|
||||
|
||||
def ensure_dir(path: Path):
|
||||
"""Create the output dir if it does not exist"""
|
||||
if not path.exists():
|
||||
path.mkdir(parents=True)
|
||||
else:
|
||||
clear_dir(path)
|
||||
|
||||
def copy_assets(js_dir, style_dir, build_dir):
|
||||
"""Copy public assets to output dir"""
|
||||
for folder in [js_dir, style_dir]:
|
||||
if folder.exists():
|
||||
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