Build and upload loader
This commit is contained in:
@ -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";
|
||||||
|
@ -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" },
|
||||||
|
@ -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 {
|
||||||
|
@ -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 = ''; }
|
||||||
});
|
});
|
||||||
|
}
|
@ -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 %}
|
||||||
|
Reference in New Issue
Block a user