Site-info front
This commit is contained in:
@ -24,6 +24,8 @@ footer:
|
|||||||
# Build parameters
|
# Build parameters
|
||||||
build:
|
build:
|
||||||
theme: modern # choose a theme in config/theme folder
|
theme: modern # choose a theme in config/theme folder
|
||||||
|
convert_images: true # true to enable image conversion
|
||||||
|
resize_images: true # true to enable image resizing
|
||||||
|
|
||||||
# Change this by your legals
|
# Change this by your legals
|
||||||
legals:
|
legals:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import yaml
|
||||||
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 (
|
||||||
@ -18,6 +19,8 @@ app = Flask(
|
|||||||
static_url_path=""
|
static_url_path=""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SITE_YAML = Path(__file__).resolve().parents[3] / "config" / "site.yaml"
|
||||||
|
|
||||||
# --- Photos directory (configurable) ---
|
# --- Photos directory (configurable) ---
|
||||||
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
|
||||||
@ -133,6 +136,25 @@ def photos(section, filename):
|
|||||||
"""Serve uploaded photos from disk."""
|
"""Serve uploaded photos from disk."""
|
||||||
return send_from_directory(PHOTOS_DIR / section, filename)
|
return send_from_directory(PHOTOS_DIR / section, filename)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/site-info")
|
||||||
|
def site_info():
|
||||||
|
return render_template("site-info/index.html")
|
||||||
|
|
||||||
|
@app.route("/api/site-info", methods=["GET"])
|
||||||
|
def get_site_info():
|
||||||
|
with open(SITE_YAML, "r") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
@app.route("/api/site-info", methods=["POST"])
|
||||||
|
def update_site_info():
|
||||||
|
data = request.json
|
||||||
|
with open(SITE_YAML, "w") as f:
|
||||||
|
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
|
||||||
|
|
||||||
# --- Run server ---
|
# --- 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")
|
||||||
|
197
src/webui/js/site-info.js
Normal file
197
src/webui/js/site-info.js
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const form = document.getElementById("site-info-form");
|
||||||
|
const status = document.getElementById("site-info-status");
|
||||||
|
const menuList = document.getElementById("menu-items-list");
|
||||||
|
const addMenuBtn = document.getElementById("add-menu-item");
|
||||||
|
|
||||||
|
let menuItems = [];
|
||||||
|
|
||||||
|
function renderMenuItems() {
|
||||||
|
menuList.innerHTML = "";
|
||||||
|
menuItems.forEach((item, idx) => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.style.display = "flex";
|
||||||
|
div.style.gap = "8px";
|
||||||
|
div.style.marginBottom = "6px";
|
||||||
|
div.innerHTML = `
|
||||||
|
<input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label">
|
||||||
|
<input type="text" placeholder="URL" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href">
|
||||||
|
<button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
|
||||||
|
`;
|
||||||
|
menuList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMenuItemsFromInputs() {
|
||||||
|
const inputs = menuList.querySelectorAll("input");
|
||||||
|
const items = [];
|
||||||
|
for (let i = 0; i < inputs.length; i += 2) {
|
||||||
|
const label = inputs[i].value.trim();
|
||||||
|
const href = inputs[i + 1].value.trim();
|
||||||
|
if (label || href) items.push({ label, href });
|
||||||
|
}
|
||||||
|
menuItems = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ipList = document.getElementById("ip-list");
|
||||||
|
const addIpBtn = document.getElementById("add-ip-paragraph");
|
||||||
|
let ipParagraphs = [];
|
||||||
|
|
||||||
|
function renderIpParagraphs() {
|
||||||
|
ipList.innerHTML = "";
|
||||||
|
ipParagraphs.forEach((item, idx) => {
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.style.display = "flex";
|
||||||
|
div.style.gap = "8px";
|
||||||
|
div.style.marginBottom = "6px";
|
||||||
|
div.innerHTML = `
|
||||||
|
<input type="text" placeholder="Paragraph" value="${item.paragraph || ""}" style="flex:1;" data-idx="${idx}">
|
||||||
|
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
|
||||||
|
`;
|
||||||
|
ipList.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIpParagraphsFromInputs() {
|
||||||
|
const inputs = ipList.querySelectorAll("input");
|
||||||
|
ipParagraphs = Array.from(inputs).map(input => ({
|
||||||
|
paragraph: input.value.trim()
|
||||||
|
})).filter(item => item.paragraph !== "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Build checkboxes ---
|
||||||
|
const convertImagesCheckbox = document.getElementById("convert-images-checkbox");
|
||||||
|
const resizeImagesCheckbox = document.getElementById("resize-images-checkbox");
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
if (form) {
|
||||||
|
fetch("/api/site-info")
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
ipParagraphs = Array.isArray(data.legals?.intellectual_property)
|
||||||
|
? data.legals.intellectual_property
|
||||||
|
: [];
|
||||||
|
renderIpParagraphs();
|
||||||
|
menuItems = Array.isArray(data.menu?.items) ? data.menu.items : [];
|
||||||
|
renderMenuItems();
|
||||||
|
form.elements["info.title"].value = data.info?.title || "";
|
||||||
|
form.elements["info.subtitle"].value = data.info?.subtitle || "";
|
||||||
|
form.elements["info.description"].value = data.info?.description || "";
|
||||||
|
form.elements["info.canonical"].value = data.info?.canonical || "";
|
||||||
|
form.elements["info.keywords"].value = Array.isArray(data.info?.keywords) ? data.info.keywords.join(", ") : (data.info?.keywords || "");
|
||||||
|
form.elements["info.author"].value = data.info?.author || "";
|
||||||
|
form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
|
||||||
|
form.elements["social.thumbnail"].value = data.social?.thumbnail || "";
|
||||||
|
form.elements["footer.copyright"].value = data.footer?.copyright || "";
|
||||||
|
form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
|
||||||
|
form.elements["build.theme"].value = data.build?.theme || "";
|
||||||
|
form.elements["legals.hoster_name"].value = data.legals?.hoster_name || "";
|
||||||
|
form.elements["legals.hoster_adress"].value = data.legals?.hoster_adress || "";
|
||||||
|
form.elements["legals.hoster_contact"].value = data.legals?.hoster_contact || "";
|
||||||
|
// --- Build checkboxes ---
|
||||||
|
if (convertImagesCheckbox) {
|
||||||
|
convertImagesCheckbox.checked = !!data.build?.convert_images;
|
||||||
|
}
|
||||||
|
if (resizeImagesCheckbox) {
|
||||||
|
resizeImagesCheckbox.checked = !!data.build?.resize_images;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add menu item
|
||||||
|
if (addMenuBtn) {
|
||||||
|
addMenuBtn.addEventListener("click", () => {
|
||||||
|
menuItems.push({ label: "", href: "" });
|
||||||
|
renderMenuItems();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove menu item
|
||||||
|
menuList.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("remove-menu-item")) {
|
||||||
|
const idx = parseInt(e.target.getAttribute("data-idx"));
|
||||||
|
menuItems.splice(idx, 1);
|
||||||
|
renderMenuItems();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update menuItems on input change
|
||||||
|
menuList.addEventListener("input", () => {
|
||||||
|
updateMenuItemsFromInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add paragraph
|
||||||
|
if (addIpBtn) {
|
||||||
|
addIpBtn.addEventListener("click", () => {
|
||||||
|
ipParagraphs.push({ paragraph: "" });
|
||||||
|
renderIpParagraphs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove paragraph
|
||||||
|
ipList.addEventListener("click", (e) => {
|
||||||
|
if (e.target.classList.contains("remove-ip-paragraph")) {
|
||||||
|
const idx = parseInt(e.target.getAttribute("data-idx"));
|
||||||
|
ipParagraphs.splice(idx, 1);
|
||||||
|
renderIpParagraphs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update ipParagraphs on input change
|
||||||
|
ipList.addEventListener("input", () => {
|
||||||
|
updateIpParagraphsFromInputs();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
if (form) {
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
updateMenuItemsFromInputs();
|
||||||
|
updateIpParagraphsFromInputs();
|
||||||
|
|
||||||
|
// --- Build object with checkboxes ---
|
||||||
|
const build = {
|
||||||
|
theme: form.elements["build.theme"].value,
|
||||||
|
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
|
||||||
|
resize_images: !!(resizeImagesCheckbox && resizeImagesCheckbox.checked)
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
info: {
|
||||||
|
title: form.elements["info.title"].value,
|
||||||
|
subtitle: form.elements["info.subtitle"].value,
|
||||||
|
description: form.elements["info.description"].value,
|
||||||
|
canonical: form.elements["info.canonical"].value,
|
||||||
|
keywords: form.elements["info.keywords"].value.split(",").map(i => i.trim()).filter(Boolean),
|
||||||
|
author: form.elements["info.author"].value
|
||||||
|
},
|
||||||
|
social: {
|
||||||
|
instagram_url: form.elements["social.instagram_url"].value,
|
||||||
|
thumbnail: form.elements["social.thumbnail"].value
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
items: menuItems
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
copyright: form.elements["footer.copyright"].value,
|
||||||
|
legal_label: form.elements["footer.legal_label"].value
|
||||||
|
},
|
||||||
|
build,
|
||||||
|
legals: {
|
||||||
|
hoster_name: form.elements["legals.hoster_name"].value,
|
||||||
|
hoster_adress: form.elements["legals.hoster_adress"].value,
|
||||||
|
hoster_contact: form.elements["legals.hoster_contact"].value,
|
||||||
|
intellectual_property: ipParagraphs
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const res = await fetch("/api/site-info", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const result = await res.json();
|
||||||
|
status.textContent = result.status === "ok" ? "✅ Saved!" : "❌ Error saving";
|
||||||
|
setTimeout(() => status.textContent = "", 2000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
109
src/webui/site-info/index.html
Normal file
109
src/webui/site-info/index.html
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Lumeex</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Top bar -->
|
||||||
|
<div class="nav-bar">
|
||||||
|
<div class="content-inner nav">
|
||||||
|
<div class="nav-cta">
|
||||||
|
<div class="arrow">→</div>
|
||||||
|
<a class="button" href="#" target="_blank">
|
||||||
|
<span id="step">🚀 Build !<i class="fa-solid fa-envelope"></i></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="nav-check">
|
||||||
|
<div class="nav-header">
|
||||||
|
<div class="nav-title">
|
||||||
|
<img src="{{ url_for('static', filename='img/logo.svg') }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-btn">
|
||||||
|
<label for="nav-check">
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
<span></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="nav-links">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item appear2"><a href="#">Site info</a>
|
||||||
|
<li class="nav-item appear2"><a href="#">Theme info</a>
|
||||||
|
<li class="nav-item appear2"><a href="#">Gallery</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Toast container for notifications -->
|
||||||
|
<div class="content-inner">
|
||||||
|
<div id="toast-container"></div>
|
||||||
|
<h1>Edit Site Info</h1>
|
||||||
|
<form id="site-info-form">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Info</legend>
|
||||||
|
<label>Title: <input type="text" name="info.title"></label><br>
|
||||||
|
<label>Subtitle: <input type="text" name="info.subtitle"></label><br>
|
||||||
|
<label>Description: <textarea name="info.description"></textarea></label><br>
|
||||||
|
<label>Canonical URL: <input type="text" name="info.canonical"></label><br>
|
||||||
|
<label>Keywords (comma separated): <input type="text" name="info.keywords"></label><br>
|
||||||
|
<label>Author: <input type="text" name="info.author"></label><br>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Social</legend>
|
||||||
|
<label>Instagram URL: <input type="text" name="social.instagram_url"></label><br>
|
||||||
|
<label>Thumbnail: <input type="text" name="social.thumbnail"></label><br>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Menu</legend>
|
||||||
|
<div id="menu-items-list"></div>
|
||||||
|
<button type="button" id="add-menu-item">+ Add menu item</button>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Footer</legend>
|
||||||
|
<label>Copyright: <input type="text" name="footer.copyright"></label><br>
|
||||||
|
<label>Legal Label: <input type="text" name="footer.legal_label"></label><br>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Build</legend>
|
||||||
|
<label>Theme: <input type="text" name="build.theme"></label><br>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
|
||||||
|
Convert images
|
||||||
|
</label><br>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
|
||||||
|
Resize images
|
||||||
|
</label><br>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Legals</legend>
|
||||||
|
<label>Hoster Name: <input type="text" name="legals.hoster_name"></label><br>
|
||||||
|
<label>Hoster Address: <input type="text" name="legals.hoster_adress"></label><br>
|
||||||
|
<label>Hoster Contact: <input type="text" name="legals.hoster_contact"></label><br>
|
||||||
|
<div>
|
||||||
|
<label>Intellectual Property:</label>
|
||||||
|
<div id="ip-list"></div>
|
||||||
|
<button type="button" id="add-ip-paragraph">+ Add paragraph</button>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit">Save</button>
|
||||||
|
</form>
|
||||||
|
<div id="site-info-status"></div>
|
||||||
|
</div>
|
||||||
|
<div id="delete-modal" class="modal" style="display:none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span id="delete-modal-close" class="modal-close">×</span>
|
||||||
|
<h3>Confirm Deletion</h3>
|
||||||
|
<p id="delete-modal-text">Are you sure you want to delete this image?</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="delete-modal-confirm" class="modal-btn danger">Delete</button>
|
||||||
|
<button id="delete-modal-cancel" class="modal-btn">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user