updateweb

This commit is contained in:
Local Server
2025-12-24 00:13:23 -06:00
parent e4b3de4a46
commit 017c6376fc
88 changed files with 17866 additions and 1191 deletions

View File

@@ -12,6 +12,11 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<!-- Quill Editor CSS -->
<link
href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
</head>
<body>
@@ -123,11 +128,33 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Create Blog Post</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
<div class="d-flex gap-2 align-items-center">
<button
type="button"
class="btn btn-sm btn-primary"
id="btnExpandModal"
onclick="toggleModalSize()"
title="Expand/Collapse"
style="
padding: 0.375rem 0.75rem;
display: flex;
align-items: center;
gap: 5px;
"
>
<i
class="bi bi-arrows-fullscreen"
id="expandIcon"
style="font-size: 16px"
></i>
<span style="font-size: 13px">Expand</span>
</button>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
</div>
<div class="modal-body">
<form id="postForm">
@@ -168,22 +195,44 @@
<div class="mb-3">
<label for="postContent" class="form-label">Content *</label>
<textarea
class="form-control"
id="postContent"
rows="10"
required
></textarea>
<div
id="postContentEditor"
style="
height: 400px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
overflow: hidden;
"
>
<style>
#postContentEditor .ql-container {
height: calc(400px - 42px);
overflow-y: auto;
font-size: 16px;
}
#postContentEditor .ql-editor {
min-height: 100%;
}
</style>
</div>
<input type="hidden" id="postContent" />
</div>
<div class="mb-3">
<label for="postImage" class="form-label">Featured Image</label>
<input
type="file"
class="form-control"
id="postImage"
accept="image/*"
/>
<label class="form-label">Featured Image</label>
<input type="hidden" id="postFeaturedImage" />
<div
id="featuredImagePreview"
style="margin-bottom: 10px"
></div>
<button
type="button"
class="btn btn-outline-primary btn-sm"
onclick="openMediaLibraryForFeaturedImage()"
>
<i class="bi bi-image"></i> Select from Media Library
</button>
</div>
<div class="row">
@@ -236,7 +285,9 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Quill Editor JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/blog.js"></script>
<script src="/admin/js/blog.js?v=8.0"></script>
</body>
</html>

View File

@@ -586,3 +586,483 @@ body {
height: 2px;
background: linear-gradient(to right, #667eea, transparent);
}
/* Product Image Variants Styling */
.image-variant-item {
transition: all 0.2s ease;
}
.image-variant-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
#imageVariantsContainer .form-control-sm {
font-size: 0.875rem;
}
#imageVariantsContainer .form-label.small {
font-size: 0.8rem;
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
#productDescriptionEditor {
background: white;
}
#productDescriptionEditor .ql-editor {
min-height: 200px;
}
/* Image Picker Grid for Color Variants */
.image-picker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 15px;
padding: 15px;
background: white;
border: 2px dashed #dee2e6;
border-radius: 8px;
margin-top: 5px;
}
.image-picker-item {
position: relative;
cursor: pointer;
border: 3px solid transparent;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
background: #f8f9fa;
aspect-ratio: 1;
display: flex;
flex-direction: column;
}
.image-picker-item:hover {
border-color: #667eea;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2);
}
.image-picker-item.selected {
border-color: #28a745;
background: #e8f5e9;
}
.image-picker-item img {
width: 100%;
height: calc(100% - 25px);
object-fit: cover;
display: block;
}
.image-picker-overlay {
position: absolute;
top: 5px;
right: 5px;
background: rgba(40, 167, 69, 0.95);
color: white;
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
font-size: 16px;
}
.image-picker-item.selected .image-picker-overlay {
opacity: 1;
}
.image-picker-label {
display: block;
padding: 4px 6px;
text-align: center;
font-size: 11px;
background: #f8f9fa;
border-top: 1px solid #dee2e6;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #495057;
height: 25px;
line-height: 17px;
}
.image-picker-item.selected .image-picker-label {
background: #e8f5e9;
color: #28a745;
font-weight: 600;
}
/* Responsive adjustments for image picker */
@media (max-width: 768px) {
.image-picker-grid {
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
padding: 10px;
}
}
@media (max-width: 480px) {
.image-picker-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 8px;
}
.image-picker-label {
font-size: 10px;
padding: 3px 4px;
}
}
/* Toast Notifications */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 10px;
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 12px;
min-width: 300px;
max-width: 500px;
padding: 16px 20px;
background: white;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.15);
pointer-events: auto;
transform: translateX(400px);
opacity: 0;
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55);
font-weight: 500;
}
.toast-show {
transform: translateX(0);
opacity: 1;
}
.toast-hide {
transform: translateX(400px);
opacity: 0;
}
.toast-icon {
font-size: 26px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-weight: bold;
}
.toast-message {
flex: 1;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
color: #1a1a1a;
}
.toast-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.toast-close:hover {
background: rgba(0, 0, 0, 0.05);
color: #666;
}
/* Toast Types */
.toast-success {
border-left: 5px solid #10b981;
background: linear-gradient(to right, #ecfdf5 0%, #ffffff 100%);
}
.toast-success .toast-icon {
color: #10b981;
}
.toast-error {
border-left: 5px solid #ef4444;
background: linear-gradient(to right, #fef2f2 0%, #ffffff 100%);
}
.toast-error .toast-icon {
color: #ef4444;
}
.toast-warning {
border-left: 5px solid #f59e0b;
background: linear-gradient(to right, #fffbeb 0%, #ffffff 100%);
}
.toast-warning .toast-icon {
color: #f59e0b;
}
.toast-info {
border-left: 5px solid #3b82f6;
background: linear-gradient(to right, #eff6ff 0%, #ffffff 100%);
}
.toast-info .toast-icon {
color: #3b82f6;
}
/* Responsive Toast */
@media (max-width: 768px) {
.toast-container {
top: 10px;
right: 10px;
left: 10px;
}
.toast {
min-width: auto;
max-width: none;
}
}
/* Dark Mode Styles */
body.dark-mode {
background-color: #1a1a1a;
color: #f0f0f0;
}
body.dark-mode .main-content {
background-color: #1a1a1a;
}
body.dark-mode .top-bar {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
color: #f0f0f0;
}
body.dark-mode .settings-section,
body.dark-mode .modal-content {
background: #2d3748;
color: #f0f0f0;
border-color: #4a5568;
}
body.dark-mode .settings-section h4 {
color: #ffffff;
border-bottom-color: #4a5568;
font-weight: 600;
}
body.dark-mode .form-control,
body.dark-mode .form-select {
background-color: #1f2937;
color: #ffffff;
border-color: #4a5568;
}
body.dark-mode .form-control:focus,
body.dark-mode .form-select:focus {
background-color: #374151;
border-color: #667eea;
color: #ffffff;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
body.dark-mode .form-control::placeholder {
color: #9ca3af;
opacity: 1;
}
body.dark-mode label {
color: #e5e7eb;
font-weight: 500;
}
body.dark-mode .btn-outline-secondary {
background-color: #374151;
color: #e0e0e0;
border-color: #4a5568;
}
body.dark-mode .btn-outline-secondary:hover {
background-color: #4a5568;
border-color: #667eea;
}
body.dark-mode .logo-preview,
body.dark-mode .favicon-preview {
background: #374151;
border-color: #4a5568;
}
body.dark-mode .theme-option {
background: #374151;
border-color: #4a5568;
color: #e0e0e0;
}
body.dark-mode .theme-option:hover {
border-color: #667eea;
background: #4a5568;
}
body.dark-mode .theme-option.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea22 0%, #764ba233 100%);
}
body.dark-mode .text-muted {
color: #9ca3af !important;
}
body.dark-mode .modal-header {
background: #2d3748;
border-bottom-color: #4a5568;
}
body.dark-mode .modal-footer {
background: #2d3748;
border-top-color: #4a5568;
}
body.dark-mode .media-item {
background: #374151;
}
body.dark-mode .media-item:hover {
background: #4a5568;
}
body.dark-mode .toast {
background: #2d3748;
color: #ffffff;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
body.dark-mode .toast-success {
background: linear-gradient(to right, #064e3b 0%, #2d3748 100%);
}
body.dark-mode .toast-error {
background: linear-gradient(to right, #7f1d1d 0%, #2d3748 100%);
}
body.dark-mode .toast-warning {
background: linear-gradient(to right, #78350f 0%, #2d3748 100%);
}
body.dark-mode .toast-info {
background: linear-gradient(to right, #1e3a8a 0%, #2d3748 100%);
}
body.dark-mode .toast-message {
color: #ffffff;
font-weight: 600;
}
body.dark-mode .btn-close {
filter: invert(1);
opacity: 0.8;
}
body.dark-mode .btn-close:hover {
opacity: 1;
}
body.dark-mode .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: #ffffff;
}
body.dark-mode .btn-primary:hover {
background: linear-gradient(135deg, #5568d3 0%, #6a3f8f 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
body.dark-mode .card {
background: #2d3748;
border-color: #4a5568;
}
body.dark-mode .card-body {
color: #f0f0f0;
}
body.dark-mode .table {
color: #f0f0f0;
}
body.dark-mode .table thead th {
background: #374151;
color: #ffffff;
border-color: #4a5568;
}
body.dark-mode .table tbody td {
border-color: #4a5568;
}
body.dark-mode .table tbody tr:hover {
background: #374151;
}
body.dark-mode select option {
background: #1f2937;
color: #ffffff;
}
body.dark-mode .input-group-text {
background: #374151;
color: #f0f0f0;
border-color: #4a5568;
}
body.dark-mode .dropdown-menu {
background: #2d3748;
border-color: #4a5568;
}
body.dark-mode .dropdown-item {
color: #f0f0f0;
}
body.dark-mode .dropdown-item:hover {
background: #374151;
color: #ffffff;
}
body.dark-mode hr {
border-color: #4a5568;
opacity: 1;
}
body.dark-mode .card-body {
color: #f0f0f0;
}

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Admin Dashboard - Sky Art Shop</title>
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Homepage Editor - Sky Art Shop</title>
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
@@ -12,6 +13,10 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link
href="https://cdn.jsdelivr.net/npm/quill@1.3.6/dist/quill.snow.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
.section-builder {
@@ -112,6 +117,30 @@
z-index: 999;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
/* Quill Editor Styling */
.ql-container {
min-height: 150px;
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
}
.ql-toolbar {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background: #f8f9fa;
}
.ql-editor {
min-height: 150px;
font-size: 15px;
line-height: 1.6;
}
.ql-editor.ql-blank::before {
color: #adb5bd;
font-style: italic;
}
</style>
</head>
<body>
@@ -222,11 +251,10 @@
<div class="mb-3">
<label class="form-label">Description</label>
<textarea
class="form-control"
<div
id="heroDescription"
rows="3"
></textarea>
style="background: white; min-height: 150px"
></div>
</div>
<div class="row">
@@ -252,16 +280,26 @@
<div class="mb-3">
<label class="form-label">Background Image/Video</label>
<input
type="file"
class="form-control"
id="heroBackground"
accept="image/*,video/*"
onchange="previewImage('hero')"
/>
<input type="hidden" id="heroBackgroundUrl" />
<button
type="button"
class="btn btn-outline-primary w-100"
onclick="openMediaLibrary('hero', 'background')"
>
<i class="bi bi-folder2-open"></i> Choose from Media Library
</button>
<div class="image-preview empty" id="heroPreview">
<i class="bi bi-image" style="font-size: 3rem"></i>
</div>
<button
type="button"
class="btn btn-sm btn-outline-danger mt-2"
onclick="clearMedia('hero', 'background')"
id="heroBackgroundClear"
style="display: none"
>
<i class="bi bi-x-circle"></i> Clear Background
</button>
</div>
<div class="mb-3">
@@ -323,25 +361,34 @@
<div class="mb-3">
<label class="form-label">Description</label>
<textarea
class="form-control"
<div
id="promotionDescription"
rows="3"
></textarea>
style="background: white; min-height: 150px"
></div>
</div>
<div class="mb-3">
<label class="form-label">Section Image</label>
<input
type="file"
class="form-control"
id="promotionImage"
accept="image/*"
onchange="previewImage('promotion')"
/>
<input type="hidden" id="promotionImageUrl" />
<button
type="button"
class="btn btn-outline-primary w-100"
onclick="openMediaLibrary('promotion', 'image')"
>
<i class="bi bi-folder2-open"></i> Choose from Media Library
</button>
<div class="image-preview empty" id="promotionPreview">
<i class="bi bi-image" style="font-size: 3rem"></i>
</div>
<button
type="button"
class="btn btn-sm btn-outline-danger mt-2"
onclick="clearMedia('promotion', 'image')"
id="promotionImageClear"
style="display: none"
>
<i class="bi bi-x-circle"></i> Clear Image
</button>
</div>
<div class="row">
@@ -428,11 +475,10 @@
<div class="mb-3">
<label class="form-label">Description</label>
<textarea
class="form-control"
<div
id="portfolioDescription"
rows="3"
></textarea>
style="background: white; min-height: 150px"
></div>
</div>
<div class="mb-3">
@@ -459,6 +505,7 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.6/dist/quill.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/homepage.js"></script>
</body>

View File

@@ -7,6 +7,50 @@ window.adminAuth = {
isAuthenticated: false,
};
// Load and apply theme on all admin pages
function loadAdminTheme() {
const savedTheme = localStorage.getItem("adminTheme") || "light";
applyAdminTheme(savedTheme);
// Watch for system theme changes if in auto mode
if (savedTheme === "auto") {
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", (e) => {
if (localStorage.getItem("adminTheme") === "auto") {
applyAdminTheme("auto");
}
});
}
}
function applyAdminTheme(theme) {
const body = document.body;
if (theme === "dark") {
body.classList.add("dark-mode");
body.classList.remove("light-mode");
} else if (theme === "light") {
body.classList.add("light-mode");
body.classList.remove("dark-mode");
} else if (theme === "auto") {
// Check system preference
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
body.classList.add("dark-mode");
body.classList.remove("light-mode");
} else {
body.classList.add("light-mode");
body.classList.remove("dark-mode");
}
}
}
// Initialize theme immediately (before page loads)
loadAdminTheme();
// Check authentication and redirect if needed - attach to window
window.checkAuth = async function () {
try {
@@ -360,3 +404,22 @@ if (window.location.pathname !== "/admin/login.html") {
});
});
}
// Fix Bootstrap modal aria-hidden focus warning for all modals - Universal Solution
(function () {
// Use event delegation on document level to catch all modal hide events
document.addEventListener(
"hide.bs.modal",
function (event) {
// Get the modal that's closing
const modalElement = event.target;
// Blur any focused element inside the modal before it closes
const focusedElement = document.activeElement;
if (focusedElement && modalElement.contains(focusedElement)) {
focusedElement.blur();
}
},
true
); // Use capture phase to run before Bootstrap's handlers
})();

View File

@@ -2,9 +2,12 @@
let postsData = [];
let postModal;
let quillEditor;
let isModalExpanded = false;
document.addEventListener("DOMContentLoaded", function () {
postModal = new bootstrap.Modal(document.getElementById("postModal"));
initializeQuillEditor();
checkAuth().then((authenticated) => {
if (authenticated) {
loadPosts();
@@ -24,16 +27,224 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
function resetModalSize() {
const modalDialog = document.querySelector("#postModal .modal-dialog");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.querySelector("#btnExpandModal span");
const editor = document.getElementById("postContentEditor");
if (modalDialog && expandIcon && expandText && editor) {
modalDialog.classList.remove("modal-fullscreen");
modalDialog.classList.add("modal-xl");
expandIcon.className = "bi bi-arrows-fullscreen";
expandText.textContent = "Expand";
editor.style.height = "400px";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(400px - 42px)";
}
isModalExpanded = false;
}
}
function toggleModalSize() {
const modalDialog = document.querySelector("#postModal .modal-dialog");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.querySelector("#btnExpandModal span");
const editor = document.getElementById("postContentEditor");
if (!modalDialog || !expandIcon || !expandText || !editor) {
console.error("Modal elements not found");
return;
}
if (isModalExpanded) {
// Collapse to normal size
modalDialog.classList.remove("modal-fullscreen");
modalDialog.classList.add("modal-xl");
expandIcon.className = "bi bi-arrows-fullscreen";
expandText.textContent = "Expand";
editor.style.height = "400px";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(400px - 42px)";
}
isModalExpanded = false;
} else {
// Expand to fullscreen
modalDialog.classList.remove("modal-xl");
modalDialog.classList.add("modal-fullscreen");
expandIcon.className = "bi bi-fullscreen-exit";
expandText.textContent = "Collapse";
editor.style.height = "60vh";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(60vh - 42px)";
}
isModalExpanded = true;
}
}
function initializeQuillEditor() {
quillEditor = new Quill("#postContentEditor", {
theme: "snow",
placeholder: "Write your blog post content here...",
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
[{ color: [] }, { background: [] }],
["link", "image"],
["blockquote", "code-block"],
["clean"],
],
},
});
}
function openMediaLibraryForFeaturedImage() {
// Create modal backdrop
const backdrop = document.createElement("div");
backdrop.id = "mediaLibraryBackdrop";
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
`;
// Create modal container
const modal = document.createElement("div");
modal.id = "mediaLibraryModal";
modal.style.cssText = `
position: relative;
width: 90%;
max-width: 1200px;
height: 85vh;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
`;
// Create close button
const closeBtn = document.createElement("button");
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
z-index: 10000;
background: #dc3545;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.onclick = closeMediaLibrary;
// Create iframe
const iframe = document.createElement("iframe");
iframe.id = "mediaLibraryFrame";
iframe.src = "/admin/media-library.html?selectMode=true";
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
modal.appendChild(closeBtn);
modal.appendChild(iframe);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.onclick = function (e) {
if (e.target === backdrop) {
closeMediaLibrary();
}
};
// Setup media selection handler
window.handleMediaSelection = function (media) {
const mediaItem = Array.isArray(media) ? media[0] : media;
if (mediaItem && mediaItem.url) {
document.getElementById("postFeaturedImage").value = mediaItem.url;
updateFeaturedImagePreview(mediaItem.url);
showToast("Featured image selected", "success");
}
closeMediaLibrary();
};
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
}
}
function updateFeaturedImagePreview(url) {
const preview = document.getElementById("featuredImagePreview");
if (url) {
preview.innerHTML = `
<div style="position: relative; display: inline-block;">
<img src="${url}" style="max-width: 200px; max-height: 150px; border-radius: 8px; border: 2px solid #e0e0e0;" />
<button type="button" onclick="removeFeaturedImage()" style="position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-size: 14px;">&times;</button>
</div>
`;
} else {
preview.innerHTML = "";
}
}
function removeFeaturedImage() {
document.getElementById("postFeaturedImage").value = "";
updateFeaturedImagePreview("");
showToast("Featured image removed", "info");
}
async function loadPosts() {
try {
const response = await fetch("/api/admin/blog", { credentials: "include" });
const data = await response.json();
console.log("Blog API Response:", data);
if (data.success) {
postsData = data.posts;
console.log("Loaded posts:", postsData);
renderPosts(postsData);
} else {
console.error("API returned success=false:", data);
const tbody = document.getElementById("postsTableBody");
tbody.innerHTML = `
<tr><td colspan="7" class="text-center p-4 text-danger">
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
<p class="mt-3">Failed to load posts: ${
data.message || "Unknown error"
}</p>
</td></tr>`;
}
} catch (error) {
console.error("Failed to load posts:", error);
const tbody = document.getElementById("postsTableBody");
tbody.innerHTML = `
<tr><td colspan="7" class="text-center p-4 text-danger">
<i class="bi bi-exclamation-triangle" style="font-size: 3rem;"></i>
<p class="mt-3">Error loading posts. Please refresh the page.</p>
</td></tr>`;
}
}
@@ -55,22 +266,24 @@ function renderPosts(posts) {
.map(
(p) => `
<tr>
<td>${p.id}</td>
<td>${escapeHtml(String(p.id))}</td>
<td><strong>${escapeHtml(p.title)}</strong></td>
<td><code>${escapeHtml(p.slug)}</code></td>
<td>${escapeHtml((p.excerpt || "").substring(0, 40))}...</td>
<td><span class="badge ${
p.ispublished ? "badge-success" : "badge-warning"
p.ispublished ? "bg-success text-white" : "bg-warning text-dark"
}">
${p.ispublished ? "Published" : "Draft"}</span></td>
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPost(${p.id})">
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
String(p.id)
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePost(${
p.id
}, '${escapeHtml(p.title)}')">
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
String(p.id)
)}', '${escapeHtml(p.title).replace(/'/g, "&#39;")}')">
<i class="bi bi-trash"></i>
</button>
</td>
@@ -94,6 +307,12 @@ function showCreatePost() {
document.getElementById("postForm").reset();
document.getElementById("postId").value = "";
document.getElementById("postPublished").checked = false;
document.getElementById("postFeaturedImage").value = "";
updateFeaturedImagePreview("");
if (quillEditor) {
quillEditor.setContents([]);
}
resetModalSize();
postModal.show();
}
@@ -110,33 +329,49 @@ async function editPost(id) {
document.getElementById("postTitle").value = post.title;
document.getElementById("postSlug").value = post.slug;
document.getElementById("postExcerpt").value = post.excerpt || "";
document.getElementById("postContent").value = post.content || "";
// Set Quill content
if (quillEditor) {
quillEditor.root.innerHTML = post.content || "";
}
// Set featured image
const featuredImage = post.featuredimage || post.imageurl || "";
document.getElementById("postFeaturedImage").value = featuredImage;
updateFeaturedImagePreview(featuredImage);
document.getElementById("postMetaTitle").value = post.metatitle || "";
document.getElementById("postMetaDescription").value =
post.metadescription || "";
document.getElementById("postPublished").checked = post.ispublished;
resetModalSize();
postModal.show();
}
} catch (error) {
console.error("Failed to load post:", error);
showError("Failed to load post details");
showToast("Failed to load post details", "error");
}
}
async function savePost() {
const id = document.getElementById("postId").value;
// Get content from Quill editor
const content = quillEditor ? quillEditor.root.innerHTML : "";
const formData = {
title: document.getElementById("postTitle").value,
slug: document.getElementById("postSlug").value,
excerpt: document.getElementById("postExcerpt").value,
content: document.getElementById("postContent").value,
content: content,
featuredimage: document.getElementById("postFeaturedImage").value,
metatitle: document.getElementById("postMetaTitle").value,
metadescription: document.getElementById("postMetaDescription").value,
ispublished: document.getElementById("postPublished").checked,
};
if (!formData.title || !formData.slug || !formData.content) {
showError("Please fill in all required fields");
showToast("Please fill in all required fields", "error");
return;
}
@@ -152,17 +387,18 @@ async function savePost() {
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Post updated successfully" : "Post created successfully"
showToast(
id ? "Post updated successfully" : "Post created successfully",
"success"
);
postModal.hide();
loadPosts();
} else {
showError(data.message || "Failed to save post");
showToast(data.message || "Failed to save post", "error");
}
} catch (error) {
console.error("Failed to save post:", error);
showError("Failed to save post");
showToast("Failed to save post", "error");
}
}
@@ -175,17 +411,53 @@ async function deletePost(id, title) {
});
const data = await response.json();
if (data.success) {
showSuccess("Post deleted successfully");
showToast("Post deleted successfully", "success");
loadPosts();
} else {
showError(data.message || "Failed to delete post");
showToast(data.message || "Failed to delete post", "error");
}
} catch (error) {
console.error("Failed to delete post:", error);
showError("Failed to delete post");
showToast("Failed to delete post", "error");
}
}
function showToast(message, type = "info") {
const toastContainer =
document.getElementById("toastContainer") || createToastContainer();
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
const icons = {
success: "check-circle-fill",
error: "exclamation-triangle-fill",
warning: "exclamation-circle-fill",
info: "info-circle-fill",
};
toast.innerHTML = `
<i class="bi bi-${icons[type] || icons.info}"></i>
<span>${message}</span>
`;
toastContainer.appendChild(toast);
setTimeout(() => toast.classList.add("show"), 10);
setTimeout(() => {
toast.classList.remove("show");
setTimeout(() => toast.remove(), 300);
}, 3000);
}
function createToastContainer() {
const container = document.createElement("div");
container.id = "toastContainer";
container.style.cssText =
"position: fixed; top: 80px; right: 20px; z-index: 9999;";
document.body.appendChild(container);
return container;
}
function slugify(text) {
return text
.toLowerCase()
@@ -194,18 +466,6 @@ function slugify(text) {
.replace(/^-+|-+$/g, "");
}
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) window.location.href = "/admin/login.html";
} catch (error) {
console.error("Logout failed:", error);
}
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
@@ -224,10 +484,3 @@ function formatDate(dateString) {
day: "numeric",
});
}
function showSuccess(message) {
alert(message);
}
function showError(message) {
alert("Error: " + message);
}

View File

@@ -1,15 +1,91 @@
// Homepage Editor JavaScript
let homepageData = {};
let quillEditors = {};
let currentMediaPicker = null;
// Initialize Quill editors
function initializeQuillEditors() {
// Check if Quill is loaded
if (typeof Quill === "undefined") {
console.error("Quill.js is not loaded!");
alert("Text editor failed to load. Please refresh the page.");
return;
}
const toolbarOptions = [
["bold", "italic", "underline", "strike"],
["blockquote", "code-block"],
[{ header: 1 }, { header: 2 }],
[{ list: "ordered" }, { list: "bullet" }],
[{ script: "sub" }, { script: "super" }],
[{ indent: "-1" }, { indent: "+1" }],
[{ direction: "rtl" }],
[{ size: ["small", false, "large", "huge"] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
["link"],
["clean"],
];
try {
// Initialize Quill for each description field
quillEditors.hero = new Quill("#heroDescription", {
theme: "snow",
modules: { toolbar: toolbarOptions },
placeholder: "Enter hero section description...",
});
quillEditors.promotion = new Quill("#promotionDescription", {
theme: "snow",
modules: { toolbar: toolbarOptions },
placeholder: "Enter promotion description...",
});
quillEditors.portfolio = new Quill("#portfolioDescription", {
theme: "snow",
modules: { toolbar: toolbarOptions },
placeholder: "Enter portfolio description...",
});
console.log("Quill editors initialized successfully");
} catch (error) {
console.error("Error initializing Quill editors:", error);
alert(
"Failed to initialize text editors. Please check the console for errors."
);
}
}
document.addEventListener("DOMContentLoaded", function () {
initializeQuillEditors();
checkAuth().then((authenticated) => {
if (authenticated) {
loadHomepageSettings();
setupMediaLibraryListener();
}
});
});
// Setup media library selection listener
function setupMediaLibraryListener() {
window.addEventListener("message", function (event) {
// Security: verify origin if needed
if (
event.data &&
event.data.type === "mediaSelected" &&
currentMediaPicker
) {
const { section, field } = currentMediaPicker;
handleMediaSelection(section, field, event.data.media);
currentMediaPicker = null;
}
});
}
async function loadHomepageSettings() {
try {
const response = await fetch("/api/admin/homepage/settings", {
@@ -18,14 +94,58 @@ async function loadHomepageSettings() {
const data = await response.json();
if (data.success) {
homepageData = data.settings || {};
// If no data exists, load defaults from the frontend
if (Object.keys(homepageData).length === 0) {
console.log("No homepage data found, loading defaults from frontend");
await loadDefaultsFromFrontend();
}
populateFields();
}
} catch (error) {
console.error("Failed to load homepage settings:", error);
// Load defaults if API fails
await loadDefaultsFromFrontend();
populateFields();
}
}
// Load default content from the current homepage
async function loadDefaultsFromFrontend() {
homepageData = {
hero: {
enabled: true,
headline: "Welcome to Sky Art Shop",
subheading: "Your destination for creative stationery and supplies",
description:
"<p>Discover our curated collection of scrapbooking, journaling, cardmaking, and collaging supplies. Express your creativity and bring your artistic vision to life.</p>",
ctaText: "Shop Now",
ctaLink: "/shop.html",
backgroundUrl: "",
layout: "text-left",
},
promotion: {
enabled: true,
title: "Get Inspired",
description:
"<p>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.</p><p>Explore our collection of washi tapes, stickers, stamps, and more. Each item is carefully selected to help you create something beautiful and meaningful.</p>",
imageUrl: "",
imagePosition: "left",
textAlignment: "left",
},
portfolio: {
enabled: true,
title: "Featured Products",
description: "<p>Discover our most popular items</p>",
count: 6,
},
};
}
function populateFields() {
console.log("Populating fields with data:", homepageData);
// Hero Section
if (homepageData.hero) {
document.getElementById("heroEnabled").checked =
@@ -34,12 +154,32 @@ function populateFields() {
homepageData.hero.headline || "";
document.getElementById("heroSubheading").value =
homepageData.hero.subheading || "";
document.getElementById("heroDescription").value =
homepageData.hero.description || "";
if (homepageData.hero.description) {
quillEditors.hero.root.innerHTML = homepageData.hero.description;
}
document.getElementById("heroCtaText").value =
homepageData.hero.ctaText || "";
document.getElementById("heroCtaLink").value =
homepageData.hero.ctaLink || "";
if (homepageData.hero.backgroundUrl) {
document.getElementById("heroBackgroundUrl").value =
homepageData.hero.backgroundUrl;
displayMediaPreview(
"hero",
"background",
homepageData.hero.backgroundUrl
);
}
if (homepageData.hero.layout) {
const heroSection = document.getElementById("heroSection");
heroSection.setAttribute("data-layout", homepageData.hero.layout);
setActiveButton(`heroSection`, `layout-${homepageData.hero.layout}`);
}
toggleSection("hero");
}
@@ -49,8 +189,46 @@ function populateFields() {
homepageData.promotion.enabled !== false;
document.getElementById("promotionTitle").value =
homepageData.promotion.title || "";
document.getElementById("promotionDescription").value =
homepageData.promotion.description || "";
if (homepageData.promotion.description) {
quillEditors.promotion.root.innerHTML =
homepageData.promotion.description;
}
if (homepageData.promotion.imageUrl) {
document.getElementById("promotionImageUrl").value =
homepageData.promotion.imageUrl;
displayMediaPreview(
"promotion",
"image",
homepageData.promotion.imageUrl
);
}
if (homepageData.promotion.imagePosition) {
const promotionSection = document.getElementById("promotionSection");
promotionSection.setAttribute(
"data-image-position",
homepageData.promotion.imagePosition
);
setActiveButton(
`promotionSection`,
`position-${homepageData.promotion.imagePosition}`
);
}
if (homepageData.promotion.textAlignment) {
const promotionSection = document.getElementById("promotionSection");
promotionSection.setAttribute(
"data-text-alignment",
homepageData.promotion.textAlignment
);
setActiveButton(
`promotionSection`,
`align-${homepageData.promotion.textAlignment}`
);
}
toggleSection("promotion");
}
@@ -60,12 +238,33 @@ function populateFields() {
homepageData.portfolio.enabled !== false;
document.getElementById("portfolioTitle").value =
homepageData.portfolio.title || "";
document.getElementById("portfolioDescription").value =
homepageData.portfolio.description || "";
if (homepageData.portfolio.description) {
quillEditors.portfolio.root.innerHTML =
homepageData.portfolio.description;
}
document.getElementById("portfolioCount").value =
homepageData.portfolio.count || 6;
toggleSection("portfolio");
}
// Show success message
showSuccess(
"Homepage content loaded! You can now edit and preview your changes."
);
}
function setActiveButton(sectionId, className) {
const section = document.getElementById(sectionId);
if (section) {
const buttons = section.querySelectorAll(".alignment-btn");
buttons.forEach((btn) => {
if (btn.classList.contains(className)) {
btn.classList.add("active");
}
});
}
}
function toggleSection(sectionName) {
@@ -75,80 +274,258 @@ function toggleSection(sectionName) {
if (enabled) {
section.classList.remove("disabled");
content
.querySelectorAll("input, textarea, button, select")
.forEach((el) => {
el.disabled = false;
});
content.querySelectorAll("input, button, select").forEach((el) => {
el.disabled = false;
});
// Enable Quill editor
if (quillEditors[sectionName]) {
quillEditors[sectionName].enable();
}
} else {
section.classList.add("disabled");
content
.querySelectorAll("input, textarea, button, select")
.forEach((el) => {
el.disabled = true;
});
content.querySelectorAll("input, button, select").forEach((el) => {
el.disabled = true;
});
// Disable Quill editor
if (quillEditors[sectionName]) {
quillEditors[sectionName].disable();
}
}
}
function previewImage(sectionName) {
const fileInput =
document.getElementById(`${sectionName}Background`) ||
document.getElementById(`${sectionName}Image`);
const preview = document.getElementById(`${sectionName}Preview`);
// Open media library in a modal
function openMediaLibrary(section, field) {
currentMediaPicker = { section, field };
if (fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function (e) {
preview.classList.remove("empty");
preview.innerHTML = `<img src="${e.target.result}" alt="Preview" />`;
};
reader.readAsDataURL(fileInput.files[0]);
// Create modal backdrop
const backdrop = document.createElement("div");
backdrop.id = "mediaLibraryBackdrop";
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
`;
// Create modal container
const modal = document.createElement("div");
modal.id = "mediaLibraryModal";
modal.style.cssText = `
position: relative;
width: 90%;
max-width: 1200px;
height: 85vh;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
`;
// Create close button
const closeBtn = document.createElement("button");
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
z-index: 10000;
background: #dc3545;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.onclick = closeMediaLibrary;
// Create iframe
const iframe = document.createElement("iframe");
iframe.id = "mediaLibraryFrame";
iframe.src = "/admin/media-library.html?selectMode=true";
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
// Setup iframe message listener
iframe.onload = function () {
iframe.contentWindow.postMessage(
{
type: "initSelectMode",
section: section,
field: field,
},
"*"
);
};
modal.appendChild(closeBtn);
modal.appendChild(iframe);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.onclick = function (e) {
if (e.target === backdrop) {
closeMediaLibrary();
}
};
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
}
currentMediaPicker = null;
}
function handleMediaSelection(section, field, media) {
closeMediaLibrary();
const urlField = document.getElementById(
`${section}${field === "background" ? "Background" : "Image"}Url`
);
if (urlField) {
urlField.value = media.url;
}
displayMediaPreview(section, field, media.url);
showSuccess(`Media selected successfully!`);
}
function displayMediaPreview(section, field, url) {
const previewId = `${section}Preview`;
const preview = document.getElementById(previewId);
const clearBtnId = `${section}${
field === "background" ? "Background" : "Image"
}Clear`;
const clearBtn = document.getElementById(clearBtnId);
if (preview) {
preview.classList.remove("empty");
// Check if it's a video
const isVideo = url.match(/\.(mp4|webm|ogg)$/i);
if (isVideo) {
preview.innerHTML = `<video src="${url}" style="max-width: 100%; max-height: 100%;" controls></video>`;
} else {
preview.innerHTML = `<img src="${url}" alt="Preview" />`;
}
}
if (clearBtn) {
clearBtn.style.display = "inline-block";
}
}
function clearMedia(section, field) {
const urlField = document.getElementById(
`${section}${field === "background" ? "Background" : "Image"}Url`
);
if (urlField) {
urlField.value = "";
}
const previewId = `${section}Preview`;
const preview = document.getElementById(previewId);
if (preview) {
preview.classList.add("empty");
preview.innerHTML = '<i class="bi bi-image" style="font-size: 3rem"></i>';
}
const clearBtnId = `${section}${
field === "background" ? "Background" : "Image"
}Clear`;
const clearBtn = document.getElementById(clearBtnId);
if (clearBtn) {
clearBtn.style.display = "none";
}
showSuccess("Media cleared");
}
function setLayout(sectionName, layout) {
const buttons = document.querySelectorAll(
`#${sectionName}Section .alignment-btn`
);
const section = document.getElementById(`${sectionName}Section`);
const buttons = section.querySelectorAll(".alignment-btn");
buttons.forEach((btn) => btn.classList.remove("active"));
event.target.closest(".alignment-btn").classList.add("active");
// Store in a data attribute
section.setAttribute(`data-layout`, layout);
}
function setImagePosition(sectionName, position) {
const section = document.getElementById(`${sectionName}Section`);
const buttons = event.target
.closest(".alignment-selector")
.querySelectorAll(".alignment-btn");
buttons.forEach((btn) => btn.classList.remove("active"));
event.target.closest(".alignment-btn").classList.add("active");
section.setAttribute(`data-image-position`, position);
}
function setTextAlignment(sectionName, alignment) {
const section = document.getElementById(`${sectionName}Section`);
const buttons = event.target
.closest(".alignment-selector")
.querySelectorAll(".alignment-btn");
buttons.forEach((btn) => btn.classList.remove("active"));
event.target.closest(".alignment-btn").classList.add("active");
section.setAttribute(`data-text-alignment`, alignment);
}
async function saveHomepage() {
// Get hero layout
const heroSection = document.getElementById("heroSection");
const heroLayout = heroSection.getAttribute("data-layout") || "text-left";
// Get promotion layout settings
const promotionSection = document.getElementById("promotionSection");
const promotionImagePosition =
promotionSection.getAttribute("data-image-position") || "left";
const promotionTextAlignment =
promotionSection.getAttribute("data-text-alignment") || "left";
const settings = {
hero: {
enabled: document.getElementById("heroEnabled").checked,
headline: document.getElementById("heroHeadline").value,
subheading: document.getElementById("heroSubheading").value,
description: document.getElementById("heroDescription").value,
description: quillEditors.hero.root.innerHTML,
ctaText: document.getElementById("heroCtaText").value,
ctaLink: document.getElementById("heroCtaLink").value,
backgroundUrl: document.getElementById("heroBackgroundUrl")?.value || "",
layout: heroLayout,
},
promotion: {
enabled: document.getElementById("promotionEnabled").checked,
title: document.getElementById("promotionTitle").value,
description: document.getElementById("promotionDescription").value,
description: quillEditors.promotion.root.innerHTML,
imageUrl: document.getElementById("promotionImageUrl")?.value || "",
imagePosition: promotionImagePosition,
textAlignment: promotionTextAlignment,
},
portfolio: {
enabled: document.getElementById("portfolioEnabled").checked,
title: document.getElementById("portfolioTitle").value,
description: document.getElementById("portfolioDescription").value,
description: quillEditors.portfolio.root.innerHTML,
count: parseInt(document.getElementById("portfolioCount").value) || 6,
},
};
@@ -164,8 +541,9 @@ async function saveHomepage() {
const data = await response.json();
if (data.success) {
showSuccess(
"Homepage settings saved successfully! Changes are now live."
"Homepage settings saved successfully! Changes are now live on the frontend."
);
homepageData = settings;
} else {
showError(data.message || "Failed to save homepage settings");
}
@@ -175,22 +553,30 @@ async function saveHomepage() {
}
}
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) window.location.href = "/admin/login.html";
} catch (error) {
console.error("Logout failed:", error);
}
}
function showSuccess(message) {
alert(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 = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}
function showError(message) {
alert("Error: " + 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 = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}

File diff suppressed because it is too large Load Diff

View File

@@ -2,9 +2,23 @@
let projectsData = [];
let projectModal;
let quillEditor;
let portfolioImages = [];
let currentMediaPicker = null;
let isModalExpanded = false;
document.addEventListener("DOMContentLoaded", function () {
projectModal = new bootstrap.Modal(document.getElementById("projectModal"));
// Fix aria-hidden accessibility issue
const projectModalElement = document.getElementById("projectModal");
projectModalElement.addEventListener("hide.bs.modal", function () {
document.querySelector(".btn.btn-primary")?.focus();
});
// Initialize Quill editor
initializeQuillEditor();
checkAuth().then((authenticated) => {
if (authenticated) {
loadProjects();
@@ -17,14 +31,100 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
function resetModalSize() {
const modalDialog = document.querySelector("#projectModal .modal-dialog");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.querySelector("#btnExpandModal span");
const editor = document.getElementById("projectDescriptionEditor");
if (modalDialog && expandIcon && expandText && editor) {
modalDialog.classList.remove("modal-fullscreen");
modalDialog.classList.add("modal-xl");
expandIcon.className = "bi bi-arrows-fullscreen";
expandText.textContent = "Expand";
editor.style.height = "300px";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(300px - 42px)";
}
isModalExpanded = false;
}
}
function toggleModalSize() {
const modalDialog = document.querySelector("#projectModal .modal-dialog");
const expandIcon = document.getElementById("expandIcon");
const expandText = document.querySelector("#btnExpandModal span");
const editor = document.getElementById("projectDescriptionEditor");
if (!modalDialog || !expandIcon || !expandText || !editor) {
console.error("Modal elements not found");
return;
}
if (isModalExpanded) {
// Collapse to normal size
modalDialog.classList.remove("modal-fullscreen");
modalDialog.classList.add("modal-xl");
expandIcon.className = "bi bi-arrows-fullscreen";
expandText.textContent = "Expand";
editor.style.height = "300px";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(300px - 42px)";
}
isModalExpanded = false;
} else {
// Expand to fullscreen
modalDialog.classList.remove("modal-xl");
modalDialog.classList.add("modal-fullscreen");
expandIcon.className = "bi bi-fullscreen-exit";
expandText.textContent = "Collapse";
editor.style.height = "60vh";
const container = editor.querySelector(".ql-container");
if (container) {
container.style.height = "calc(60vh - 42px)";
}
isModalExpanded = true;
}
}
// Initialize Quill Editor
function initializeQuillEditor() {
quillEditor = new Quill("#projectDescriptionEditor", {
theme: "snow",
placeholder: "Describe your portfolio project here...",
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
[{ color: [] }, { background: [] }],
["link", "image"],
["clean"],
],
},
});
}
async function loadProjects() {
try {
const response = await fetch("/api/admin/portfolio/projects", {
credentials: "include",
cache: "no-cache", // Force fresh data
});
const data = await response.json();
if (data.success) {
projectsData = data.projects;
console.log(
"📊 Loaded projects:",
projectsData.map((p) => ({
id: p.id,
title: p.title,
isactive: p.isactive,
isactiveType: typeof p.isactive,
}))
);
renderProjects(projectsData);
}
} catch (error) {
@@ -47,28 +147,45 @@ function renderProjects(projects) {
}
tbody.innerHTML = projects
.map(
(p) => `
.map((p) => {
// Explicitly check and log the status
console.log(
`Project ${p.id}: isactive =`,
p.isactive,
`(type: ${typeof p.isactive})`
);
const isActive =
p.isactive === true || p.isactive === "true" || p.isactive === 1;
console.log(` -> Evaluated as: ${isActive ? "ACTIVE" : "INACTIVE"}`);
const statusClass = isActive
? "bg-success text-white"
: "bg-danger text-white";
const statusText = isActive ? "Active" : "Inactive";
const statusIcon = isActive ? "✓" : "✗";
return `
<tr>
<td>${p.id}</td>
<td>${escapeHtml(String(p.id))}</td>
<td><strong>${escapeHtml(p.title)}</strong></td>
<td>${escapeHtml((p.description || "").substring(0, 50))}...</td>
<td>${p.category || "-"}</td>
<td><span class="badge ${p.isactive ? "badge-success" : "badge-danger"}">
${p.isactive ? "Active" : "Inactive"}</span></td>
<td><span class="badge ${statusClass}">
${statusIcon} ${statusText}</span></td>
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editProject(${p.id})">
<button class="btn btn-sm btn-info" onclick="editProject('${escapeHtml(
String(p.id)
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteProject(${
p.id
}, '${escapeHtml(p.title)}')">
<button class="btn btn-sm btn-danger" onclick="deleteProject('${escapeHtml(
String(p.id)
)}', '${escapeHtml(p.title).replace(/'/g, "&#39;")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
)
</tr>`;
})
.join("");
}
@@ -85,6 +202,17 @@ function showCreateProject() {
document.getElementById("projectForm").reset();
document.getElementById("projectId").value = "";
document.getElementById("projectActive").checked = true;
// Clear Quill editor
if (quillEditor) {
quillEditor.setContents([]);
}
// Clear images
portfolioImages = [];
renderPortfolioImages();
resetModalSize();
projectModal.show();
}
@@ -100,10 +228,27 @@ async function editProject(id) {
"Edit Portfolio Project";
document.getElementById("projectId").value = project.id;
document.getElementById("projectTitle").value = project.title;
document.getElementById("projectDescription").value =
project.description || "";
// Set Quill editor content
if (quillEditor && project.description) {
quillEditor.root.innerHTML = project.description;
}
document.getElementById("projectCategory").value = project.category || "";
document.getElementById("projectActive").checked = project.isactive;
// Load images if available (imageurl field or parse from description)
portfolioImages = [];
if (project.imageurl) {
// If single image URL exists
portfolioImages.push({
url: project.imageurl,
filename: project.imageurl.split("/").pop(),
});
}
renderPortfolioImages();
resetModalSize();
projectModal.show();
}
} catch (error) {
@@ -114,15 +259,21 @@ async function editProject(id) {
async function saveProject() {
const id = document.getElementById("projectId").value;
// Get description from Quill editor
const description = quillEditor.root.innerHTML;
const formData = {
title: document.getElementById("projectTitle").value,
description: document.getElementById("projectDescription").value,
description: description,
category: document.getElementById("projectCategory").value,
isactive: document.getElementById("projectActive").checked,
imageurl: portfolioImages.length > 0 ? portfolioImages[0].url : null,
images: portfolioImages.map((img) => img.url),
};
if (!formData.title || !formData.description) {
showError("Please fill in all required fields");
showError("Please fill in all required fields (Title and Description)");
return;
}
@@ -141,7 +292,9 @@ async function saveProject() {
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Project updated successfully" : "Project created successfully"
id
? "Project updated successfully! 🎉"
: "Project created successfully! 🎉"
);
projectModal.hide();
loadProjects();
@@ -174,18 +327,6 @@ async function deleteProject(id, name) {
}
}
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) window.location.href = "/admin/login.html";
} catch (error) {
console.error("Logout failed:", error);
}
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
@@ -205,9 +346,213 @@ function formatDate(dateString) {
});
}
// Render portfolio images gallery
function renderPortfolioImages() {
const gallery = document.getElementById("portfolioImagesGallery");
if (!gallery) return;
if (portfolioImages.length === 0) {
gallery.innerHTML = `
<div class="text-muted small">
No images added yet. Click above to add images.
</div>
`;
return;
}
gallery.innerHTML = portfolioImages
.map(
(img, index) => `
<div class="position-relative" style="width: 100px; height: 100px;">
<img
src="${img.url}"
alt="${img.filename}"
class="img-thumbnail w-100 h-100 object-fit-cover"
title="${img.filename}"
/>
<button
type="button"
class="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 p-1"
onclick="removePortfolioImage(${index})"
style="line-height: 1; width: 24px; height: 24px; font-size: 12px;"
>
<i class="bi bi-x"></i>
</button>
</div>
`
)
.join("");
}
// Remove portfolio image
function removePortfolioImage(index) {
portfolioImages.splice(index, 1);
renderPortfolioImages();
}
// Media Library Integration
function openMediaLibrary(purpose) {
currentMediaPicker = { purpose };
// Create backdrop
const backdrop = document.createElement("div");
backdrop.id = "mediaLibraryBackdrop";
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
`;
// Create modal
const modal = document.createElement("div");
modal.style.cssText = `
width: 90%;
height: 90%;
background: white;
border-radius: 12px;
overflow: hidden;
position: relative;
`;
// Create close button
const closeBtn = document.createElement("button");
closeBtn.innerHTML = "×";
closeBtn.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
z-index: 10000;
background: #dc3545;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.onclick = closeMediaLibrary;
// Create iframe
const iframe = document.createElement("iframe");
iframe.id = "mediaLibraryFrame";
iframe.src = "/admin/media-library.html?selectMode=true";
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
modal.appendChild(closeBtn);
modal.appendChild(iframe);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.onclick = function (e) {
if (e.target === backdrop) {
closeMediaLibrary();
}
};
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
}
currentMediaPicker = null;
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
if (currentMediaPicker.purpose === "portfolioImages") {
// Handle multiple images
const mediaArray = Array.isArray(media) ? media : [media];
// Add all selected images to portfolio images array
mediaArray.forEach((item) => {
// Check if image already exists
if (!portfolioImages.find((img) => img.url === item.url)) {
portfolioImages.push({
url: item.url,
filename: item.filename || item.url.split("/").pop(),
});
}
});
renderPortfolioImages();
showSuccess(`${mediaArray.length} image(s) added to portfolio gallery`);
}
closeMediaLibrary();
}
// Toast Notification System
function showSuccess(message) {
alert(message);
showToast(message, "success");
}
function showError(message) {
alert("Error: " + message);
showToast(message, "error");
}
function showToast(message, type = "info") {
// Create toast container if it doesn't exist
let container = document.getElementById("toastContainer");
if (!container) {
container = document.createElement("div");
container.id = "toastContainer";
container.className = "toast-container";
document.body.appendChild(container);
}
// Create toast element
const toast = document.createElement("div");
toast.className = `toast toast-${type} toast-show`;
// Set icon based on type
let icon = "";
if (type === "success") {
icon = '<i class="bi bi-check-circle-fill"></i>';
} else if (type === "error") {
icon = '<i class="bi bi-exclamation-circle-fill"></i>';
} else if (type === "info") {
icon = '<i class="bi bi-info-circle-fill"></i>';
} else if (type === "warning") {
icon = '<i class="bi bi-exclamation-triangle-fill"></i>';
}
toast.innerHTML = `
<div class="toast-icon">${icon}</div>
<div class="toast-message">${escapeHtml(message)}</div>
<button class="toast-close" onclick="this.parentElement.remove()">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(toast);
// Auto remove after 4 seconds
setTimeout(() => {
toast.classList.remove("toast-show");
toast.classList.add("toast-hide");
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}, 4000);
}

View File

@@ -2,11 +2,55 @@
let productsData = [];
let productModal;
let quillEditor;
let imageVariants = [];
let productImages = []; // Stores general product images
let currentMediaPicker = null; // Tracks which field is selecting media
// Initialize on page load
document.addEventListener("DOMContentLoaded", function () {
// Initialize Bootstrap modal
productModal = new bootstrap.Modal(document.getElementById("productModal"));
const productModalElement = document.getElementById("productModal");
productModal = new bootstrap.Modal(productModalElement);
// Fix aria-hidden accessibility issue: move focus before modal hides
productModalElement.addEventListener("hide.bs.modal", function () {
// Move focus to a safe element outside the modal before it gets aria-hidden
document.getElementById("btnAddProduct")?.focus();
});
// Initialize Quill editor
initializeQuillEditor();
// Add event listeners for buttons
const btnAddProduct = document.getElementById("btnAddProduct");
if (btnAddProduct) {
btnAddProduct.addEventListener("click", showCreateProduct);
}
// Add event listener for search input
const searchInput = document.getElementById("searchInput");
if (searchInput) {
searchInput.addEventListener("input", filterProducts);
}
// Add event listener for save product button
const btnSaveProduct = document.getElementById("btnSaveProduct");
if (btnSaveProduct) {
btnSaveProduct.addEventListener("click", saveProduct);
}
// Add event listener for logout button
const btnLogout = document.getElementById("btnLogout");
if (btnLogout) {
btnLogout.addEventListener("click", logout);
}
// Add event listener for add image variant button
const btnAddImageVariant = document.getElementById("btnAddImageVariant");
if (btnAddImageVariant) {
btnAddImageVariant.addEventListener("click", addImageVariantField);
}
// Check authentication (from auth.js)
checkAuth().then((authenticated) => {
@@ -22,6 +66,24 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
// Initialize Quill Editor
function initializeQuillEditor() {
quillEditor = new Quill("#productDescriptionEditor", {
theme: "snow",
placeholder: "Write your product description here...",
modules: {
toolbar: [
[{ header: [1, 2, 3, false] }],
["bold", "italic", "underline", "strike"],
[{ list: "ordered" }, { list: "bullet" }],
[{ color: [] }, { background: [] }],
["link", "image"],
["clean"],
],
},
});
}
// Load all products
async function loadProducts() {
try {
@@ -50,12 +112,19 @@ function renderProducts(products) {
<td colspan="8" class="text-center p-4">
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-3 text-muted">No products found</p>
<button class="btn btn-primary" onclick="showCreateProduct()">
<button class="btn btn-primary" id="btnAddFirstProduct">
<i class="bi bi-plus-circle"></i> Add Your First Product
</button>
</td>
</tr>
`;
// Add event listener to the "Add First Product" button
setTimeout(() => {
const btn = document.getElementById("btnAddFirstProduct");
if (btn) {
btn.addEventListener("click", showCreateProduct);
}
}, 0);
return;
}
@@ -83,14 +152,14 @@ function renderProducts(products) {
</td>
<td>${formatDate(product.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editProduct(${
<button class="btn btn-sm btn-info" data-action="edit" data-id="${
product.id
})">
}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteProduct(${
<button class="btn btn-sm btn-danger" data-action="delete" data-id="${
product.id
}, '${escapeHtml(product.name)}')">
}" data-name="${escapeHtml(product.name)}">
<i class="bi bi-trash"></i>
</button>
</td>
@@ -98,6 +167,17 @@ function renderProducts(products) {
`
)
.join("");
// Add event listeners to edit and delete buttons
tbody.querySelectorAll('button[data-action="edit"]').forEach((btn) => {
btn.addEventListener("click", () => editProduct(btn.dataset.id));
});
tbody.querySelectorAll('button[data-action="delete"]').forEach((btn) => {
btn.addEventListener("click", () =>
deleteProduct(btn.dataset.id, btn.dataset.name)
);
});
}
// Filter products
@@ -115,9 +195,333 @@ function showCreateProduct() {
document.getElementById("productForm").reset();
document.getElementById("productId").value = "";
document.getElementById("productActive").checked = true;
// Clear Quill editor
if (quillEditor) {
quillEditor.setContents([]);
}
// Clear arrays
productImages = [];
imageVariants = [];
renderProductImages();
renderImageVariants();
productModal.show();
}
// Add image variant field
function addImageVariantField() {
const variant = {
id: Date.now().toString(),
image_url: "",
color_variant: "",
color_code: "#000000",
alt_text: "",
variant_price: null,
variant_stock: 0,
is_primary: imageVariants.length === 0,
};
imageVariants.push(variant);
renderImageVariants();
}
// Render product images gallery
function renderProductImages() {
const gallery = document.getElementById("productImagesGallery");
if (!gallery) return;
if (productImages.length === 0) {
gallery.innerHTML = `
<div class="text-muted small">
No images added yet. Click above to add images.
</div>
`;
return;
}
gallery.innerHTML = productImages
.map(
(img, index) => `
<div class="position-relative" style="width: 100px; height: 100px;">
<img
src="${img.url}"
alt="${img.filename}"
class="img-thumbnail w-100 h-100 object-fit-cover"
title="${img.filename}"
/>
<button
type="button"
class="btn btn-sm btn-danger position-absolute top-0 end-0 m-1 p-1"
onclick="removeProductImage(${index})"
style="line-height: 1; width: 24px; height: 24px; font-size: 12px;"
>
<i class="bi bi-x"></i>
</button>
</div>
`
)
.join("");
}
// Remove product image
function removeProductImage(index) {
productImages.splice(index, 1);
renderProductImages();
}
// Render image variant fields
function renderImageVariants() {
const container = document.getElementById("imageVariantsContainer");
if (imageVariants.length === 0) {
container.innerHTML = `
<div class="text-center text-muted p-3">
<i class="bi bi-palette" style="font-size: 2rem;"></i>
<p class="mb-0 mt-2">No color variants added yet. Add product images above first, then create color variants here.</p>
</div>
`;
return;
}
container.innerHTML = imageVariants
.map((variant, index) => {
// Generate image picker HTML with thumbnails
const imagePickerHTML =
productImages.length > 0
? `
<div class="image-picker-grid" data-variant-id="${variant.id}">
${productImages
.map((img, idx) => {
const isSelected = img.url === variant.image_url;
return `
<div class="image-picker-item ${isSelected ? "selected" : ""}"
data-image-url="${img.url}"
data-variant-id="${variant.id}"
title="${img.filename || "Image " + (idx + 1)}">
<img src="${img.url}" alt="${
img.filename || "Image " + (idx + 1)
}">
<div class="image-picker-overlay">
<i class="bi bi-check-circle-fill"></i>
</div>
<small class="image-picker-label">${
img.filename || "Image " + (idx + 1)
}</small>
</div>
`;
})
.join("")}
</div>
`
: '<small class="text-danger">Add product images first</small>';
return `
<div class="image-variant-item mb-3 p-3 border rounded" data-variant-id="${
variant.id
}" style="background: #f8f9fa;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">
<i class="bi bi-palette"></i> Color Variant ${index + 1}
${
variant.is_primary
? '<span class="badge bg-primary ms-2">Primary</span>'
: ""
}
</h6>
<button type="button" class="btn btn-sm btn-danger" data-action="remove" data-variant-id="${
variant.id
}">
<i class="bi bi-trash"></i> Remove
</button>
</div>
<div class="row">
<!-- Image Selector with Visual Preview -->
<div class="col-md-12 mb-3">
<label class="form-label small fw-bold">Select Image *</label>
${imagePickerHTML}
</div>
<!-- Color Name -->
<div class="col-md-6 mb-3">
<label class="form-label small fw-bold">Color Name *</label>
<input
type="text"
class="form-control form-control-sm"
placeholder="e.g., Ruby Red, Ocean Blue"
value="${variant.color_variant || ""}"
data-field="color_variant"
data-variant-id="${variant.id}"
/>
</div>
</div>
<div class="row">
<!-- Color Picker -->
<div class="col-md-3 mb-3">
<label class="form-label small fw-bold">Color Code</label>
<div class="d-flex gap-2 align-items-center">
<input
type="color"
class="form-control form-control-color"
value="${variant.color_code || "#000000"}"
data-field="color_code"
data-variant-id="${variant.id}"
style="width: 60px; height: 38px;"
/>
<input
type="text"
class="form-control form-control-sm"
placeholder="#000000"
value="${variant.color_code || ""}"
data-field="color_code_text"
data-variant-id="${variant.id}"
maxlength="7"
style="font-family: monospace;"
/>
</div>
</div>
<!-- Variant Price -->
<div class="col-md-3 mb-3">
<label class="form-label small fw-bold">Variant Price</label>
<div class="input-group input-group-sm">
<span class="input-group-text">$</span>
<input
type="number"
class="form-control form-control-sm"
placeholder="Optional"
value="${variant.variant_price || ""}"
data-field="variant_price"
data-variant-id="${variant.id}"
step="0.01"
min="0"
/>
</div>
<small class="text-muted">Leave empty to use base price</small>
</div>
<!-- Variant Stock -->
<div class="col-md-3 mb-3">
<label class="form-label small fw-bold">Stock Quantity *</label>
<input
type="number"
class="form-control form-control-sm"
placeholder="0"
value="${variant.variant_stock || 0}"
data-field="variant_stock"
data-variant-id="${variant.id}"
min="0"
/>
</div>
<!-- Primary Checkbox -->
<div class="col-md-3 mb-3">
<label class="form-label small fw-bold">Primary Image</label>
<div class="form-check">
<input
type="radio"
class="form-check-input"
name="primaryVariant"
${variant.is_primary ? "checked" : ""}
data-field="is_primary"
data-variant-id="${variant.id}"
/>
<label class="form-check-label small">
Set as primary
</label>
</div>
</div>
</div>
<!-- Alt Text -->
<div class="row">
<div class="col-12 mb-2">
<label class="form-label small">Alt Text (for accessibility)</label>
<input
type="text"
class="form-control form-control-sm"
placeholder="Description of the image"
value="${variant.alt_text || ""}"
data-field="alt_text"
data-variant-id="${variant.id}"
/>
</div>
</div>
</div>
`;
})
.join("");
// Add event listeners for remove buttons
container.querySelectorAll('[data-action="remove"]').forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = e.currentTarget.dataset.variantId;
imageVariants = imageVariants.filter((v) => v.id !== id);
renderImageVariants();
});
});
// Add event listeners for image picker items
container.querySelectorAll(".image-picker-item").forEach((item) => {
item.addEventListener("click", (e) => {
const variantId = e.currentTarget.dataset.variantId;
const imageUrl = e.currentTarget.dataset.imageUrl;
const variant = imageVariants.find((v) => v.id === variantId);
if (variant) {
variant.image_url = imageUrl;
// Update visual selection
const pickerGrid = e.currentTarget.closest(".image-picker-grid");
pickerGrid
.querySelectorAll(".image-picker-item")
.forEach((i) => i.classList.remove("selected"));
e.currentTarget.classList.add("selected");
}
});
});
// Add event listeners for input changes
container.querySelectorAll("[data-variant-id]").forEach((input) => {
input.addEventListener("input", (e) => {
const id = e.target.dataset.variantId;
const field = e.target.dataset.field;
const variant = imageVariants.find((v) => v.id === id);
if (variant) {
if (field === "color_code_text") {
// Update both color picker and text
variant.color_code = e.target.value;
const colorPicker = container.querySelector(
`input[type="color"][data-variant-id="${id}"]`
);
if (colorPicker && /^#[0-9A-F]{6}$/i.test(e.target.value)) {
colorPicker.value = e.target.value;
}
} else if (field === "color_code") {
// Update both color picker and text
variant.color_code = e.target.value;
const colorText = container.querySelector(
`input[data-field="color_code_text"][data-variant-id="${id}"]`
);
if (colorText) {
colorText.value = e.target.value;
}
} else if (field === "is_primary") {
// Set all to false, then this one to true
imageVariants.forEach((v) => (v.is_primary = false));
variant.is_primary = true;
} else {
variant[field] = e.target.value;
}
}
});
});
}
// Edit product
async function editProduct(id) {
try {
@@ -132,16 +536,48 @@ async function editProduct(id) {
document.getElementById("modalTitle").textContent = "Edit Product";
document.getElementById("productId").value = product.id;
document.getElementById("productName").value = product.name;
document.getElementById("productDescription").value =
product.description || "";
document.getElementById("productShortDescription").value =
product.shortdescription || "";
// Set Quill editor content
if (quillEditor && product.description) {
quillEditor.root.innerHTML = product.description;
}
document.getElementById("productPrice").value = product.price;
document.getElementById("productStock").value =
product.stockquantity || 0;
document.getElementById("productSKU").value = product.sku || "";
document.getElementById("productCategory").value = product.category || "";
document.getElementById("productMaterial").value = product.material || "";
document.getElementById("productDimensions").value =
product.dimensions || "";
document.getElementById("productWeight").value = product.weight || "";
document.getElementById("productActive").checked = product.isactive;
document.getElementById("productFeatured").checked =
product.isfeatured || false;
document.getElementById("productBestSeller").checked =
product.isbestseller || false;
// Load image variants and extract unique product images
imageVariants = product.images || [];
// Build productImages array from unique image URLs in variants
const uniqueImages = {};
imageVariants.forEach((variant) => {
if (variant.image_url && !uniqueImages[variant.image_url]) {
uniqueImages[variant.image_url] = {
url: variant.image_url,
filename: variant.image_url.split("/").pop(),
alt_text: variant.alt_text || "",
};
}
});
productImages = Object.values(uniqueImages);
renderProductImages();
renderImageVariants();
productModal.show();
}
} catch (error) {
@@ -153,19 +589,52 @@ async function editProduct(id) {
// Save product
async function saveProduct() {
const id = document.getElementById("productId").value;
// Get description from Quill editor
const description = quillEditor.root.innerHTML;
// Prepare images array for backend with all new fields
const images = imageVariants.map((variant, index) => ({
image_url: variant.image_url,
color_variant: variant.color_variant || null,
color_code: variant.color_code || null,
alt_text: variant.alt_text || document.getElementById("productName").value,
display_order: index,
is_primary: variant.is_primary || false,
variant_price: variant.variant_price
? parseFloat(variant.variant_price)
: null,
variant_stock: parseInt(variant.variant_stock) || 0,
}));
const formData = {
name: document.getElementById("productName").value,
description: document.getElementById("productDescription").value,
shortdescription: document.getElementById("productShortDescription").value,
description: description,
price: parseFloat(document.getElementById("productPrice").value),
stockquantity: parseInt(document.getElementById("productStock").value) || 0,
sku: document.getElementById("productSKU").value,
category: document.getElementById("productCategory").value,
material: document.getElementById("productMaterial").value,
dimensions: document.getElementById("productDimensions").value,
weight: parseFloat(document.getElementById("productWeight").value) || null,
isactive: document.getElementById("productActive").checked,
isfeatured: document.getElementById("productFeatured").checked,
isbestseller: document.getElementById("productBestSeller").checked,
images: images,
};
// Validation
if (!formData.name || !formData.price) {
showError("Please fill in all required fields");
showError("Please fill in all required fields (Name and Price)");
return;
}
if (
imageVariants.length > 0 &&
imageVariants.some((v) => !v.image_url || !v.color_variant)
) {
showError("All color variants must have an image and color name selected");
return;
}
@@ -185,7 +654,9 @@ async function saveProduct() {
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Product updated successfully" : "Product created successfully"
id
? "Product updated successfully! 🎉"
: "Product created successfully! 🎉"
);
productModal.hide();
loadProducts();
@@ -223,22 +694,131 @@ async function deleteProduct(id, name) {
}
}
// Logout function
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
// ===== MEDIA LIBRARY INTEGRATION =====
// Listen for media selections from media library
window.addEventListener("message", function (event) {
// Security: verify origin if needed
if (event.data.type === "mediaSelected" && currentMediaPicker) {
handleMediaSelection(event.data.media);
}
});
// Open media library modal
function openMediaLibrary(purpose) {
currentMediaPicker = { purpose }; // purpose: 'productImage' or 'variantImage'
// Create modal backdrop
const backdrop = document.createElement("div");
backdrop.id = "mediaLibraryBackdrop";
backdrop.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
`;
// Create modal container
const modal = document.createElement("div");
modal.id = "mediaLibraryModal";
modal.style.cssText = `
position: relative;
width: 90%;
max-width: 1200px;
height: 85vh;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
`;
// Create close button
const closeBtn = document.createElement("button");
closeBtn.innerHTML = '<i class="bi bi-x-lg"></i>';
closeBtn.style.cssText = `
position: absolute;
top: 15px;
right: 15px;
z-index: 10000;
background: #dc3545;
color: white;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
`;
closeBtn.onclick = closeMediaLibrary;
// Create iframe
const iframe = document.createElement("iframe");
iframe.id = "mediaLibraryFrame";
iframe.src = "/admin/media-library.html?selectMode=true";
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
`;
modal.appendChild(closeBtn);
modal.appendChild(iframe);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
// Close on backdrop click
backdrop.onclick = function (e) {
if (e.target === backdrop) {
closeMediaLibrary();
}
};
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
}
currentMediaPicker = null;
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
if (currentMediaPicker.purpose === "productImage") {
// Handle multiple images
const mediaArray = Array.isArray(media) ? media : [media];
// Add all selected images to product images array
mediaArray.forEach((item) => {
// Check if image already exists
if (!productImages.find((img) => img.url === item.url)) {
productImages.push({
url: item.url,
alt_text: item.filename || "",
filename: item.filename,
});
}
});
if (response.ok) {
window.location.href = "/admin/login.html";
}
} catch (error) {
console.error("Logout failed:", error);
renderProductImages();
showSuccess(`${mediaArray.length} image(s) added to product gallery`);
}
closeMediaLibrary();
}
// ===== UTILITY FUNCTIONS =====
// Utility functions
function escapeHtml(text) {
const map = {
@@ -261,10 +841,57 @@ function formatDate(dateString) {
}
function showSuccess(message) {
// Simple alert for now - can be replaced with toast notification
alert(message);
showToast(message, "success");
}
function showError(message) {
alert("Error: " + message);
showToast(message, "error");
}
function showToast(message, type = "info") {
// Create toast container if it doesn't exist
let container = document.getElementById("toastContainer");
if (!container) {
container = document.createElement("div");
container.id = "toastContainer";
container.className = "toast-container";
document.body.appendChild(container);
}
// Create toast element
const toast = document.createElement("div");
toast.className = `toast toast-${type} toast-show`;
// Set icon based on type
let icon = "";
if (type === "success") {
icon = '<i class="bi bi-check-circle-fill"></i>';
} else if (type === "error") {
icon = '<i class="bi bi-exclamation-circle-fill"></i>';
} else if (type === "info") {
icon = '<i class="bi bi-info-circle-fill"></i>';
} else if (type === "warning") {
icon = '<i class="bi bi-exclamation-triangle-fill"></i>';
}
toast.innerHTML = `
<div class="toast-icon">${icon}</div>
<div class="toast-message">${escapeHtml(message)}</div>
<button class="toast-close" onclick="this.parentElement.remove()">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(toast);
// Auto remove after 4 seconds
setTimeout(() => {
toast.classList.remove("toast-show");
toast.classList.add("toast-hide");
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}, 4000);
}

View File

@@ -1,8 +1,32 @@
// Settings Management JavaScript
let currentSettings = {};
let mediaLibraryModal;
let currentMediaTarget = null;
let selectedMediaUrl = null;
let allMedia = [];
document.addEventListener("DOMContentLoaded", function () {
// Initialize modal
const modalElement = document.getElementById("mediaLibraryModal");
if (modalElement) {
mediaLibraryModal = new bootstrap.Modal(modalElement);
}
// Setup media search
const searchInput = document.getElementById("mediaSearch");
if (searchInput) {
searchInput.addEventListener("input", filterMedia);
}
const typeFilter = document.getElementById("mediaTypeFilter");
if (typeFilter) {
typeFilter.addEventListener("change", filterMedia);
}
// Load saved theme
loadTheme();
checkAuth().then((authenticated) => {
if (authenticated) {
loadSettings();
@@ -10,6 +34,128 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
// Toast Notification System - Make global for onclick handlers
window.showToast = function (message, type = "success") {
const container = document.getElementById("toastContainer");
if (!container) {
console.error("Toast container not found!");
return;
}
const icons = {
success: "bi-check-circle-fill",
error: "bi-x-circle-fill",
warning: "bi-exclamation-triangle-fill",
info: "bi-info-circle-fill",
};
const toast = document.createElement("div");
toast.className = `toast toast-${type}`;
toast.innerHTML = `
<div class="toast-icon">
<i class="bi ${icons[type] || icons.info}"></i>
</div>
<div class="toast-message">${message}</div>
<button class="toast-close" onclick="window.closeToast(this)">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(toast);
// Trigger animation
setTimeout(() => toast.classList.add("toast-show"), 10);
// Add visual feedback for success saves
if (type === "success" && message.includes("saved")) {
const saveBtn = document.querySelector('button[onclick*="saveSettings"]');
if (saveBtn) {
const originalBg = saveBtn.style.background;
const originalTransform = saveBtn.style.transform;
saveBtn.style.background =
"linear-gradient(135deg, #10b981 0%, #059669 100%)";
saveBtn.style.transform = "scale(1.05)";
saveBtn.innerHTML = '<i class="bi bi-check-lg"></i> Saved!';
setTimeout(() => {
saveBtn.style.background = originalBg;
saveBtn.style.transform = originalTransform;
saveBtn.innerHTML = '<i class="bi bi-save"></i> Save All Settings';
}, 2000);
}
}
// Auto remove after 5 seconds
setTimeout(() => {
toast.classList.add("toast-hide");
setTimeout(() => toast.remove(), 300);
}, 5000);
};
window.closeToast = function (button) {
const toast = button.closest(".toast");
toast.classList.add("toast-hide");
setTimeout(() => toast.remove(), 300);
};
// Theme Management - Make global for onclick handlers
function loadTheme() {
const savedTheme = localStorage.getItem("adminTheme") || "light";
applyTheme(savedTheme);
}
window.selectTheme = function (theme) {
console.log("selectTheme called with:", theme);
// Update UI
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
el.classList.remove("active");
});
event.target.closest(".theme-option").classList.add("active");
// Save and apply theme
localStorage.setItem("adminTheme", theme);
applyTheme(theme);
window.showToast(`Theme changed to ${theme} mode`, "success");
};
function applyTheme(theme) {
console.log("applyTheme called with:", theme);
const body = document.body;
if (theme === "dark") {
body.classList.add("dark-mode");
body.classList.remove("light-mode");
} else if (theme === "light") {
body.classList.add("light-mode");
body.classList.remove("dark-mode");
} else if (theme === "auto") {
// Check system preference
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (prefersDark) {
body.classList.add("dark-mode");
body.classList.remove("light-mode");
} else {
body.classList.add("light-mode");
body.classList.remove("dark-mode");
}
}
// Update active state in UI
const themeOptions = document.querySelectorAll(
".theme-selector .theme-option"
);
themeOptions.forEach((option, index) => {
const themes = ["light", "dark", "auto"];
if (themes[index] === theme) {
option.classList.add("active");
} else {
option.classList.remove("active");
}
});
}
async function loadSettings() {
try {
const response = await fetch("/api/admin/settings", {
@@ -38,6 +184,22 @@ function populateSettings() {
currentSettings.general.sitePhone || "";
document.getElementById("timezone").value =
currentSettings.general.timezone || "UTC";
// Logo and Favicon
if (currentSettings.general.siteLogo) {
document.getElementById("siteLogo").value =
currentSettings.general.siteLogo;
document.getElementById(
"logoPreview"
).innerHTML = `<img src="${currentSettings.general.siteLogo}" alt="Logo" />`;
}
if (currentSettings.general.siteFavicon) {
document.getElementById("siteFavicon").value =
currentSettings.general.siteFavicon;
document.getElementById(
"faviconPreview"
).innerHTML = `<img src="${currentSettings.general.siteFavicon}" alt="Favicon" />`;
}
}
// Homepage Settings
@@ -88,45 +250,184 @@ function populateSettings() {
}
}
function previewLogo() {
const fileInput = document.getElementById("siteLogo");
const preview = document.getElementById("logoPreview");
// Media Library Functions - Make global for onclick handlers
window.openMediaLibrary = async function (targetField) {
console.log("openMediaLibrary called for:", targetField);
currentMediaTarget = targetField;
selectedMediaUrl = null;
if (fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function (e) {
preview.innerHTML = `<img src="${e.target.result}" alt="Logo" />`;
};
reader.readAsDataURL(fileInput.files[0]);
// Load media files
try {
const response = await fetch("/api/admin/uploads", {
credentials: "include",
});
const data = await response.json();
if (data.success) {
allMedia = data.files || [];
renderMediaGrid(allMedia);
mediaLibraryModal.show();
} else {
showToast(data.message || "Failed to load media library", "error");
}
} catch (error) {
console.error("Failed to load media library:", error);
showToast("Failed to load media library. Please try again.", "error");
}
};
function renderMediaGrid(media) {
const grid = document.getElementById("mediaGrid");
if (media.length === 0) {
grid.innerHTML = `
<div class="text-center py-5" style="grid-column: 1/-1;">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted">No media files found</p>
</div>
`;
return;
}
grid.innerHTML = media
.map(
(file) => `
<div class="media-item" data-url="${
file.path
}" style="cursor: pointer; border: 3px solid transparent; border-radius: 8px; overflow: hidden; transition: all 0.3s;">
${
file.mimetype?.startsWith("image/")
? `<img src="${file.path}" alt="${
file.originalName || file.filename
}" style="width: 100%; height: 150px; object-fit: cover;" />`
: `<div style="width: 100%; height: 150px; background: #f8f9fa; display: flex; align-items: center; justify-content: center;">
<i class="bi bi-file-earmark fs-1 text-muted"></i>
</div>`
}
<div style="padding: 8px; font-size: 12px; text-align: center; background: white;">
<div style="font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${
file.originalName || file.filename
}</div>
<div style="color: #6c757d; font-size: 11px;">${formatFileSize(
file.size
)}</div>
</div>
</div>
`
)
.join("");
// Add click listeners to all media items
document.querySelectorAll(".media-item").forEach((item) => {
item.addEventListener("click", function () {
selectMedia(this.dataset.url);
});
});
}
function selectMedia(url) {
// Remove previous selection
document.querySelectorAll(".media-item").forEach((el) => {
el.style.border = "3px solid transparent";
});
// Mark current selection - find the clicked item
document.querySelectorAll(".media-item").forEach((el) => {
if (el.dataset.url === url) {
el.style.border = "3px solid #667eea";
el.style.background = "#f8f9fa";
}
});
selectedMediaUrl = url;
}
window.selectMediaFile = function () {
if (!selectedMediaUrl) {
window.showToast("Please select a media file", "warning");
return;
}
// Set the selected URL to the target field
document.getElementById(currentMediaTarget).value = selectedMediaUrl;
// Update preview
if (currentMediaTarget === "siteLogo") {
document.getElementById(
"logoPreview"
).innerHTML = `<img src="${selectedMediaUrl}" alt="Logo" />`;
} else if (currentMediaTarget === "siteFavicon") {
document.getElementById(
"faviconPreview"
).innerHTML = `<img src="${selectedMediaUrl}" alt="Favicon" />`;
}
// Close modal
mediaLibraryModal.hide();
window.showToast("Image selected successfully", "success");
};
function filterMedia() {
const searchTerm = document.getElementById("mediaSearch").value.toLowerCase();
const typeFilter = document.getElementById("mediaTypeFilter").value;
let filtered = allMedia;
// Filter by search term
if (searchTerm) {
filtered = filtered.filter(
(file) =>
file.filename.toLowerCase().includes(searchTerm) ||
file.originalName?.toLowerCase().includes(searchTerm)
);
}
// Filter by type
if (typeFilter !== "all") {
filtered = filtered.filter((file) => {
if (typeFilter === "image") return file.mimetype?.startsWith("image/");
if (typeFilter === "video") return file.mimetype?.startsWith("video/");
if (typeFilter === "document")
return (
file.mimetype?.includes("pdf") ||
file.mimetype?.includes("document") ||
file.mimetype?.includes("text")
);
return true;
});
}
renderMediaGrid(filtered);
}
function formatFileSize(bytes) {
if (!bytes) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}
function previewLogo() {
const url = document.getElementById("siteLogo").value;
const preview = document.getElementById("logoPreview");
if (url) {
preview.innerHTML = `<img src="${url}" alt="Logo" />`;
}
}
function previewFavicon() {
const fileInput = document.getElementById("siteFavicon");
const url = document.getElementById("siteFavicon").value;
const preview = document.getElementById("faviconPreview");
if (fileInput.files && fileInput.files[0]) {
const reader = new FileReader();
reader.onload = function (e) {
preview.innerHTML = `<img src="${e.target.result}" alt="Favicon" />`;
};
reader.readAsDataURL(fileInput.files[0]);
if (url) {
preview.innerHTML = `<img src="${url}" alt="Favicon" />`;
}
}
function selectLayout(layout) {
window.selectLayout = function (layout) {
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
el.classList.remove("active");
});
event.target.closest(".theme-option").classList.add("active");
}
function selectTheme(theme) {
document.querySelectorAll(".theme-selector .theme-option").forEach((el) => {
el.classList.remove("active");
});
event.target.closest(".theme-option").classList.add("active");
}
};
function updateColorPreview() {
const color = document.getElementById("accentColor").value;
@@ -134,7 +435,9 @@ function updateColorPreview() {
document.getElementById("colorValue").textContent = color;
}
async function saveSettings() {
window.saveSettings = async function () {
console.log("saveSettings called");
const settings = {
general: {
siteName: document.getElementById("siteName").value,
@@ -142,6 +445,8 @@ async function saveSettings() {
siteEmail: document.getElementById("siteEmail").value,
sitePhone: document.getElementById("sitePhone").value,
timezone: document.getElementById("timezone").value,
siteLogo: document.getElementById("siteLogo").value,
siteFavicon: document.getElementById("siteFavicon").value,
},
homepage: {
showHero: document.getElementById("showHero").checked,
@@ -171,6 +476,8 @@ async function saveSettings() {
},
};
console.log("Settings to save:", settings);
try {
const response = await fetch("/api/admin/settings", {
method: "POST",
@@ -180,34 +487,16 @@ async function saveSettings() {
});
const data = await response.json();
console.log("Save response:", data);
if (data.success) {
showSuccess("Settings saved successfully!");
window.showToast("Settings saved successfully!", "success");
currentSettings = settings;
} else {
showError(data.message || "Failed to save settings");
window.showToast(data.message || "Failed to save settings", "error");
}
} catch (error) {
console.error("Failed to save settings:", error);
showError("Failed to save settings");
window.showToast("Failed to save settings. Please try again.", "error");
}
}
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) window.location.href = "/admin/login.html";
} catch (error) {
console.error("Logout failed:", error);
}
}
function showSuccess(message) {
alert(message);
}
function showError(message) {
alert("Error: " + message);
}
};

View File

@@ -0,0 +1,266 @@
let teamMemberModal, notificationModal, confirmationModal;
let currentMemberId = null;
document.addEventListener("DOMContentLoaded", function () {
teamMemberModal = new bootstrap.Modal(
document.getElementById("teamMemberModal")
);
notificationModal = new bootstrap.Modal(
document.getElementById("notificationModal")
);
confirmationModal = new bootstrap.Modal(
document.getElementById("confirmationModal")
);
checkAuth().then((authenticated) => {
if (authenticated) {
loadTeamMembers();
}
});
// Image preview on URL change
document.getElementById("memberImage").addEventListener("input", function () {
updateImagePreview(this.value);
});
});
// Load all team members
async function loadTeamMembers() {
try {
const response = await fetch("/api/admin/team-members");
const data = await response.json();
if (data.success && data.teamMembers) {
displayTeamMembers(data.teamMembers);
} else {
showNotification("Failed to load team members", "error");
}
} catch (error) {
console.error("Error loading team members:", error);
showNotification("Error loading team members", "error");
}
}
// Display team members in grid
function displayTeamMembers(members) {
const container = document.getElementById("teamMembersContainer");
if (members.length === 0) {
container.innerHTML = `
<div class="col-12 text-center py-5">
<i class="bi bi-people" style="font-size: 4rem; color: #cbd5e0;"></i>
<p class="mt-3 text-muted">No team members yet. Add your first team member!</p>
</div>
`;
return;
}
container.innerHTML = members
.map(
(member) => `
<div class="col-md-6 col-lg-4 mb-4">
<div class="team-preview-card">
<div class="team-preview-image">
${
member.image_url
? `<img src="${member.image_url}" alt="${member.name}" />`
: `<i class="bi bi-person-circle"></i>`
}
</div>
<div class="team-preview-name">${escapeHtml(member.name)}</div>
<div class="team-preview-position">${escapeHtml(member.position)}</div>
<div class="team-preview-bio">${
member.bio ? escapeHtml(member.bio) : ""
}</div>
<div class="mt-3 d-flex justify-content-center gap-2">
<button class="btn btn-sm btn-outline-primary" onclick="editTeamMember('${
member.id
}')">
<i class="bi bi-pencil"></i> Edit
</button>
<button class="btn btn-sm btn-outline-danger" onclick="confirmDelete('${
member.id
}', '${escapeHtml(member.name)}')">
<i class="bi bi-trash"></i> Delete
</button>
</div>
<div class="mt-2">
<small class="text-muted">Order: ${member.display_order}</small>
</div>
</div>
</div>
`
)
.join("");
}
// Show add modal
function showAddModal() {
currentMemberId = null;
document.getElementById("modalTitle").textContent = "Add Team Member";
document.getElementById("teamMemberForm").reset();
document.getElementById("memberId").value = "";
document.getElementById("imagePreview").innerHTML = "";
teamMemberModal.show();
}
// Edit team member
async function editTeamMember(id) {
try {
const response = await fetch(`/api/admin/team-members/${id}`);
const data = await response.json();
if (data.success && data.teamMember) {
currentMemberId = id;
const member = data.teamMember;
document.getElementById("modalTitle").textContent = "Edit Team Member";
document.getElementById("memberId").value = member.id;
document.getElementById("memberName").value = member.name;
document.getElementById("memberPosition").value = member.position;
document.getElementById("memberBio").value = member.bio || "";
document.getElementById("memberImage").value = member.image_url || "";
document.getElementById("displayOrder").value = member.display_order || 0;
updateImagePreview(member.image_url);
teamMemberModal.show();
} else {
showNotification("Failed to load team member details", "error");
}
} catch (error) {
console.error("Error loading team member:", error);
showNotification("Error loading team member", "error");
}
}
// Save team member (create or update)
async function saveTeamMember() {
const id = document.getElementById("memberId").value;
const name = document.getElementById("memberName").value.trim();
const position = document.getElementById("memberPosition").value.trim();
const bio = document.getElementById("memberBio").value.trim();
const image_url = document.getElementById("memberImage").value.trim();
const display_order =
parseInt(document.getElementById("displayOrder").value) || 0;
if (!name || !position) {
showNotification("Name and position are required", "error");
return;
}
const payload = {
name,
position,
bio,
image_url,
display_order,
};
try {
const url = id
? `/api/admin/team-members/${id}`
: "/api/admin/team-members";
const method = id ? "PUT" : "POST";
const response = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await response.json();
if (data.success) {
showNotification(
data.message || "Team member saved successfully",
"success"
);
teamMemberModal.hide();
loadTeamMembers();
} else {
showNotification(data.message || "Failed to save team member", "error");
}
} catch (error) {
console.error("Error saving team member:", error);
showNotification("Error saving team member", "error");
}
}
// Confirm delete
function confirmDelete(id, name) {
currentMemberId = id;
document.getElementById(
"confirmationMessage"
).textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
const confirmBtn = document.getElementById("confirmButton");
confirmBtn.onclick = () => deleteTeamMember(id);
confirmationModal.show();
}
// Delete team member
async function deleteTeamMember(id) {
try {
const response = await fetch(`/api/admin/team-members/${id}`, {
method: "DELETE",
});
const data = await response.json();
if (data.success) {
showNotification("Team member deleted successfully", "success");
confirmationModal.hide();
loadTeamMembers();
} else {
showNotification("Failed to delete team member", "error");
}
} catch (error) {
console.error("Error deleting team member:", error);
showNotification("Error deleting team member", "error");
}
}
// Update image preview
function updateImagePreview(url) {
const preview = document.getElementById("imagePreview");
if (url) {
preview.innerHTML = `
<img src="${url}" alt="Preview" style="max-width: 150px; max-height: 150px; border-radius: 50%; border: 3px solid #667eea;" />
`;
} else {
preview.innerHTML = "";
}
}
// Open media library (placeholder for future integration)
function openMediaLibrary() {
// For now, redirect to media library in a new window
window.open("/admin/media-library.html", "_blank");
showNotification(
"Select an image from the media library and copy its URL back here",
"success"
);
}
// Show notification
function showNotification(message, type = "success") {
const modal = document.getElementById("notificationModal");
const header = modal.querySelector(".modal-header");
const messageEl = document.getElementById("notificationMessage");
const title = document.getElementById("notificationTitle");
header.className = "modal-header " + type;
title.textContent = type === "success" ? "Success" : "Error";
messageEl.textContent = message;
notificationModal.show();
}
// Escape HTML
function escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}

View File

@@ -313,18 +313,6 @@ function updatePermissionsPreview() {
.join("");
}
async function logout() {
try {
const response = await fetch("/api/admin/logout", {
method: "POST",
credentials: "include",
});
if (response.ok) window.location.href = "/admin/login.html";
} catch (error) {
console.error("Logout failed:", error);
}
}
function escapeHtml(text) {
const map = {
"&": "&amp;",

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,221 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<!-- Quill Editor CSS -->
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
/* Quill Editor Styling */
.ql-container {
font-size: 16px;
position: relative;
}
.ql-editor {
overflow-y: auto;
overflow-x: hidden;
}
/* Quill Editor Scrollbar */
.ql-editor::-webkit-scrollbar {
width: 12px;
}
.ql-editor::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 6px;
}
.ql-editor::-webkit-scrollbar-thumb {
background: #888;
border-radius: 6px;
}
.ql-editor::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Modal Enhancements */
#pageModal .modal-dialog {
max-width: 90vw;
margin: 1.75rem auto;
}
#pageModal .modal-content {
max-height: 90vh;
display: flex;
flex-direction: column;
}
#pageModal .modal-header {
user-select: none;
flex-shrink: 0;
}
#pageModal .modal-body {
overflow-y: auto;
overflow-x: hidden;
flex: 1 1 auto;
max-height: calc(90vh - 140px);
}
#pageModal .modal-footer {
flex-shrink: 0;
}
/* Scrollbar Styling */
#pageModal .modal-body::-webkit-scrollbar {
width: 10px;
}
#pageModal .modal-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 5px;
}
#pageModal .modal-body::-webkit-scrollbar-thumb {
background: #888;
border-radius: 5px;
}
#pageModal .modal-body::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Contact Fields - removed duplicate overflow styles */
/* Resize Handle */
.modal-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #6c757d 50%);
opacity: 0.5;
z-index: 1;
}
.modal-resize-handle:hover {
opacity: 0.8;
}
/* Fullscreen Toggle Button */
.btn-fullscreen {
position: absolute;
right: 50px;
top: 12px;
padding: 0.25rem 0.5rem;
font-size: 1.2rem;
background: transparent;
border: none;
color: #6c757d;
cursor: pointer;
}
.btn-fullscreen:hover {
color: #000;
}
/* Fullscreen Mode */
.modal-fullscreen .modal-dialog {
max-width: 100vw;
margin: 0;
height: 100vh;
}
.modal-fullscreen .modal-content {
max-height: 100vh;
height: 100vh;
border-radius: 0;
}
.modal-fullscreen .modal-body {
max-height: calc(100vh - 140px);
}
/* Editor resize styling */
.editor-resizable {
position: relative;
border: 1px solid #dee2e6;
border-radius: 4px;
overflow: visible;
}
.editor-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
background: linear-gradient(135deg, transparent 50%, #667eea 50%);
z-index: 1000;
transition: background 0.2s;
}
.editor-resize-handle:hover {
background: linear-gradient(135deg, transparent 50%, #5568d3 50%);
}
.editor-resize-handle:active {
background: linear-gradient(135deg, transparent 50%, #4451b8 50%);
}
/* Expanded state removed - not needed */
/* Team Member Card in Admin */
.team-member-card {
border: 2px solid #e2e8f0;
border-radius: 12px;
padding: 20px;
background: white;
transition: all 0.3s ease;
}
.team-member-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.team-member-preview {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #667eea;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
}
.team-member-preview img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.team-member-preview i {
font-size: 2rem;
color: #667eea;
}
.team-member-handle {
cursor: move;
color: #cbd5e0;
padding: 5px;
}
.team-member-handle:hover {
color: #667eea;
}
</style>
</head>
<body>
<div class="sidebar">
@@ -157,8 +371,185 @@
<label for="pageContent" class="form-label"
>Page Content *</label
>
<!-- Structured Contact Fields (shown only for contact page) -->
<div
id="contactStructuredFields"
style="display: none"
class="editor-resizable"
>
<div
id="contactFieldsContent"
style="
height: 500px;
overflow-y: auto;
overflow-x: hidden;
padding: 15px;
"
>
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
<strong>Contact Page:</strong> Edit each section
independently. The layout will remain organized.
</div>
<!-- Header Section -->
<div class="card mb-3">
<div class="card-header bg-primary text-white">
<i class="bi bi-card-heading"></i> Header Section
</div>
<div class="card-body">
<div class="mb-2">
<label class="form-label">Title</label>
<input
type="text"
class="form-control"
id="contactHeaderTitle"
placeholder="Our Contact Information"
/>
</div>
<div class="mb-2">
<label class="form-label">Subtitle</label>
<input
type="text"
class="form-control"
id="contactHeaderSubtitle"
placeholder="Reach out to us through any of these channels"
/>
</div>
</div>
</div>
<!-- Contact Information -->
<div class="card mb-3">
<div class="card-header bg-success text-white">
<i class="bi bi-telephone"></i> Contact Information
</div>
<div class="card-body">
<div class="mb-2">
<label class="form-label">Phone Number</label>
<input
type="text"
class="form-control"
id="contactPhone"
placeholder="+1 (555) 123-4567"
/>
</div>
<div class="mb-2">
<label class="form-label">Email Address</label>
<input
type="email"
class="form-control"
id="contactEmail"
placeholder="contact@skyartshop.com"
/>
</div>
<div class="mb-2">
<label class="form-label">Physical Address</label>
<input
type="text"
class="form-control"
id="contactAddress"
placeholder="123 Art Street, Creative City, CC 12345"
/>
</div>
</div>
</div>
<!-- Business Hours -->
<div class="card mb-3">
<div class="card-header bg-warning text-dark">
<i class="bi bi-clock"></i> Business Hours
</div>
<div class="card-body">
<div id="businessHoursList">
<!-- Dynamic business hours will be added here -->
</div>
<button
type="button"
class="btn btn-sm btn-outline-primary"
onclick="addBusinessHour()"
>
<i class="bi bi-plus-circle"></i> Add Time Slot
</button>
</div>
</div>
</div>
<div
class="editor-resize-handle"
data-target="contactFieldsContent"
></div>
</div>
<!-- About Page with Team Members Section -->
<div id="aboutWithTeamFields" style="display: none">
<div class="alert alert-info mb-3">
<i class="bi bi-info-circle"></i>
<strong>About Page:</strong> Edit the main content and
manage your team members below.
</div>
<!-- About Content Editor -->
<div class="card mb-3">
<div class="card-header bg-primary text-white">
<i class="bi bi-file-text"></i> About Content
</div>
<div class="card-body p-0 position-relative">
<div class="editor-resizable">
<div
id="aboutContentEditor"
style="background: white; height: 300px"
></div>
<div
class="editor-resize-handle"
data-target="aboutContentEditor"
></div>
</div>
</div>
</div>
<!-- Team Members Section -->
<div class="card mb-3">
<div
class="card-header bg-success text-white d-flex justify-content-between align-items-center"
>
<span><i class="bi bi-people"></i> Team Members</span>
<button
type="button"
class="btn btn-sm btn-light"
onclick="addTeamMember()"
>
<i class="bi bi-plus-lg"></i> Add Member
</button>
</div>
<div class="card-body">
<div id="teamMembersList" class="row g-3">
<div class="col-12 text-center text-muted py-3">
<i class="bi bi-people" style="font-size: 3rem"></i>
<p class="mt-2">
No team members yet. Click "Add Member" to get
started.
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Regular Quill Editor (for other pages) -->
<div id="regularContentEditor" class="editor-resizable">
<div
id="pageContentEditor"
style="background: white; height: 400px"
></div>
<div
class="editor-resize-handle"
data-target="pageContentEditor"
></div>
</div>
<textarea
class="form-control"
class="form-control d-none"
id="pageContent"
rows="15"
required
@@ -211,10 +602,84 @@
<i class="bi bi-save"></i> Save Page
</button>
</div>
<div class="modal-resize-handle" title="Drag to resize"></div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div class="modal fade" id="notificationModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content" id="notificationModalContent">
<div class="modal-header" id="notificationModalHeader">
<h5 class="modal-title" id="notificationModalTitle">
<i class="bi" id="notificationModalIcon"></i>
<span id="notificationModalTitleText"></span>
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body" id="notificationModalBody">
<!-- Message will be inserted here -->
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-primary"
data-bs-dismiss="modal"
>
OK
</button>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="confirmModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content border-warning" style="border-width: 3px">
<div class="modal-header bg-warning text-dark">
<h5 class="modal-title">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
Confirm Action
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body" id="confirmModalBody">
<!-- Confirmation message will be inserted here -->
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button
type="button"
class="btn btn-danger"
id="confirmModalButton"
>
<i class="bi bi-trash"></i> Delete
</button>
</div>
</div>
</div>
</div>
<!-- Quill Editor JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/pages.js"></script>

View File

@@ -12,6 +12,11 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<!-- Quill Editor CSS -->
<link
href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
</head>
<body>
@@ -117,15 +122,37 @@
</div>
<div class="modal fade" id="projectModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Add Portfolio Project</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
<div class="d-flex gap-2 align-items-center">
<button
type="button"
class="btn btn-sm btn-primary"
id="btnExpandModal"
onclick="toggleModalSize()"
title="Expand/Collapse"
style="
padding: 0.375rem 0.75rem;
display: flex;
align-items: center;
gap: 5px;
"
>
<i
class="bi bi-arrows-fullscreen"
id="expandIcon"
style="font-size: 16px"
></i>
<span style="font-size: 13px">Expand</span>
</button>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
</div>
<div class="modal-body">
<form id="projectForm">
@@ -147,12 +174,14 @@
<label for="projectDescription" class="form-label"
>Description *</label
>
<div id="projectDescriptionEditor" style="height: 200px"></div>
<textarea
class="form-control"
id="projectDescription"
rows="4"
required
style="display: none"
></textarea>
<small class="text-muted"
>Use the editor to format your project description</small
>
</div>
<div class="mb-3">
@@ -163,24 +192,35 @@
type="text"
class="form-control"
id="projectCategory"
placeholder="e.g., Digital Art, Photography"
placeholder="e.g., Digital Art, Photography, Illustration"
/>
</div>
<div class="mb-3">
<label for="projectImages" class="form-label"
>Project Images/Gallery</label
<!-- Project Images Gallery -->
<div class="mb-4">
<label class="form-label"
><i class="bi bi-images"></i> Project Images/Gallery</label
>
<input
type="file"
class="form-control"
id="projectImages"
multiple
accept="image/*"
/>
<small class="text-muted"
>Upload multiple images for gallery</small
<p class="text-muted small">
Select images from your media library for this portfolio
project.
</p>
<button
type="button"
class="btn btn-sm btn-primary mb-3"
onclick="openMediaLibrary('portfolioImages')"
>
<i class="bi bi-cloud-upload"></i> Select from Media Library
</button>
<div
id="portfolioImagesGallery"
class="d-flex flex-wrap gap-2 border rounded p-3"
style="min-height: 100px"
>
<div class="text-muted small">
No images added yet. Click above to add images.
</div>
</div>
</div>
<div class="mb-3">
@@ -219,7 +259,9 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Quill Editor JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/portfolio.js"></script>
<script src="/admin/js/portfolio.js?v=5.0"></script>
</body>
</html>

View File

@@ -12,6 +12,11 @@
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<!-- Quill Editor CSS -->
<link
href="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.snow.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
</head>
<body>
@@ -73,7 +78,7 @@
<p class="mb-0 text-muted">Manage your product catalog</p>
</div>
<div>
<button class="btn-logout" onclick="logout()">
<button class="btn-logout" id="btnLogout">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
@@ -81,7 +86,7 @@
<!-- Actions Bar -->
<div class="actions-bar">
<button class="btn btn-primary" onclick="showCreateProduct()">
<button class="btn btn-primary" id="btnAddProduct">
<i class="bi bi-plus-circle"></i> Add New Product
</button>
<div class="search-box">
@@ -90,7 +95,6 @@
type="text"
placeholder="Search products..."
id="searchInput"
oninput="filterProducts()"
/>
</div>
</div>
@@ -140,6 +144,7 @@
<form id="productForm">
<input type="hidden" id="productId" />
<!-- Product Name -->
<div class="mb-3">
<label for="productName" class="form-label"
>Product Name *</label
@@ -152,57 +157,175 @@
/>
</div>
<!-- Short Description -->
<div class="mb-3">
<label for="productDescription" class="form-label"
>Description</label
<label for="productShortDescription" class="form-label"
>Short Description</label
>
<textarea
class="form-control"
id="productDescription"
rows="4"
id="productShortDescription"
rows="2"
placeholder="Brief description for product listings (max 500 characters)"
maxlength="500"
></textarea>
<small class="text-muted"
>This will be shown in product listings</small
>
</div>
<!-- Full Description with Rich Text Editor -->
<div class="mb-3">
<label for="productDescription" class="form-label"
>Full Description *</label
>
<div id="productDescriptionEditor" style="height: 200px"></div>
<textarea
id="productDescription"
style="display: none"
></textarea>
<small class="text-muted"
>Use the editor to format your product description</small
>
</div>
<!-- Pricing and Stock -->
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="productPrice" class="form-label">Price *</label>
<input
type="number"
step="0.01"
class="form-control"
id="productPrice"
required
/>
<div class="input-group">
<span class="input-group-text">$</span>
<input
type="number"
step="0.01"
class="form-control"
id="productPrice"
required
/>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="productStock" class="form-label"
>Stock Quantity</label
>
<input type="number" class="form-control" id="productStock" />
<input
type="number"
class="form-control"
id="productStock"
value="0"
/>
</div>
<div class="col-md-4 mb-3">
<label for="productSKU" class="form-label">SKU</label>
<input type="text" class="form-control" id="productSKU" />
</div>
</div>
<div class="mb-3">
<label for="productCategory" class="form-label">Category</label>
<input type="text" class="form-control" id="productCategory" />
</div>
<div class="mb-3">
<label for="productImages" class="form-label"
>Product Images</label
>
<input
type="file"
class="form-control"
id="productImages"
multiple
accept="image/*"
/>
<small class="text-muted">You can upload multiple images</small>
</div>
<!-- Category and Details -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="productCategory" class="form-label"
>Category</label
>
<input
type="text"
class="form-control"
id="productCategory"
placeholder="e.g., Canvas Art, Prints"
/>
</div>
<div class="col-md-6 mb-3">
<label for="productMaterial" class="form-label"
>Material</label
>
<input
type="text"
class="form-control"
id="productMaterial"
placeholder="e.g., Canvas, Acrylic"
/>
</div>
</div>
<!-- Dimensions and Weight -->
<div class="row">
<div class="col-md-6 mb-3">
<label for="productDimensions" class="form-label"
>Dimensions</label
>
<input
type="text"
class="form-control"
id="productDimensions"
placeholder="e.g., 24x36 inches"
/>
</div>
<div class="col-md-6 mb-3">
<label for="productWeight" class="form-label"
>Weight (lbs)</label
>
<input
type="number"
step="0.1"
class="form-control"
id="productWeight"
placeholder="e.g., 2.5"
/>
</div>
</div>
<!-- Product Images Gallery -->
<div class="mb-4">
<label class="form-label"
><i class="bi bi-image"></i> Product Images</label
>
<p class="text-muted small">
Upload or select images for this product. These images will be
available to assign to color variants below.
</p>
<button
type="button"
class="btn btn-sm btn-primary mb-3"
onclick="openMediaLibrary('productImage')"
>
<i class="bi bi-cloud-upload"></i> Select from Media Library
</button>
<div
id="productImagesGallery"
class="d-flex flex-wrap gap-2 border rounded p-3"
style="min-height: 100px"
>
<!-- Product images will be displayed here -->
<div class="text-muted small">
No images added yet. Click above to add images.
</div>
</div>
</div>
<!-- Product Images with Color Variants -->
<div class="mb-3">
<label class="form-label"
><i class="bi bi-images"></i> Product Images with Color
Variants</label
>
<div id="imageVariantsContainer" class="border rounded p-3">
<!-- Image variants will be added here -->
</div>
<button
type="button"
class="btn btn-sm btn-outline-primary mt-2"
id="btnAddImageVariant"
>
<i class="bi bi-plus-circle"></i> Add Image with Color Variant
</button>
<small class="text-muted d-block mt-2"
>Add multiple images and assign color variants to each</small
>
</div>
<!-- Status Checkboxes -->
<div class="row">
<div class="col-md-4 mb-3">
<div class="form-check form-switch">
<input
class="form-check-input"
@@ -211,11 +334,23 @@
checked
/>
<label class="form-check-label" for="productActive">
Active (Visible on website)
<i class="bi bi-eye"></i> Active (Visible on website)
</label>
</div>
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="productFeatured"
/>
<label class="form-check-label" for="productFeatured">
<i class="bi bi-star"></i> Featured
</label>
</div>
</div>
<div class="col-md-4 mb-3">
<div class="form-check form-switch">
<input
class="form-check-input"
@@ -223,7 +358,7 @@
id="productBestSeller"
/>
<label class="form-check-label" for="productBestSeller">
Mark as Best Seller
<i class="bi bi-award"></i> Best Seller
</label>
</div>
</div>
@@ -238,11 +373,7 @@
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
onclick="saveProduct()"
>
<button type="button" class="btn btn-primary" id="btnSaveProduct">
<i class="bi bi-save"></i> Save & Publish
</button>
</div>
@@ -251,6 +382,8 @@
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Quill Editor JS -->
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.7/dist/quill.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/products.js"></script>
</body>

View File

@@ -117,6 +117,9 @@
</style>
</head>
<body>
<!-- Toast Notification Container -->
<div class="toast-container" id="toastContainer"></div>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
@@ -227,26 +230,44 @@
<div class="row">
<div class="col-md-6 mb-3">
<label for="siteLogo" class="form-label">Logo</label>
<input
type="file"
class="form-control"
id="siteLogo"
accept="image/*"
onchange="previewLogo()"
/>
<div class="input-group">
<input
type="text"
class="form-control"
id="siteLogo"
placeholder="Select logo from media library"
readonly
/>
<button
class="btn btn-outline-secondary"
type="button"
onclick="openMediaLibrary('siteLogo')"
>
<i class="bi bi-images"></i> Choose from Library
</button>
</div>
<div class="logo-preview" id="logoPreview">
<span class="text-muted">No logo uploaded</span>
<span class="text-muted">No logo selected</span>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="siteFavicon" class="form-label">Favicon</label>
<input
type="file"
class="form-control"
id="siteFavicon"
accept="image/*"
onchange="previewFavicon()"
/>
<div class="input-group">
<input
type="text"
class="form-control"
id="siteFavicon"
placeholder="Select favicon from media library"
readonly
/>
<button
class="btn btn-outline-secondary"
type="button"
onclick="openMediaLibrary('siteFavicon')"
>
<i class="bi bi-images"></i> Choose from Library
</button>
</div>
<div class="favicon-preview" id="faviconPreview">
<i class="bi bi-image text-muted"></i>
</div>
@@ -531,6 +552,82 @@
</div>
</div>
<!-- Media Library Modal -->
<div
class="modal fade"
id="mediaLibraryModal"
tabindex="-1"
aria-labelledby="mediaLibraryModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="mediaLibraryModalLabel">
<i class="bi bi-images"></i> Select from Media Library
</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="row mb-3">
<div class="col-md-8">
<input
type="text"
class="form-control"
id="mediaSearch"
placeholder="Search media files..."
/>
</div>
<div class="col-md-4">
<select class="form-select" id="mediaTypeFilter">
<option value="all">All Types</option>
<option value="image">Images</option>
<option value="video">Videos</option>
<option value="document">Documents</option>
</select>
</div>
</div>
<div
id="mediaGrid"
style="
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 15px;
max-height: 500px;
overflow-y: auto;
"
>
<div class="text-center py-5">
<i class="bi bi-hourglass-split fs-1 text-muted"></i>
<p class="text-muted">Loading media...</p>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
onclick="selectMediaFile()"
>
<i class="bi bi-check-lg"></i> Select
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/settings.js"></script>

View File

@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Team Members - Admin</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/admin/css/admin.css" />
<style>
/* Team Member Card Preview */
.team-preview-card {
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
text-align: center;
background: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.team-preview-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.2);
}
.team-preview-image {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #667eea;
margin: 0 auto 15px;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
}
.team-preview-image img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.team-preview-image i {
font-size: 3rem;
color: #667eea;
}
.team-preview-name {
font-size: 1.25rem;
font-weight: 600;
color: #2d3748;
margin-bottom: 5px;
}
.team-preview-position {
font-size: 1rem;
color: #667eea;
font-weight: 500;
margin-bottom: 10px;
}
.team-preview-bio {
font-size: 0.875rem;
color: #718096;
line-height: 1.6;
}
/* Reorder handle */
.reorder-handle {
cursor: move;
color: #667eea;
font-size: 1.2rem;
margin-right: 10px;
}
/* Custom Notification Modal */
#notificationModal .modal-header.success {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}
#notificationModal .modal-header.error {
background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
color: white;
}
#confirmationModal .modal-header {
background: linear-gradient(135deg, #ecc94b 0%, #d69e2e 100%);
color: white;
}
.modal-header .btn-close {
filter: brightness(0) invert(1);
}
</style>
</head>
<body>
<div class="admin-container">
<!-- Sidebar -->
<aside class="admin-sidebar" id="sidebar">
<div class="sidebar-header">
<h4>Sky Art Shop</h4>
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-x-lg"></i>
</button>
</div>
<nav class="sidebar-nav">
<a href="/admin/dashboard.html" class="nav-item">
<i class="bi bi-speedometer2"></i>
<span>Dashboard</span>
</a>
<a href="/admin/products.html" class="nav-item">
<i class="bi bi-box-seam"></i>
<span>Products</span>
</a>
<a href="/admin/portfolio.html" class="nav-item">
<i class="bi bi-images"></i>
<span>Portfolio</span>
</a>
<a href="/admin/blog.html" class="nav-item">
<i class="bi bi-file-text"></i>
<span>Blog</span>
</a>
<a href="/admin/pages.html" class="nav-item">
<i class="bi bi-file-earmark"></i>
<span>Pages</span>
</a>
<a href="/admin/team-members.html" class="nav-item active">
<i class="bi bi-people"></i>
<span>Team Members</span>
</a>
<a href="/admin/media-library.html" class="nav-item">
<i class="bi bi-image"></i>
<span>Media Library</span>
</a>
<a href="/admin/menu.html" class="nav-item">
<i class="bi bi-list"></i>
<span>Menu</span>
</a>
<a href="/admin/users.html" class="nav-item">
<i class="bi bi-person"></i>
<span>Users</span>
</a>
<a href="/admin/settings.html" class="nav-item">
<i class="bi bi-gear"></i>
<span>Settings</span>
</a>
</nav>
<div class="sidebar-footer">
<button class="btn btn-danger w-100" id="logoutBtn">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</aside>
<!-- Main Content -->
<main class="admin-main">
<div class="admin-header">
<button class="mobile-toggle" id="mobileToggle">
<i class="bi bi-list"></i>
</button>
<h1>Team Members</h1>
<button class="btn btn-primary" onclick="showAddModal()">
<i class="bi bi-plus-lg"></i> Add Team Member
</button>
</div>
<div class="admin-content">
<div class="row" id="teamMembersContainer">
<div class="col-12 text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Team Member Modal -->
<div
class="modal fade"
id="teamMemberModal"
tabindex="-1"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Add Team Member</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<form id="teamMemberForm">
<input type="hidden" id="memberId" />
<div class="mb-3">
<label for="memberName" class="form-label">Name *</label>
<input
type="text"
class="form-control"
id="memberName"
required
/>
</div>
<div class="mb-3">
<label for="memberPosition" class="form-label"
>Position/Title *</label
>
<input
type="text"
class="form-control"
id="memberPosition"
placeholder="e.g., Founder & Lead Artist"
required
/>
</div>
<div class="mb-3">
<label for="memberBio" class="form-label">Bio</label>
<textarea
class="form-control"
id="memberBio"
rows="4"
placeholder="Brief introduction about the team member..."
></textarea>
</div>
<div class="mb-3">
<label for="memberImage" class="form-label">Image URL</label>
<div class="input-group">
<input
type="text"
class="form-control"
id="memberImage"
placeholder="Enter image URL or select from media library"
/>
<button
type="button"
class="btn btn-outline-secondary"
onclick="openMediaLibrary()"
>
<i class="bi bi-image"></i> Browse
</button>
</div>
<div class="mt-2" id="imagePreview"></div>
</div>
<div class="mb-3">
<label for="displayOrder" class="form-label"
>Display Order</label
>
<input
type="number"
class="form-control"
id="displayOrder"
value="0"
min="0"
/>
<small class="text-muted">Lower numbers appear first</small>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
onclick="saveTeamMember()"
>
Save
</button>
</div>
</div>
</div>
</div>
<!-- Notification Modal -->
<div class="modal fade" id="notificationModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="notificationTitle">Notification</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body" id="notificationMessage"></div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
OK
</button>
</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div class="modal fade" id="confirmationModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Confirm Action</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body" id="confirmationMessage"></div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="button" class="btn btn-danger" id="confirmButton">
Confirm
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/sidebar.js"></script>
<script src="/admin/js/team-members.js"></script>
</body>
</html>

View File

@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Logout Fix Test - Sky Art Shop</title>
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<style>
body {
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
}
.test-section {
margin: 20px 0;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.btn-logout {
background: #dc3545;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
}
.btn-logout:hover {
background: #c82333;
}
.log-output {
background: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-top: 10px;
max-height: 300px;
overflow-y: auto;
font-family: monospace;
font-size: 12px;
}
.log-entry {
margin: 5px 0;
padding: 5px;
border-left: 3px solid #007bff;
padding-left: 10px;
}
.log-success {
border-left-color: #28a745;
}
.log-error {
border-left-color: #dc3545;
}
</style>
</head>
<body>
<div class="container">
<h1>🔍 Logout Fix Verification Test</h1>
<p class="text-muted">
This page tests that the logout confirmation dialog appears correctly.
</p>
<div class="test-section">
<h3>Test 1: Dashboard-style Button (onclick via event listener)</h3>
<p>This simulates how the dashboard logout button works:</p>
<button class="btn-logout" id="logoutBtn">
<i class="bi bi-box-arrow-right"></i> Logout (Dashboard Style)
</button>
</div>
<div class="test-section">
<h3>Test 2: Other Pages-style Button (inline onclick)</h3>
<p>
This simulates how other pages (settings, blog, etc.) logout buttons
work:
</p>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout (Inline onclick)
</button>
</div>
<div class="test-section">
<h3>Test 3: Direct window.logout() Call</h3>
<p>This tests the global logout function directly:</p>
<button class="btn btn-warning" onclick="window.logout()">
<i class="bi bi-box-arrow-right"></i> Test window.logout()
</button>
</div>
<div class="test-section">
<h3>Expected Behavior</h3>
<ul>
<li>✅ All buttons should show the same confirmation dialog</li>
<li>✅ Dialog should say "Confirm Logout"</li>
<li>✅ Dialog should have "Cancel" and "Logout" buttons</li>
<li>✅ Cancel should close the dialog without logging out</li>
<li>
✅ Logout should proceed (for this test, it will redirect to login)
</li>
</ul>
</div>
<div class="test-section">
<h3>Test Log</h3>
<div class="log-output" id="logOutput"></div>
<button class="btn btn-secondary btn-sm mt-2" onclick="clearLog()">
Clear Log
</button>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script>
// Test logging
function addLog(message, type = "info") {
const logOutput = document.getElementById("logOutput");
const entry = document.createElement("div");
entry.className = `log-entry ${
type === "success"
? "log-success"
: type === "error"
? "log-error"
: ""
}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
logOutput.appendChild(entry);
logOutput.scrollTop = logOutput.scrollHeight;
}
function clearLog() {
document.getElementById("logOutput").innerHTML = "";
}
// Override performLogout to prevent actual logout during testing
const originalPerformLogout = window.performLogout;
if (typeof performLogout !== "undefined") {
window.performLogout = async function () {
addLog(
"✅ Logout confirmed! (Redirect disabled for testing)",
"success"
);
console.log("Logout would execute here");
};
}
// Monitor logout function calls
const originalLogout = window.logout;
window.logout = function (skipConfirm) {
addLog(`🔵 logout() called with skipConfirm=${skipConfirm}`);
if (originalLogout) {
return originalLogout(skipConfirm);
}
};
// Page loaded
document.addEventListener("DOMContentLoaded", function () {
addLog("✅ Page loaded successfully");
addLog(
`✅ window.logout exists: ${typeof window.logout === "function"}`
);
addLog(
`✅ window.showLogoutConfirm exists: ${
typeof window.showLogoutConfirm === "function"
}`
);
// Test that auth.js event listeners are attached
const logoutBtn = document.getElementById("logoutBtn");
if (logoutBtn) {
addLog("✅ Dashboard-style logout button found and ready");
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Products Button Test</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.test-section {
background: #f5f5f5;
padding: 20px;
margin: 20px 0;
border-radius: 8px;
}
.btn {
padding: 10px 20px;
margin: 10px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 5px;
}
.success {
background: #d4edda;
color: #155724;
}
.error {
background: #f8d7da;
color: #721c24;
}
</style>
</head>
<body>
<h1>🧪 Products Button Test</h1>
<p>Testing if the "Add New Product" button works without CSP errors.</p>
<div class="test-section">
<h3>Test 1: Event Listener (Recommended)</h3>
<button class="btn btn-primary" id="testBtn1">
Test Button with Event Listener
</button>
<div id="result1"></div>
</div>
<div class="test-section">
<h3>Test 2: Inline Handler (With CSP Fix)</h3>
<button class="btn btn-primary" onclick="testInlineHandler()">
Test Button with Inline Handler
</button>
<div id="result2"></div>
</div>
<div class="test-section">
<h3>Test 3: Navigate to Products Page</h3>
<a href="/admin/products.html" class="btn btn-primary">
🛍️ Go to Products Management
</a>
</div>
<div class="test-section">
<h3>CSP Status Check</h3>
<div id="cspStatus">Checking...</div>
</div>
<script>
// Test 1: Event Listener
document
.getElementById("testBtn1")
.addEventListener("click", function () {
document.getElementById("result1").innerHTML =
'<div class="status success">✅ Event listener works! No CSP errors.</div>';
});
// Test 2: Inline Handler Function
function testInlineHandler() {
document.getElementById("result2").innerHTML =
'<div class="status success">✅ Inline handler works! CSP is configured correctly.</div>';
}
// Check CSP Headers
fetch("/admin/products.html", { method: "HEAD" })
.then((response) => {
const csp = response.headers.get("Content-Security-Policy");
const hasScriptSrcAttr = csp && csp.includes("script-src-attr");
const hasUnsafeInline = csp && csp.includes("'unsafe-inline'");
let statusHtml = "";
if (hasScriptSrcAttr && hasUnsafeInline) {
statusHtml =
'<div class="status success">✅ CSP Headers are correctly configured!<br>';
statusHtml += "script-src-attr includes unsafe-inline</div>";
} else {
statusHtml =
'<div class="status error">⚠️ CSP may need adjustment<br>';
statusHtml += "Missing script-src-attr or unsafe-inline</div>";
}
document.getElementById("cspStatus").innerHTML = statusHtml;
})
.catch((error) => {
document.getElementById("cspStatus").innerHTML =
'<div class="status error">❌ Error checking CSP: ' +
error.message +
"</div>";
});
</script>
</body>
</html>

View File

@@ -0,0 +1,257 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Toast Notification Demo</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
padding: 40px;
background: #f8f9fa;
}
.demo-container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
}
.button-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.demo-btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.demo-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-success {
background: #28a745;
color: white;
}
.btn-error {
background: #dc3545;
color: white;
}
.btn-warning {
background: #ffc107;
color: #333;
}
.btn-info {
background: #17a2b8;
color: white;
}
.info-box {
background: #e7f3ff;
border-left: 4px solid #17a2b8;
padding: 20px;
border-radius: 4px;
margin-top: 30px;
}
.info-box h3 {
margin: 0 0 10px 0;
color: #17a2b8;
}
.info-box ul {
margin: 10px 0;
padding-left: 20px;
}
.info-box li {
margin: 5px 0;
}
</style>
</head>
<body>
<div class="demo-container">
<h1>🎉 Custom Toast Notifications Demo</h1>
<p class="subtitle">
Click the buttons below to see the beautiful toast notifications in
action!
</p>
<div class="button-group">
<button class="demo-btn btn-success" onclick="testSuccess()">
<i class="bi bi-check-circle"></i>
Show Success
</button>
<button class="demo-btn btn-error" onclick="testError()">
<i class="bi bi-exclamation-circle"></i>
Show Error
</button>
<button class="demo-btn btn-warning" onclick="testWarning()">
<i class="bi bi-exclamation-triangle"></i>
Show Warning
</button>
<button class="demo-btn btn-info" onclick="testInfo()">
<i class="bi bi-info-circle"></i>
Show Info
</button>
</div>
<button
class="demo-btn"
style="background: #667eea; color: white; width: 100%"
onclick="testMultiple()"
>
<i class="bi bi-stars"></i>
Show Multiple Toasts
</button>
<div class="info-box">
<h3><i class="bi bi-lightbulb"></i> Features</h3>
<ul>
<li>
<strong>Smooth Animations:</strong> Slide-in from right with bounce
effect
</li>
<li>
<strong>Auto Dismiss:</strong> Automatically disappears after 4
seconds
</li>
<li>
<strong>Manual Close:</strong> Click the × button to close
immediately
</li>
<li>
<strong>Multiple Toasts:</strong> Stack multiple notifications
</li>
<li>
<strong>Color Coded:</strong> Different colors for different message
types
</li>
<li><strong>Responsive:</strong> Works great on mobile devices</li>
<li>
<strong>Icon Support:</strong> Bootstrap Icons for visual clarity
</li>
</ul>
</div>
</div>
<script>
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
function showToast(message, type = "info") {
let container = document.getElementById("toastContainer");
if (!container) {
container = document.createElement("div");
container.id = "toastContainer";
container.className = "toast-container";
document.body.appendChild(container);
}
const toast = document.createElement("div");
toast.className = `toast toast-${type} toast-show`;
let icon = "";
if (type === "success") {
icon = '<i class="bi bi-check-circle-fill"></i>';
} else if (type === "error") {
icon = '<i class="bi bi-exclamation-circle-fill"></i>';
} else if (type === "info") {
icon = '<i class="bi bi-info-circle-fill"></i>';
} else if (type === "warning") {
icon = '<i class="bi bi-exclamation-triangle-fill"></i>';
}
toast.innerHTML = `
<div class="toast-icon">${icon}</div>
<div class="toast-message">${escapeHtml(message)}</div>
<button class="toast-close" onclick="this.parentElement.remove()">
<i class="bi bi-x"></i>
</button>
`;
container.appendChild(toast);
setTimeout(() => {
toast.classList.remove("toast-show");
toast.classList.add("toast-hide");
setTimeout(() => {
if (toast.parentElement) {
toast.remove();
}
}, 300);
}, 4000);
}
function testSuccess() {
showToast("3 image(s) added to product gallery", "success");
}
function testError() {
showToast("Failed to upload image. Please try again.", "error");
}
function testWarning() {
showToast("Image size is large. Upload may take longer.", "warning");
}
function testInfo() {
showToast(
"Select images from the media library to add to your product.",
"info"
);
}
function testMultiple() {
showToast("First notification", "info");
setTimeout(() => showToast("Second notification", "success"), 500);
setTimeout(() => showToast("Third notification", "warning"), 1000);
setTimeout(
() => showToast("Multiple toasts stack nicely!", "info"),
1500
);
}
// Show welcome message
setTimeout(() => {
showToast(
"Welcome! Click any button to see toast notifications in action.",
"info"
);
}, 500);
</script>
</body>
</html>