691 lines
21 KiB
JavaScript
691 lines
21 KiB
JavaScript
// Homepage Editor JavaScript - Updated for Hero Slides
|
|
|
|
let homepageData = {
|
|
heroSlides: [],
|
|
featuredProducts: {
|
|
enabled: true,
|
|
title: "Featured Products",
|
|
count: 4,
|
|
},
|
|
blog: {
|
|
enabled: true,
|
|
title: "Get Inspired",
|
|
count: 3,
|
|
},
|
|
about: {
|
|
enabled: true,
|
|
title: "About Sky Art Shop",
|
|
description:
|
|
"At Sky Art Shop, we believe in the power of creativity to transform and inspire. Whether you're an experienced crafter or just beginning your creative journey, we have everything you need to bring your ideas to life.",
|
|
imageUrl: "",
|
|
},
|
|
};
|
|
|
|
let currentMediaTarget = null;
|
|
let mediaLibrary = null;
|
|
|
|
// Quill editors storage
|
|
let quillEditors = {};
|
|
|
|
// Quill toolbar configuration
|
|
const quillToolbarOptions = [
|
|
["bold", "italic", "underline", "strike"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
[{ header: [1, 2, 3, false] }],
|
|
[{ color: [] }, { background: [] }],
|
|
[{ align: [] }],
|
|
["link"],
|
|
["clean"],
|
|
];
|
|
|
|
// Initialize on DOM load
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
// Initialize the modern media library
|
|
mediaLibrary = new MediaLibrary({
|
|
selectMode: true,
|
|
multiple: false,
|
|
onSelect: handleMediaSelection,
|
|
});
|
|
|
|
checkAuth().then((authenticated) => {
|
|
if (authenticated) {
|
|
loadHomepageSettings();
|
|
}
|
|
});
|
|
});
|
|
|
|
// Load homepage settings from backend
|
|
async function loadHomepageSettings() {
|
|
try {
|
|
const response = await fetch("/api/admin/homepage/settings", {
|
|
credentials: "include",
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.settings) {
|
|
// Migrate old format to new format if needed
|
|
if (data.settings.hero && !data.settings.heroSlides) {
|
|
// Convert old single hero to slides
|
|
homepageData.heroSlides = [
|
|
{
|
|
id: generateSlideId(),
|
|
title: data.settings.hero.headline || "Welcome to Sky Art Shop",
|
|
description:
|
|
stripHtml(data.settings.hero.description) ||
|
|
"Discover our beautiful collection of stationery and crafting supplies.",
|
|
buttonText: data.settings.hero.ctaText || "Shop Now",
|
|
buttonLink: data.settings.hero.ctaLink || "/shop",
|
|
imageUrl: data.settings.hero.backgroundUrl || "",
|
|
},
|
|
];
|
|
} else if (data.settings.heroSlides) {
|
|
homepageData.heroSlides = data.settings.heroSlides;
|
|
}
|
|
|
|
// Load featured products settings
|
|
if (data.settings.featuredProducts) {
|
|
homepageData.featuredProducts = data.settings.featuredProducts;
|
|
}
|
|
|
|
// Load blog settings
|
|
if (data.settings.blog) {
|
|
homepageData.blog = data.settings.blog;
|
|
}
|
|
|
|
// Load about settings
|
|
if (data.settings.about) {
|
|
homepageData.about = data.settings.about;
|
|
}
|
|
}
|
|
|
|
// Initialize with default slides if empty
|
|
if (homepageData.heroSlides.length === 0) {
|
|
homepageData.heroSlides = getDefaultSlides();
|
|
}
|
|
|
|
renderAllSections();
|
|
showSuccess("Homepage settings loaded successfully!");
|
|
} catch (error) {
|
|
console.error("Failed to load homepage settings:", error);
|
|
// Load defaults
|
|
homepageData.heroSlides = getDefaultSlides();
|
|
renderAllSections();
|
|
}
|
|
}
|
|
|
|
// Get default slides
|
|
function getDefaultSlides() {
|
|
return [
|
|
{
|
|
id: generateSlideId(),
|
|
title: "Welcome to Sky Art Shop",
|
|
description:
|
|
"Discover our beautiful collection of stationery, washi tapes, stickers, and crafting supplies. Perfect for bullet journaling, scrapbooking, and all your creative projects.",
|
|
buttonText: "Shop Now",
|
|
buttonLink: "/shop",
|
|
imageUrl:
|
|
"https://images.unsplash.com/photo-1513519245088-0e12902e35a6?w=1920&q=80",
|
|
},
|
|
{
|
|
id: generateSlideId(),
|
|
title: "Express Your Creativity",
|
|
description:
|
|
"From vibrant markers to elegant journals, find everything you need to bring your artistic vision to life. Quality supplies for every crafter.",
|
|
buttonText: "Explore Collection",
|
|
buttonLink: "/shop",
|
|
imageUrl:
|
|
"https://images.unsplash.com/photo-1456735190827-d1262f71b8a3?w=1920&q=80",
|
|
},
|
|
{
|
|
id: generateSlideId(),
|
|
title: "New Arrivals Every Week",
|
|
description:
|
|
"Stay inspired with our constantly updated selection of unique and trendy stationery. Be the first to discover our latest additions.",
|
|
buttonText: "See What's New",
|
|
buttonLink: "/shop",
|
|
imageUrl:
|
|
"https://images.unsplash.com/photo-1452860606245-08befc0ff44b?w=1920&q=80",
|
|
},
|
|
];
|
|
}
|
|
|
|
// Strip HTML tags from text
|
|
function stripHtml(html) {
|
|
if (!html) return "";
|
|
const tmp = document.createElement("DIV");
|
|
tmp.innerHTML = html;
|
|
return tmp.textContent || tmp.innerText || "";
|
|
}
|
|
|
|
// Generate unique slide ID
|
|
function generateSlideId() {
|
|
return "slide_" + Date.now() + "_" + Math.random().toString(36).substr(2, 9);
|
|
}
|
|
|
|
// Render all sections
|
|
function renderAllSections() {
|
|
renderSlides();
|
|
renderFeaturedProducts();
|
|
renderBlogSection();
|
|
renderAboutSection();
|
|
}
|
|
|
|
// Render hero slides
|
|
function renderSlides() {
|
|
const container = document.getElementById("slidesContainer");
|
|
const countBadge = document.getElementById("slideCount");
|
|
|
|
// Destroy existing Quill editors for slides
|
|
Object.keys(quillEditors).forEach((key) => {
|
|
if (key.startsWith("slide_desc_")) {
|
|
delete quillEditors[key];
|
|
}
|
|
});
|
|
|
|
countBadge.textContent = `${homepageData.heroSlides.length} slides`;
|
|
|
|
if (homepageData.heroSlides.length === 0) {
|
|
container.innerHTML =
|
|
'<p class="text-muted text-center py-4">No slides yet. Click "Add New Slide" to create one.</p>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = homepageData.heroSlides
|
|
.map(
|
|
(slide, index) => `
|
|
<div class="slide-card" data-slide-id="${slide.id}">
|
|
<div class="slide-header">
|
|
<span class="slide-number">
|
|
<i class="bi bi-grip-vertical drag-handle me-2"></i>
|
|
Slide ${index + 1}
|
|
</span>
|
|
<div class="slide-actions">
|
|
${
|
|
index > 0
|
|
? `<button class="btn btn-sm btn-outline-secondary" onclick="moveSlide('${slide.id}', -1)" title="Move Up"><i class="bi bi-arrow-up"></i></button>`
|
|
: ""
|
|
}
|
|
${
|
|
index < homepageData.heroSlides.length - 1
|
|
? `<button class="btn btn-sm btn-outline-secondary" onclick="moveSlide('${slide.id}', 1)" title="Move Down"><i class="bi bi-arrow-down"></i></button>`
|
|
: ""
|
|
}
|
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteSlide('${
|
|
slide.id
|
|
}')" title="Delete Slide"><i class="bi bi-trash"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Slide Title *</label>
|
|
<input type="text" class="form-control" value="${escapeHtml(
|
|
slide.title
|
|
)}" onchange="updateSlide('${
|
|
slide.id
|
|
}', 'title', this.value)" placeholder="Enter slide title">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<div class="quill-container">
|
|
<div id="slideDesc_${slide.id}"></div>
|
|
</div>
|
|
</div>
|
|
<div class="row">
|
|
<div class="col-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Button Text</label>
|
|
<input type="text" class="form-control" value="${escapeHtml(
|
|
slide.buttonText
|
|
)}" onchange="updateSlide('${
|
|
slide.id
|
|
}', 'buttonText', this.value)" placeholder="Shop Now">
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Button Link</label>
|
|
<input type="text" class="form-control" value="${escapeHtml(
|
|
slide.buttonLink
|
|
)}" onchange="updateSlide('${
|
|
slide.id
|
|
}', 'buttonLink', this.value)" placeholder="/shop">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Background Image *</label>
|
|
<small class="d-block text-muted mb-2"><i class="bi bi-info-circle"></i> Recommended: <strong>1920 x 600 px</strong> (landscape)</small>
|
|
<input type="hidden" id="slideImage_${slide.id}" value="${
|
|
slide.imageUrl || ""
|
|
}">
|
|
<button type="button" class="btn btn-outline-primary w-100" onclick="openMediaLibrary('slide', '${
|
|
slide.id
|
|
}')">
|
|
<i class="bi bi-folder2-open"></i> Choose Image
|
|
</button>
|
|
<div class="image-preview ${
|
|
slide.imageUrl ? "" : "empty"
|
|
}" id="slidePreview_${slide.id}">
|
|
${
|
|
slide.imageUrl
|
|
? `<img src="${slide.imageUrl}" alt="Slide preview">`
|
|
: '<i class="bi bi-image" style="font-size: 2rem"></i>'
|
|
}
|
|
</div>
|
|
${
|
|
slide.imageUrl
|
|
? `
|
|
<button type="button" class="btn btn-sm btn-outline-danger mt-2" onclick="clearSlideImage('${slide.id}')">
|
|
<i class="bi bi-x-circle"></i> Clear Image
|
|
</button>
|
|
`
|
|
: ""
|
|
}
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Or enter image URL directly</label>
|
|
<input type="text" class="form-control" value="${escapeHtml(
|
|
slide.imageUrl || ""
|
|
)}" onchange="updateSlideImage('${
|
|
slide.id
|
|
}', this.value)" placeholder="https://example.com/image.jpg">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`
|
|
)
|
|
.join("");
|
|
|
|
// Initialize Quill editors for each slide description
|
|
setTimeout(() => {
|
|
homepageData.heroSlides.forEach((slide) => {
|
|
initSlideQuillEditor(slide.id, slide.description);
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
// Add new slide
|
|
function addNewSlide() {
|
|
const newSlide = {
|
|
id: generateSlideId(),
|
|
title: "",
|
|
description: "",
|
|
buttonText: "Shop Now",
|
|
buttonLink: "/shop",
|
|
imageUrl: "",
|
|
};
|
|
|
|
homepageData.heroSlides.push(newSlide);
|
|
renderSlides();
|
|
|
|
// Scroll to the new slide
|
|
setTimeout(() => {
|
|
const newSlideEl = document.querySelector(
|
|
`[data-slide-id="${newSlide.id}"]`
|
|
);
|
|
if (newSlideEl) {
|
|
newSlideEl.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
newSlideEl.classList.add("active");
|
|
setTimeout(() => newSlideEl.classList.remove("active"), 2000);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
// Update slide field
|
|
function updateSlide(slideId, field, value) {
|
|
const slide = homepageData.heroSlides.find((s) => s.id === slideId);
|
|
if (slide) {
|
|
slide[field] = value;
|
|
}
|
|
}
|
|
|
|
// Update slide image
|
|
function updateSlideImage(slideId, url) {
|
|
const slide = homepageData.heroSlides.find((s) => s.id === slideId);
|
|
if (slide) {
|
|
slide.imageUrl = url;
|
|
const preview = document.getElementById(`slidePreview_${slideId}`);
|
|
if (preview) {
|
|
if (url) {
|
|
preview.classList.remove("empty");
|
|
preview.innerHTML = `<img src="${url}" alt="Slide preview">`;
|
|
} else {
|
|
preview.classList.add("empty");
|
|
preview.innerHTML =
|
|
'<i class="bi bi-image" style="font-size: 2rem"></i>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear slide image
|
|
function clearSlideImage(slideId) {
|
|
updateSlide(slideId, "imageUrl", "");
|
|
document.getElementById(`slideImage_${slideId}`).value = "";
|
|
renderSlides();
|
|
}
|
|
|
|
// Move slide up or down
|
|
function moveSlide(slideId, direction) {
|
|
const index = homepageData.heroSlides.findIndex((s) => s.id === slideId);
|
|
if (index === -1) return;
|
|
|
|
const newIndex = index + direction;
|
|
if (newIndex < 0 || newIndex >= homepageData.heroSlides.length) return;
|
|
|
|
// Swap
|
|
const temp = homepageData.heroSlides[index];
|
|
homepageData.heroSlides[index] = homepageData.heroSlides[newIndex];
|
|
homepageData.heroSlides[newIndex] = temp;
|
|
|
|
renderSlides();
|
|
}
|
|
|
|
// Delete slide
|
|
function deleteSlide(slideId) {
|
|
if (homepageData.heroSlides.length <= 1) {
|
|
showError("You must have at least one slide");
|
|
return;
|
|
}
|
|
|
|
showDeleteConfirm(
|
|
"Are you sure you want to delete this slide?",
|
|
() => {
|
|
homepageData.heroSlides = homepageData.heroSlides.filter(
|
|
(s) => s.id !== slideId
|
|
);
|
|
renderSlides();
|
|
showSuccess("Slide deleted");
|
|
},
|
|
{ title: "Delete Slide", confirmText: "Delete Slide" }
|
|
);
|
|
}
|
|
|
|
// Render featured products section
|
|
function renderFeaturedProducts() {
|
|
document.getElementById("featuredEnabled").checked =
|
|
homepageData.featuredProducts.enabled;
|
|
document.getElementById("featuredTitle").value =
|
|
homepageData.featuredProducts.title || "Featured Products";
|
|
|
|
// Set active count button
|
|
document
|
|
.querySelectorAll("#featuredProductsSection .count-btn")
|
|
.forEach((btn) => {
|
|
btn.classList.toggle(
|
|
"active",
|
|
parseInt(btn.dataset.count) === homepageData.featuredProducts.count
|
|
);
|
|
});
|
|
}
|
|
|
|
// Set featured products count
|
|
function setFeaturedCount(count) {
|
|
homepageData.featuredProducts.count = count;
|
|
document
|
|
.querySelectorAll("#featuredProductsSection .count-btn")
|
|
.forEach((btn) => {
|
|
btn.classList.toggle("active", parseInt(btn.dataset.count) === count);
|
|
});
|
|
}
|
|
|
|
// Render blog section
|
|
function renderBlogSection() {
|
|
document.getElementById("blogEnabled").checked = homepageData.blog.enabled;
|
|
document.getElementById("blogTitle").value =
|
|
homepageData.blog.title || "Get Inspired";
|
|
|
|
// Set active count button
|
|
document.querySelectorAll("#blogSection .count-btn").forEach((btn) => {
|
|
btn.classList.toggle(
|
|
"active",
|
|
parseInt(btn.dataset.count) === homepageData.blog.count
|
|
);
|
|
});
|
|
}
|
|
|
|
// Set blog posts count
|
|
function setBlogCount(count) {
|
|
homepageData.blog.count = count;
|
|
document.querySelectorAll("#blogSection .count-btn").forEach((btn) => {
|
|
btn.classList.toggle("active", parseInt(btn.dataset.count) === count);
|
|
});
|
|
}
|
|
|
|
// Render about section
|
|
function renderAboutSection() {
|
|
document.getElementById("aboutEnabled").checked = homepageData.about.enabled;
|
|
document.getElementById("aboutTitle").value =
|
|
homepageData.about.title || "About Sky Art Shop";
|
|
|
|
// Initialize Quill for about description
|
|
initAboutQuillEditor(homepageData.about.description || "");
|
|
|
|
const preview = document.getElementById("aboutPreview");
|
|
if (homepageData.about.imageUrl) {
|
|
document.getElementById("aboutImageUrl").value =
|
|
homepageData.about.imageUrl;
|
|
preview.classList.remove("empty");
|
|
preview.innerHTML = `<img src="${homepageData.about.imageUrl}" alt="About preview">`;
|
|
}
|
|
}
|
|
|
|
// Initialize Quill editor for About section
|
|
function initAboutQuillEditor(content) {
|
|
const editorEl = document.getElementById("aboutDescriptionEditor");
|
|
if (!editorEl) return;
|
|
|
|
// Destroy existing editor if any
|
|
if (quillEditors["about"]) {
|
|
delete quillEditors["about"];
|
|
}
|
|
|
|
quillEditors["about"] = new Quill("#aboutDescriptionEditor", {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: quillToolbarOptions,
|
|
},
|
|
placeholder: "Write a short description about your shop...",
|
|
});
|
|
|
|
// Set initial content
|
|
if (content) {
|
|
quillEditors["about"].root.innerHTML = content;
|
|
}
|
|
|
|
// Update data on change
|
|
quillEditors["about"].on("text-change", function () {
|
|
homepageData.about.description = quillEditors["about"].root.innerHTML;
|
|
});
|
|
}
|
|
|
|
// Initialize Quill editor for slide description
|
|
function initSlideQuillEditor(slideId, content) {
|
|
const editorEl = document.getElementById(`slideDesc_${slideId}`);
|
|
if (!editorEl) return;
|
|
|
|
const editorKey = `slide_desc_${slideId}`;
|
|
|
|
quillEditors[editorKey] = new Quill(`#slideDesc_${slideId}`, {
|
|
theme: "snow",
|
|
modules: {
|
|
toolbar: [
|
|
["bold", "italic", "underline"],
|
|
[{ list: "ordered" }, { list: "bullet" }],
|
|
["link"],
|
|
["clean"],
|
|
],
|
|
},
|
|
placeholder: "Enter slide description...",
|
|
});
|
|
|
|
// Set initial content
|
|
if (content) {
|
|
quillEditors[editorKey].root.innerHTML = content;
|
|
}
|
|
|
|
// Update slide data on change
|
|
quillEditors[editorKey].on("text-change", function () {
|
|
const slide = homepageData.heroSlides.find((s) => s.id === slideId);
|
|
if (slide) {
|
|
slide.description = quillEditors[editorKey].root.innerHTML;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Open media library
|
|
function openMediaLibrary(type, id = null) {
|
|
currentMediaTarget = { type, id };
|
|
mediaLibrary.open();
|
|
}
|
|
|
|
// Handle media selection
|
|
function handleMediaSelection(media) {
|
|
if (!currentMediaTarget) return;
|
|
|
|
// Media is now a single object with path property
|
|
if (!media || !media.path) {
|
|
showError("No image selected");
|
|
return;
|
|
}
|
|
|
|
console.log("Selected media:", media);
|
|
|
|
const { type, id } = currentMediaTarget;
|
|
|
|
if (type === "slide" && id) {
|
|
const slide = homepageData.heroSlides.find((s) => s.id === id);
|
|
if (slide) {
|
|
slide.imageUrl = media.path;
|
|
const hiddenInput = document.getElementById(`slideImage_${id}`);
|
|
if (hiddenInput) {
|
|
hiddenInput.value = media.path;
|
|
}
|
|
const preview = document.getElementById(`slidePreview_${id}`);
|
|
if (preview) {
|
|
preview.classList.remove("empty");
|
|
preview.innerHTML = `<img src="${media.path}" alt="Slide preview">`;
|
|
}
|
|
console.log("Updated slide:", slide);
|
|
// Don't re-render, just update the preview and data
|
|
}
|
|
} else if (type === "about") {
|
|
homepageData.about.imageUrl = media.path;
|
|
document.getElementById("aboutImageUrl").value = media.path;
|
|
const preview = document.getElementById("aboutPreview");
|
|
preview.classList.remove("empty");
|
|
preview.innerHTML = `<img src="${media.path}" alt="About preview">`;
|
|
}
|
|
|
|
currentMediaTarget = null;
|
|
showSuccess("Image selected successfully!");
|
|
}
|
|
|
|
// Escape HTML
|
|
function escapeHtml(text) {
|
|
if (!text) return "";
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Save all homepage settings
|
|
async function saveHomepage() {
|
|
// Collect all data from form
|
|
homepageData.featuredProducts = {
|
|
enabled: document.getElementById("featuredEnabled").checked,
|
|
title: document.getElementById("featuredTitle").value,
|
|
count: homepageData.featuredProducts.count,
|
|
};
|
|
|
|
homepageData.blog = {
|
|
enabled: document.getElementById("blogEnabled").checked,
|
|
title: document.getElementById("blogTitle").value,
|
|
count: homepageData.blog.count,
|
|
};
|
|
|
|
// Get description from Quill editor
|
|
const aboutDescription = quillEditors["about"]
|
|
? quillEditors["about"].root.innerHTML
|
|
: homepageData.about.description;
|
|
|
|
homepageData.about = {
|
|
enabled: document.getElementById("aboutEnabled").checked,
|
|
title: document.getElementById("aboutTitle").value,
|
|
description: aboutDescription,
|
|
imageUrl: document.getElementById("aboutImageUrl").value,
|
|
};
|
|
|
|
// Update slide descriptions from Quill editors
|
|
homepageData.heroSlides.forEach((slide) => {
|
|
const editorKey = `slide_desc_${slide.id}`;
|
|
if (quillEditors[editorKey]) {
|
|
slide.description = quillEditors[editorKey].root.innerHTML;
|
|
}
|
|
});
|
|
|
|
// Validate slides
|
|
for (const slide of homepageData.heroSlides) {
|
|
if (!slide.title) {
|
|
showError("All slides must have a title");
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch("/api/admin/homepage/settings", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
credentials: "include",
|
|
body: JSON.stringify(homepageData),
|
|
});
|
|
|
|
const data = await response.json();
|
|
if (data.success) {
|
|
showSuccess(
|
|
"Homepage settings saved successfully! Changes are now live on the frontend."
|
|
);
|
|
} else {
|
|
showError(data.message || "Failed to save homepage settings");
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to save homepage:", error);
|
|
showError("Failed to save homepage settings");
|
|
}
|
|
}
|
|
|
|
// Show success notification
|
|
function showSuccess(message) {
|
|
const alert = document.createElement("div");
|
|
alert.className =
|
|
"alert alert-success alert-dismissible fade show position-fixed";
|
|
alert.style.cssText =
|
|
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
|
alert.innerHTML = `
|
|
<i class="bi bi-check-circle me-2"></i>${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.body.appendChild(alert);
|
|
setTimeout(() => alert.remove(), 5000);
|
|
}
|
|
|
|
// Show error notification
|
|
function showError(message) {
|
|
const alert = document.createElement("div");
|
|
alert.className =
|
|
"alert alert-danger alert-dismissible fade show position-fixed";
|
|
alert.style.cssText =
|
|
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
|
|
alert.innerHTML = `
|
|
<i class="bi bi-exclamation-circle me-2"></i>${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
document.body.appendChild(alert);
|
|
setTimeout(() => alert.remove(), 5000);
|
|
}
|