Build and upload loader

This commit is contained in:
Djeex
2025-08-22 12:30:10 +02:00
parent a6b63c2d2b
commit 1591886505
5 changed files with 132 additions and 49 deletions

View File

@ -4,6 +4,19 @@
* @param {string} type - "success" or "error". * @param {string} type - "success" or "error".
* @param {number} duration - Duration in ms. * @param {number} duration - Duration in ms.
*/ */
function showLoader(text = "Building...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.style.display = "flex";
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.style.display = "none";
}
function showToast(message, type = "success", duration = 3000) { function showToast(message, type = "success", duration = 3000) {
const container = document.getElementById("toast-container"); const container = document.getElementById("toast-container");
if (!container) return; if (!container) return;
@ -29,9 +42,11 @@ document.addEventListener("DOMContentLoaded", () => {
// Build action handler // Build action handler
async function handleBuildClick() { async function handleBuildClick() {
showLoader("Building static site...");
// Trigger build on backend // Trigger build on backend
const res = await fetch("/api/build", { method: "POST" }); const res = await fetch("/api/build", { method: "POST" });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
// Show build success modal // Show build success modal
if (buildModal) buildModal.style.display = "flex"; if (buildModal) buildModal.style.display = "flex";

View File

@ -12,6 +12,19 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, duration);
} }
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.style.display = "flex";
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.style.display = "none";
}
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
// Form and menu logic // Form and menu logic
const form = document.getElementById("site-info-form"); const form = document.getElementById("site-info-form");
@ -130,10 +143,12 @@ document.addEventListener("DOMContentLoaded", () => {
thumbnailUpload.addEventListener("change", async (e) => { thumbnailUpload.addEventListener("change", async (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (!file) return; if (!file) return;
showLoader("Uploading thumbnail...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData }); const res = await fetch("/api/thumbnail/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
if (thumbnailInput) thumbnailInput.value = result.filename; if (thumbnailInput) thumbnailInput.value = result.filename;
updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`); updateThumbnailPreview(`/photos/${result.filename}?t=${Date.now()}`);
@ -183,12 +198,14 @@ document.addEventListener("DOMContentLoaded", () => {
themeUpload.addEventListener("change", async (e) => { themeUpload.addEventListener("change", async (e) => {
const files = Array.from(e.target.files); const files = Array.from(e.target.files);
if (files.length === 0) return; if (files.length === 0) return;
showLoader("Uploading theme...");
const formData = new FormData(); const formData = new FormData();
files.forEach(file => { files.forEach(file => {
formData.append("files", file, file.webkitRelativePath || file.name); formData.append("files", file, file.webkitRelativePath || file.name);
}); });
const res = await fetch("/api/theme/upload", { method: "POST", body: formData }); const res = await fetch("/api/theme/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Theme uploaded!", "success"); showToast("✅ Theme uploaded!", "success");
// Refresh theme select after upload // Refresh theme select after upload
@ -239,12 +256,14 @@ document.addEventListener("DOMContentLoaded", () => {
}; };
deleteThemeModalConfirm.onclick = async () => { deleteThemeModalConfirm.onclick = async () => {
if (!themeToDelete) return; if (!themeToDelete) return;
showLoader("Removing theme...");
const res = await fetch("/api/theme/remove", { const res = await fetch("/api/theme/remove", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeToDelete }) body: JSON.stringify({ theme: themeToDelete })
}); });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Theme removed!", "success"); showToast("✅ Theme removed!", "success");
// Refresh theme select // Refresh theme select
@ -379,7 +398,9 @@ document.addEventListener("DOMContentLoaded", () => {
// Check if thumbnail is set before saving (uploaded or present in input) // Check if thumbnail is set before saving (uploaded or present in input)
if (!thumbnailInput || !thumbnailInput.value) { if (!thumbnailInput || !thumbnailInput.value) {
showLoader("Saving...");
showToast("❌ Thumbnail is required.", "error"); showToast("❌ Thumbnail is required.", "error");
hideLoader();
return; return;
} }
@ -417,6 +438,8 @@ document.addEventListener("DOMContentLoaded", () => {
intellectual_property: ipParagraphs intellectual_property: ipParagraphs
} }
}; };
// --- REMOVE loader for save ---
// showLoader("Saving...");
const res = await fetch("/api/site-info", { const res = await fetch("/api/site-info", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },

View File

@ -31,6 +31,19 @@ function showToast(message, type = "success", duration = 3000) {
}, duration); }, duration);
} }
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.style.display = "flex";
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.style.display = "none";
}
function setupColorPicker(colorId, btnId, textId, initial) { function setupColorPicker(colorId, btnId, textId, initial) {
const colorInput = document.getElementById(colorId); const colorInput = document.getElementById(colorId);
const colorBtn = document.getElementById(btnId); const colorBtn = document.getElementById(btnId);
@ -40,7 +53,6 @@ function setupColorPicker(colorId, btnId, textId, initial) {
colorBtn.style.background = initial; colorBtn.style.background = initial;
textInput.value = initial.toUpperCase(); textInput.value = initial.toUpperCase();
// Color input is positioned over the button and is clickable
colorInput.addEventListener("input", () => { colorInput.addEventListener("input", () => {
colorBtn.style.background = colorInput.value; colorBtn.style.background = colorInput.value;
textInput.value = colorInput.value.toUpperCase(); textInput.value = colorInput.value.toUpperCase();
@ -178,11 +190,13 @@ document.addEventListener("DOMContentLoaded", async () => {
showToast("Only .woff and .woff2 fonts are allowed.", "error"); showToast("Only .woff and .woff2 fonts are allowed.", "error");
return; return;
} }
showLoader("Uploading font...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("theme", themeInfo.theme_name); formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/font/upload", { method: "POST", body: formData }); const res = await fetch("/api/font/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("✅ Font uploaded!", "success"); showToast("✅ Font uploaded!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name); localFonts = await fetchLocalFonts(themeInfo.theme_name);
@ -219,9 +233,11 @@ document.addEventListener("DOMContentLoaded", async () => {
}; };
deleteFontModalConfirm.onclick = async () => { deleteFontModalConfirm.onclick = async () => {
if (!fontToDelete) return; if (!fontToDelete) return;
showLoader("Removing font...");
const result = await removeFont(themeInfo.theme_name, fontToDelete); const result = await removeFont(themeInfo.theme_name, fontToDelete);
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
showToast("Font removed!", "success"); showToast("Font removed!", "success");
localFonts = await fetchLocalFonts(themeInfo.theme_name); localFonts = await fetchLocalFonts(themeInfo.theme_name);
refreshLocalFonts(); refreshLocalFonts();
} else { } else {
@ -272,11 +288,13 @@ document.addEventListener("DOMContentLoaded", async () => {
showToast("Invalid file type for favicon.", "error"); showToast("Invalid file type for favicon.", "error");
return; return;
} }
showLoader("Uploading favicon...");
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
formData.append("theme", themeInfo.theme_name); formData.append("theme", themeInfo.theme_name);
const res = await fetch("/api/favicon/upload", { method: "POST", body: formData }); const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
faviconInput.value = result.filename; faviconInput.value = result.filename;
updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`); updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
@ -303,12 +321,14 @@ document.addEventListener("DOMContentLoaded", async () => {
} }
}; };
deleteFaviconModalConfirm.onclick = async () => { deleteFaviconModalConfirm.onclick = async () => {
showLoader("Removing favicon...");
const res = await fetch("/api/favicon/remove", { const res = await fetch("/api/favicon/remove", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme: themeInfo.theme_name }) body: JSON.stringify({ theme: themeInfo.theme_name })
}); });
const result = await res.json(); const result = await res.json();
hideLoader();
if (result.status === "ok") { if (result.status === "ok") {
faviconInput.value = ""; faviconInput.value = "";
updateFaviconPreview(""); updateFaviconPreview("");
@ -335,13 +355,11 @@ document.addEventListener("DOMContentLoaded", async () => {
if (addGoogleFontBtn) { if (addGoogleFontBtn) {
addGoogleFontBtn.addEventListener("click", async () => { addGoogleFontBtn.addEventListener("click", async () => {
googleFonts.push({ family: "", weights: [] }); googleFonts.push({ family: "", weights: [] });
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -353,13 +371,11 @@ document.addEventListener("DOMContentLoaded", async () => {
const googleFontsFields = document.getElementById("google-fonts-fields"); const googleFontsFields = document.getElementById("google-fonts-fields");
if (googleFontsFields) { if (googleFontsFields) {
// Save on blur for family/weights fields
googleFontsFields.addEventListener("blur", async (e) => { googleFontsFields.addEventListener("blur", async (e) => {
if ( if (
e.target.name && e.target.name &&
(e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]")) (e.target.name.endsWith("[family]") || e.target.name.endsWith("[weights]"))
) { ) {
// Update googleFonts array from the form fields
const fontFields = googleFontsFields.querySelectorAll(".input-field"); const fontFields = googleFontsFields.querySelectorAll(".input-field");
googleFonts.length = 0; googleFonts.length = 0;
fontFields.forEach(field => { fontFields.forEach(field => {
@ -368,13 +384,11 @@ document.addEventListener("DOMContentLoaded", async () => {
.split(",").map(w => w.trim()).filter(Boolean); .split(",").map(w => w.trim()).filter(Boolean);
googleFonts.push({ family, weights }); googleFonts.push({ family, weights });
}); });
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -382,20 +396,17 @@ document.addEventListener("DOMContentLoaded", async () => {
renderGoogleFonts(googleFonts); renderGoogleFonts(googleFonts);
refreshFontDropdowns(); refreshFontDropdowns();
} }
}, true); // Use capture phase to catch blur from children }, true);
// Delegate remove button click for Google Fonts
googleFontsFields.addEventListener("click", async (e) => { googleFontsFields.addEventListener("click", async (e) => {
if (e.target.classList.contains("remove-google-font")) { if (e.target.classList.contains("remove-google-font")) {
const idx = Number(e.target.dataset.idx); const idx = Number(e.target.dataset.idx);
googleFonts.splice(idx, 1); googleFonts.splice(idx, 1);
// Save immediately to backend
await fetch("/api/theme-google-fonts", { await fetch("/api/theme-google-fonts", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts }) body: JSON.stringify({ theme_name: themeInfo.theme_name, google_fonts: googleFonts })
}); });
// Fetch updated theme info and refresh dropdowns
const updatedThemeInfo = await fetchThemeInfo(); const updatedThemeInfo = await fetchThemeInfo();
const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || []; const updatedGoogleFonts = updatedThemeInfo.theme_yaml.google_fonts || [];
googleFonts.length = 0; googleFonts.length = 0;
@ -406,9 +417,9 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
} }
// Form submit
document.getElementById("theme-editor-form").addEventListener("submit", async (e) => { document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
e.preventDefault(); e.preventDefault();
showLoader("Saving theme...");
const data = {}; const data = {};
data.colors = { data.colors = {
primary: document.getElementById("color-primary-text").value, primary: document.getElementById("color-primary-text").value,
@ -445,6 +456,7 @@ document.addEventListener("DOMContentLoaded", async () => {
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data }) body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
}); });
hideLoader();
if (res.ok) { if (res.ok) {
showToast("✅ Theme saved!", "success"); showToast("✅ Theme saved!", "success");
} else { } else {

View File

@ -1,41 +1,64 @@
// --- Loader helpers ---
function showLoader(text = "Uploading...") {
const loader = document.getElementById("global-loader");
if (loader) {
loader.style.display = "flex";
document.getElementById("loader-text").textContent = text;
}
}
function hideLoader() {
const loader = document.getElementById("global-loader");
if (loader) loader.style.display = "none";
}
// --- Upload gallery images --- // --- Upload gallery images ---
document.getElementById('upload-gallery').addEventListener('change', async (e) => { const galleryInput = document.getElementById('upload-gallery');
if (galleryInput) {
galleryInput.addEventListener('change', async (e) => {
const files = e.target.files; const files = e.target.files;
if (!files.length) return; if (!files.length) return;
showLoader("Uploading photos...");
const formData = new FormData(); const formData = new FormData();
for (const file of files) formData.append('files', file); for (const file of files) formData.append('files', file);
try { try {
const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData }); const res = await fetch('/api/gallery/upload', { method: 'POST', body: formData });
const data = await res.json(); const data = await res.json();
hideLoader();
if (res.ok) { if (res.ok) {
showToast(`${data.uploaded.length} gallery image(s) uploaded!`, "success"); showToast(`${data.uploaded.length} gallery image(s) uploaded!`, "success");
refreshGallery(); if (typeof refreshGallery === "function") refreshGallery();
} else showToast('Error: ' + data.error, "error"); } else showToast('Error: ' + data.error, "error");
} catch(err) { } catch(err) {
hideLoader();
console.error(err); console.error(err);
showToast('Server error!', "error"); showToast('Server error!', "error");
} finally { e.target.value = ''; } } finally { e.target.value = ''; }
}); });
}
// --- Upload hero images --- // --- Upload hero images ---
document.getElementById('upload-hero').addEventListener('change', async (e) => { const heroInput = document.getElementById('upload-hero');
if (heroInput) {
heroInput.addEventListener('change', async (e) => {
const files = e.target.files; const files = e.target.files;
if (!files.length) return; if (!files.length) return;
showLoader("Uploading hero photos...");
const formData = new FormData(); const formData = new FormData();
for (const file of files) formData.append('files', file); for (const file of files) formData.append('files', file);
try { try {
const res = await fetch('/api/hero/upload', { method: 'POST', body: formData }); const res = await fetch('/api/hero/upload', { method: 'POST', body: formData });
const data = await res.json(); const data = await res.json();
hideLoader();
if (res.ok) { if (res.ok) {
showToast(`${data.uploaded.length} hero image(s) uploaded!`, "success"); showToast(`${data.uploaded.length} hero image(s) uploaded!`, "success");
refreshHero(); if (typeof refreshHero === "function") refreshHero();
} else showToast('Error: ' + data.error, "error"); } else showToast('Error: ' + data.error, "error");
} catch(err) { } catch(err) {
hideLoader();
console.error(err); console.error(err);
showToast('Server error!', "error"); showToast('Server error!', "error");
} finally { e.target.value = ''; } } finally { e.target.value = ''; }
}); });
}

View File

@ -65,6 +65,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Loader -->
<div id="global-loader" style="display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;background:rgba(0,0,0,0.4);align-items:center;justify-content:center;">
<div style="background:#222;padding:32px 48px;border-radius:16px;box-shadow:0 2px 24px #000;display:flex;flex-direction:column;align-items:center;">
<div class="loader-spinner" style="width:48px;height:48px;border:6px solid #55c3ec;border-top:6px solid #222;border-radius:50%;animation:spin 1s linear infinite;"></div>
<div style="margin-top:18px;color:#fff;font-size:18px;" id="loader-text">Uploading...</div>
</div>
</div>
<style>
@keyframes spin { 100% { transform: rotate(360deg); } }
</style>
<!-- Scripts --> <!-- Scripts -->
<script src="{{ url_for('static', filename='js/build.js') }}" defer></script> <script src="{{ url_for('static', filename='js/build.js') }}" defer></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}