Better UI form

This commit is contained in:
Djeex
2025-08-17 23:49:36 +02:00
parent 5d863223e3
commit c193fd49aa
6 changed files with 346 additions and 151 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 KiB

View File

@ -1,36 +1,26 @@
# Please change this by your settings.
info: info:
title: title: ''
subtitle: subtitle: ''
description: description: ''
canonical: canonical: ''
keywords: keywords: []
author: author: ''
social: social:
instagram_url: instagram_url: ''
thumbnail: thumbnail: ''
menu: menu:
items: items:
- label: Home - label: Home
href: / href: /
footer: footer:
copyright: Copyright © 2025 copyright: Copyright © 2025
legal_link: '/legals/'
legal_label: Legal notice legal_label: Legal notice
# Build parameters
build: build:
theme: modern # choose a theme in config/theme folder theme: modern
convert_images: true # true to enable image conversion convert_images: true
resize_images: true # true to enable image resizing resize_images: true
# Change this by your legals
legals: legals:
hoster_name: hoster_name: ''
hoster_adress: hoster_adress: ''
hoster_contact: hoster_contact: ''
intellectual_property: intellectual_property: []
- paragraph: ""

View File

@ -181,6 +181,22 @@ def upload_thumbnail():
yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True) yaml.safe_dump(data, f, sort_keys=False, allow_unicode=True)
return jsonify({"status": "ok", "filename": filename}) return jsonify({"status": "ok", "filename": filename})
@app.route("/api/thumbnail/remove", methods=["POST"])
def remove_thumbnail():
PHOTOS_DIR = app.config["PHOTOS_DIR"]
thumbnail_path = PHOTOS_DIR / "thumbnail.png"
# Remove thumbnail file if exists
if thumbnail_path.exists():
thumbnail_path.unlink()
# Update site.yaml to remove thumbnail key
with open(SITE_YAML, "r") as f:
data = yaml.safe_load(f)
if "social" in data and "thumbnail" in data["social"]:
data["social"]["thumbnail"] = ""
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")

View File

@ -1,6 +1,19 @@
function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container");
if (!container) return;
const toast = document.createElement("div");
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add("show"));
setTimeout(() => {
toast.classList.remove("show");
toast.addEventListener("transitionend", () => toast.remove());
}, duration);
}
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
const form = document.getElementById("site-info-form"); const form = document.getElementById("site-info-form");
const status = document.getElementById("site-info-status");
const menuList = document.getElementById("menu-items-list"); const menuList = document.getElementById("menu-items-list");
const addMenuBtn = document.getElementById("add-menu-item"); const addMenuBtn = document.getElementById("add-menu-item");
@ -45,7 +58,7 @@ document.addEventListener("DOMContentLoaded", () => {
div.style.gap = "8px"; div.style.gap = "8px";
div.style.marginBottom = "6px"; div.style.marginBottom = "6px";
div.innerHTML = ` div.innerHTML = `
<input type="text" placeholder="Paragraph" value="${item.paragraph || ""}" style="flex:1;" data-idx="${idx}"> <textarea placeholder="Paragraph" style="flex:1;" data-idx="${idx}">${item.paragraph || ""}</textarea>
<button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button> <button type="button" class="remove-ip-paragraph" data-idx="${idx}">🗑</button>
`; `;
ipList.appendChild(div); ipList.appendChild(div);
@ -53,9 +66,9 @@ document.addEventListener("DOMContentLoaded", () => {
} }
function updateIpParagraphsFromInputs() { function updateIpParagraphsFromInputs() {
const inputs = ipList.querySelectorAll("input"); const textareas = ipList.querySelectorAll("textarea");
ipParagraphs = Array.from(inputs).map(input => ({ ipParagraphs = Array.from(textareas).map(textarea => ({
paragraph: input.value.trim() paragraph: textarea.value.trim()
})).filter(item => item.paragraph !== ""); })).filter(item => item.paragraph !== "");
} }
@ -66,10 +79,36 @@ document.addEventListener("DOMContentLoaded", () => {
// --- Theme select --- // --- Theme select ---
const themeSelect = document.getElementById("theme-select"); const themeSelect = document.getElementById("theme-select");
// --- Thumbnail upload --- // --- Thumbnail upload & modal logic ---
const thumbnailInput = form?.elements["social.thumbnail"]; const thumbnailInput = form?.elements["social.thumbnail"];
const thumbnailUpload = document.getElementById("thumbnail-upload"); const thumbnailUpload = document.getElementById("thumbnail-upload");
const chooseThumbnailBtn = document.getElementById("choose-thumbnail-btn");
const thumbnailPreview = document.getElementById("thumbnail-preview"); const thumbnailPreview = document.getElementById("thumbnail-preview");
const removeThumbnailBtn = document.getElementById("remove-thumbnail-btn");
// Modal elements
const deleteModal = document.getElementById("delete-modal");
const deleteModalClose = document.getElementById("delete-modal-close");
const deleteModalConfirm = document.getElementById("delete-modal-confirm");
const deleteModalCancel = document.getElementById("delete-modal-cancel");
// Show/hide remove button, preview, and choose button
function updateThumbnailPreview(src) {
if (thumbnailPreview) {
thumbnailPreview.src = src || "";
thumbnailPreview.style.display = src ? "block" : "none";
}
if (removeThumbnailBtn) {
removeThumbnailBtn.style.display = src ? "inline-block" : "none";
}
if (chooseThumbnailBtn) {
chooseThumbnailBtn.style.display = src ? "none" : "inline-block";
}
}
if (chooseThumbnailBtn && thumbnailUpload) {
chooseThumbnailBtn.addEventListener("click", () => thumbnailUpload.click());
}
if (thumbnailUpload) { if (thumbnailUpload) {
thumbnailUpload.addEventListener("change", async (e) => { thumbnailUpload.addEventListener("change", async (e) => {
@ -81,19 +120,45 @@ document.addEventListener("DOMContentLoaded", () => {
const result = await res.json(); const result = await res.json();
if (result.status === "ok") { if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = result.filename; if (thumbnailInput) thumbnailInput.value = result.filename;
if (thumbnailPreview) { updateThumbnailPreview(`/photos/${result.filename}`);
thumbnailPreview.src = `/photos/${result.filename}`; showToast("Thumbnail uploaded!", "success");
thumbnailPreview.style.display = "block";
}
status.textContent = "✅ Thumbnail uploaded!";
setTimeout(() => status.textContent = "", 2000);
} else { } else {
status.textContent = "❌ Error uploading thumbnail"; showToast("Error uploading thumbnail", "error");
setTimeout(() => status.textContent = "", 2000);
} }
}); });
} }
// Attach modal logic to remove button
if (removeThumbnailBtn) {
removeThumbnailBtn.addEventListener("click", () => {
deleteModal.style.display = "flex";
});
}
// Modal logic
if (deleteModal && deleteModalClose && deleteModalConfirm && deleteModalCancel) {
deleteModalClose.onclick = deleteModalCancel.onclick = () => {
deleteModal.style.display = "none";
};
window.onclick = function(event) {
if (event.target === deleteModal) {
deleteModal.style.display = "none";
}
};
deleteModalConfirm.onclick = async () => {
const res = await fetch("/api/thumbnail/remove", { method: "POST" });
const result = await res.json();
if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = "";
updateThumbnailPreview("");
showToast("Thumbnail removed!", "success");
} else {
showToast("Error removing thumbnail", "error");
}
deleteModal.style.display = "none";
};
}
// Fetch theme list and populate select // Fetch theme list and populate select
if (themeSelect) { if (themeSelect) {
fetch("/api/themes") fetch("/api/themes")
@ -134,10 +199,7 @@ document.addEventListener("DOMContentLoaded", () => {
form.elements["info.author"].value = data.info?.author || ""; form.elements["info.author"].value = data.info?.author || "";
form.elements["social.instagram_url"].value = data.social?.instagram_url || ""; form.elements["social.instagram_url"].value = data.social?.instagram_url || "";
if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || ""; if (thumbnailInput) thumbnailInput.value = data.social?.thumbnail || "";
if (thumbnailPreview && data.social?.thumbnail) { updateThumbnailPreview(data.social?.thumbnail ? `/photos/${data.social.thumbnail}` : "");
thumbnailPreview.src = `/photos/${data.social.thumbnail}`;
thumbnailPreview.style.display = "block";
}
form.elements["footer.copyright"].value = data.footer?.copyright || ""; form.elements["footer.copyright"].value = data.footer?.copyright || "";
form.elements["footer.legal_label"].value = data.footer?.legal_label || ""; form.elements["footer.legal_label"].value = data.footer?.legal_label || "";
if (themeSelect) { if (themeSelect) {
@ -207,7 +269,6 @@ document.addEventListener("DOMContentLoaded", () => {
updateMenuItemsFromInputs(); updateMenuItemsFromInputs();
updateIpParagraphsFromInputs(); updateIpParagraphsFromInputs();
// --- Build object with checkboxes and theme select ---
const build = { const build = {
theme: themeSelect ? themeSelect.value : "", theme: themeSelect ? themeSelect.value : "",
convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked), convert_images: !!(convertImagesCheckbox && convertImagesCheckbox.checked),
@ -248,8 +309,11 @@ document.addEventListener("DOMContentLoaded", () => {
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
const result = await res.json(); const result = await res.json();
status.textContent = result.status === "ok" ? "✅ Saved!" : "❌ Error saving"; if (result.status === "ok") {
setTimeout(() => status.textContent = "", 2000); showToast("Site info saved!", "success");
} else {
showToast("Error saving site info", "error");
}
}); });
} }
}); });

View File

@ -31,9 +31,9 @@
</div> </div>
<div class="nav-links"> <div class="nav-links">
<ul class="nav-list"> <ul class="nav-list">
<li class="nav-item appear2"><a href="#">Site info</a> <li class="nav-item appear2"><a href="#">Site info</a></li>
<li class="nav-item appear2"><a href="#">Theme info</a> <li class="nav-item appear2"><a href="#">Theme info</a></li>
<li class="nav-item appear2"><a href="#">Gallery</a> <li class="nav-item appear2"><a href="#">Gallery</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -43,8 +43,10 @@
<div id="toast-container"></div> <div id="toast-container"></div>
<h1>Edit Site Info</h1> <h1>Edit Site Info</h1>
<form id="site-info-form"> <form id="site-info-form">
<!-- Info Section -->
<fieldset> <fieldset>
<legend>Info</legend> <legend>Info</legend>
<div class="fields">
<div class="input-field"> <div class="input-field">
<label>Title:</label> <label>Title:</label>
<input type="text" name="info.title"> <input type="text" name="info.title">
@ -55,7 +57,7 @@
</div> </div>
<div class="input-field"> <div class="input-field">
<label>Description:</label> <label>Description:</label>
<textarea name="info.description"></textarea> <input type="text" name="info.description"></input>
</div> </div>
<div class="input-field"> <div class="input-field">
<label>Canonical URL:</label> <label>Canonical URL:</label>
@ -69,57 +71,51 @@
<label>Author:</label> <label>Author:</label>
<input type="text" name="info.author"> <input type="text" name="info.author">
</div> </div>
</div>
</fieldset> </fieldset>
<!-- Social Section -->
<fieldset> <fieldset>
<legend>Social</legend> <legend>Social</legend>
<div class="fields">
<div class="input-field"> <div class="input-field">
<label>Instagram URL: </label> <label>Instagram URL:</label>
<input type="text" name="social.instagram_url"> <input type="text" name="social.instagram_url">
</div> <label>Upload Thumbnail:</label>
<div class="input-field"> <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
<label>Thumbnail: </label> <button type="button" id="choose-thumbnail-btn" class="up-btn">Choose a photo</button>
<input type="text" name="social.thumbnail" readonly>
</div>
<div class="input-field">
<label>Upload Thumbnail: </label>
<input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp">
<img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;"> <img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
<button type="button" id="remove-thumbnail-btn" class="up-btn danger" style="display:none;">Remove</button>
</div>
</div> </div>
</fieldset> </fieldset>
<!-- Menu Section -->
<fieldset> <fieldset>
<legend>Menu</legend> <legend>Menu</legend>
<div id="menu-items-list"> <div class="fields">
</div> <div class="input-field" style="flex: 1 1 100%;">
<div id="menu-items-list"></div>
<button type="button" id="add-menu-item">+ Add menu item</button> <button type="button" id="add-menu-item">+ Add menu item</button>
</div>
</div>
</fieldset> </fieldset>
<!-- Footer Section -->
<fieldset> <fieldset>
<legend>Footer</legend> <legend>Footer</legend>
<div class="fields">
<div class="input-field"> <div class="input-field">
<label>Copyright: </label> <label>Copyright:</label>
<input type="text" name="footer.copyright"></label> <input type="text" name="footer.copyright">
</div> </div>
<div class="input-field"> <div class="input-field">
<label>Legal Label: </label> <label>Legal Label:</label>
<input type="text" name="footer.legal_label"></label> <input type="text" name="footer.legal_label">
</div> </div>
</fieldset>
<fieldset>
<legend>Build</legend>
<div class="input-field">
<label>Theme:</label>
<select name="build.theme" id="theme-select"></select>
</div>
<div class="input-field">
<label>Convert images</label>
<input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
</div>
<div class="input-field">
<label>Resize images</label>
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
</div> </div>
</fieldset> </fieldset>
<!-- Legals Section -->
<fieldset> <fieldset>
<legend>Legals</legend> <legend>Legals</legend>
<div class="fields">
<div class="input-field"> <div class="input-field">
<label>Hoster Name:</label> <label>Hoster Name:</label>
<input type="text" name="legals.hoster_name"> <input type="text" name="legals.hoster_name">
@ -132,18 +128,46 @@
<label>Hoster Contact:</label> <label>Hoster Contact:</label>
<input type="text" name="legals.hoster_contact"> <input type="text" name="legals.hoster_contact">
</div> </div>
<div class="input-field"> <div class="input-field" style="flex: 1 1 100%;">
<label>Intellectual Property:</label> <label>Intellectual Property:</label>
<div id="ip-list"></div> <div id="ip-list"></div>
<button type="button" id="add-ip-paragraph">+ Add paragraph</button> <button type="button" id="add-ip-paragraph">+ Add paragraph</button>
</div> </div>
</div> </div>
</fieldset> </fieldset>
<!-- Build Section -->
<fieldset>
<legend>Build</legend>
<div class="fields">
<div class="input-field">
<label>Theme:</label>
<select name="build.theme" id="theme-select"></select>
<label>
<input type="checkbox" name="build.convert_images" id="convert-images-checkbox">
Convert images
</label>
<label>
<input type="checkbox" name="build.resize_images" id="resize-images-checkbox">
Resize images
</label>
</div>
</div>
</fieldset>
<button type="submit">Save</button> <button type="submit">Save</button>
</form>
</div>
<!-- Delete confirmation modal (now outside .content-inner) -->
<div id="delete-modal" class="modal" style="display:none;">
<div class="modal-content">
<span id="delete-modal-close" class="modal-close">&times;</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>
</div> </div>
</div>
<script src="{{ url_for('static', filename='js/site-info.js') }}"></script> <script src="{{ url_for('static', filename='js/site-info.js')}}"></script>
</body> </body>
</html> </html>

View File

@ -468,44 +468,145 @@ h1, h2 {
/* --- Site Info --- */ /* --- Site Info --- */
#site-info-form input, #site-info-form textarea { /* --- Site Info Form --- */
display: block; #site-info-form {
margin-top: 5px; max-width: 950px;
border: none;
backdrop-filter: blur(20px);
background: #00293054;
border-radius: 5px;
outline: none;
font-size: 14px;
font-weight: 400;
color: #fff;
border-radius: 5px;
border: 1px solid #aaa;
padding: 0 15px;
height: 42px;
margin: 8px 0;
} }
#site-info-form label { #site-info-form fieldset {
font-size: 12px; border: none;
font-weight: 500; margin-bottom: 28px;
padding: 0;
background: rgba(0, 41, 48, 0.45);
border-radius: 18px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.25);
padding: 32px 28px;
margin: 32px auto;
backdrop-filter: blur(12px);
}
#site-info-form legend {
font-size: 1.2em;
font-weight: 700;
color: #26c4ff;
margin-bottom: 12px;
letter-spacing: 1px;
}
#site-info-form .fields {
display: flex;
flex-wrap: wrap;
gap: 18px;
} }
#site-info-form .input-field { #site-info-form .input-field {
flex: 1 1 calc(33.333% - 18px);
min-width: 220px;
max-width: 100%;
box-sizing: border-box;
display: flex; display: flex;
width: calc(100% / 3 - 15px);
flex-direction: column; flex-direction: column;
margin: 4px 0; margin-bottom: 10px;
} }
#site-info-form fieldset{ #site-info-form label {
display: flex; font-size: 13px;
align-items: center; font-weight: 600;
justify-content: space-between; color: #b8eaff;
flex-wrap: wrap; margin-bottom: 6px;
border:none; letter-spacing: 0.5px;
} }
h3 { #site-info-form input,
display: block; #site-info-form textarea,
#site-info-form select {
background: rgba(4, 44, 60, 0.55);
color: #fff;
border: 1px solid #26c4ff33;
border-radius: 8px;
font-size: 15px;
font-weight: 400;
padding: 10px 14px;
margin-bottom: 4px;
outline: none;
transition: border-color 0.2s, background 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
}
#site-info-form input:focus,
#site-info-form textarea:focus,
#site-info-form select:focus {
border-color: #26c4ff;
background: rgba(38, 196, 255, 0.09);
}
#site-info-form textarea {
min-height: 60px;
resize: vertical;
}
#site-info-form input[type="file"] {
background: none;
color: #fff;
border: none;
padding: 0;
margin-top: 2px;
}
#site-info-form img#thumbnail-preview {
margin-top: 8px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(38,196,255,0.12);
border: 1px solid #26c4ff33;
}
#site-info-form button[type="submit"] {
background: linear-gradient(135deg, #26c4ff, #016074);
color: #fff;
font-weight: 700;
border: none;
border-radius: 30px;
padding: 12px 32px;
font-size: 1.1em;
margin-top: 18px;
cursor: pointer;
box-shadow: 0 4px 16px rgba(38,196,255,0.15);
transition: background 0.2s;
}
#site-info-form button[type="submit"]:hover {
background: linear-gradient(135deg, #72d9ff, #26657e);
}
#site-info-form button[type="button"] {
background: #074053;
color: #fff;
border: none;
border-radius: 18px;
padding: 7px 18px;
font-size: 0.98em;
margin-top: 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(38,196,255,0.09);
transition: background 0.2s;
}
#site-info-form button[type="button"]:hover {
background: #26c4ff;
color: #002a30;
}
@media (max-width: 900px) {
#site-info-form {
padding: 18px 8px;
}
#site-info-form .fields,
#site-info-form fieldset {
flex-direction: column;
gap: 0;
}
#site-info-form .input-field {
min-width: 100%;
margin-bottom: 12px;
}
} }