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>

View File

@@ -0,0 +1,79 @@
// Dynamic Menu Loader for Sky Art Shop
// Include this in all public pages to load menu from database
(function () {
"use strict";
// Load and render navigation menu from API
async function loadNavigationMenu() {
try {
const response = await fetch("/api/menu");
const data = await response.json();
if (data.success && data.items && data.items.length > 0) {
renderDesktopMenu(data.items);
renderMobileMenu(data.items);
}
} catch (error) {
console.error("Failed to load menu:", error);
// Keep existing hardcoded menu as fallback
}
}
function renderDesktopMenu(items) {
const desktopMenuList = document.querySelector(".nav-menu-list");
if (!desktopMenuList) return;
desktopMenuList.innerHTML = items
.map(
(item) => `
<li class="nav-item">
<a href="${item.url}" class="nav-link">
${item.icon ? `<i class="bi ${item.icon}"></i> ` : ""}${item.label}
</a>
</li>
`
)
.join("");
// Set active state based on current page
const currentPath = window.location.pathname;
document.querySelectorAll(".nav-link").forEach((link) => {
if (link.getAttribute("href") === currentPath) {
link.classList.add("active");
}
});
}
function renderMobileMenu(items) {
const mobileMenuList = document.querySelector(".mobile-menu-list");
if (!mobileMenuList) return;
mobileMenuList.innerHTML = items
.map(
(item) => `
<li>
<a href="${item.url}" class="mobile-link">
${item.icon ? `<i class="bi ${item.icon}"></i> ` : ""}${item.label}
</a>
</li>
`
)
.join("");
// Set active state for mobile menu
const currentPath = window.location.pathname;
document.querySelectorAll(".mobile-link").forEach((link) => {
if (link.getAttribute("href") === currentPath) {
link.classList.add("active");
}
});
}
// Load menu when DOM is ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadNavigationMenu);
} else {
loadNavigationMenu();
}
})();

View File

@@ -0,0 +1,143 @@
// Smooth Page Transitions for Sky Art Shop
// Provides fade-out/fade-in effects when navigating between pages
(function () {
"use strict";
// Add page transition styles (less aggressive approach)
const style = document.createElement("style");
style.textContent = `
body {
transition: opacity 0.25s ease-in-out;
}
body.page-transitioning {
opacity: 0;
pointer-events: none;
}
`;
document.head.appendChild(style);
// Fade in page on load (if coming from a transition)
function initPageTransition() {
// Check if we're coming from a transition
const isTransitioning = sessionStorage.getItem("page-transitioning");
if (isTransitioning === "true") {
document.body.style.opacity = "0";
sessionStorage.removeItem("page-transitioning");
// Wait for content to be ready, then fade in
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.body.style.opacity = "1";
});
});
}
}
// Handle navigation with transitions
function setupNavigationTransitions() {
// Get all internal links
document.addEventListener("click", function (e) {
const link = e.target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
// Skip if:
// - External link
// - Opens in new tab
// - Has download attribute
// - Is a hash link on same page
// - Is a javascript: link
// - Is a mailto: or tel: link
if (
!href ||
link.target === "_blank" ||
link.hasAttribute("download") ||
href.startsWith("javascript:") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
href.startsWith("#") ||
(href.includes("://") && !href.includes(window.location.host))
) {
return;
}
// Prevent default navigation
e.preventDefault();
// Start transition
document.body.classList.add("page-transitioning");
sessionStorage.setItem("page-transitioning", "true");
// Navigate after fade-out completes
setTimeout(() => {
window.location.href = href;
}, 250); // Match CSS transition duration
});
}
// Use View Transitions API if available (Chrome 111+, Safari 18+)
function setupViewTransitions() {
if (!document.startViewTransition) return;
document.addEventListener(
"click",
function (e) {
const link = e.target.closest("a");
if (!link) return;
const href = link.getAttribute("href");
// Same checks as above
if (
!href ||
link.target === "_blank" ||
link.hasAttribute("download") ||
href.startsWith("javascript:") ||
href.startsWith("mailto:") ||
href.startsWith("tel:") ||
href.startsWith("#") ||
(href.includes("://") && !href.includes(window.location.host))
) {
return;
}
e.preventDefault();
// Use View Transitions API for smooth cross-page transitions
sessionStorage.setItem("page-transitioning", "true");
document.startViewTransition(() => {
window.location.href = href;
});
},
true
); // Use capture to run before other handlers
}
// Initialize
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
initPageTransition();
setupNavigationTransitions();
});
} else {
initPageTransition();
setupNavigationTransitions();
}
// For browsers that support View Transitions API (progressive enhancement)
if ("startViewTransition" in document) {
const viewStyle = document.createElement("style");
viewStyle.textContent = `
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.25s;
}
`;
document.head.appendChild(viewStyle);
}
})();

View File

@@ -149,49 +149,229 @@
<div class="container">
<div class="about-layout">
<div class="about-main-content">
<div class="about-text">
<h2>Our Story</h2>
<p>
Sky Art Shop specializes in scrapbooking, journaling,
cardmaking, and collaging stationery. We are passionate about
helping people express their creativity and preserve their
memories.
</p>
<p>
Our mission is to promote mental health and wellness through
creative art activities. We believe that crafting is more than
just a hobby—it's a therapeutic journey that brings joy,
mindfulness, and self-expression.
</p>
<h2>What We Offer</h2>
<p>Our carefully curated collection includes:</p>
<ul>
<li>Washi tape in various designs and patterns</li>
<li>Unique stickers for journaling and scrapbooking</li>
<li>High-quality journals and notebooks</li>
<li>Card making supplies and kits</li>
<li>Collage materials and ephemera</li>
<li>Creative tools and accessories</li>
</ul>
<h2>Why Choose Us</h2>
<p>
We hand-select every item in our store to ensure the highest
quality and uniqueness. Whether you're a seasoned crafter or
just starting your creative journey, we have something special
for everyone.
</p>
<p>
Join our community of creative minds and let your imagination
soar!
</p>
<div class="about-text" id="aboutContent">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading content...</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Team Members Section -->
<section class="team-section" id="teamSection" style="display: none">
<div class="container">
<div class="team-header">
<h2 class="section-title">Meet Our Team</h2>
<p class="section-subtitle">
The talented people behind Sky Art Shop
</p>
</div>
<div class="team-grid" id="teamMembersGrid">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading team...</p>
</div>
</div>
</div>
</section>
<style>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Team Section Styles */
.team-section {
padding: 80px 0;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
}
.team-header {
text-align: center;
margin-bottom: 60px;
}
.section-title {
font-size: 2.5rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-subtitle {
font-size: 1.125rem;
color: #718096;
max-width: 600px;
margin: 0 auto;
}
.team-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 40px;
max-width: 1200px;
margin: 0 auto;
}
.team-card {
background: white;
border-radius: 20px;
padding: 40px 30px;
text-align: center;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.team-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 5px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
transform: scaleX(0);
transition: transform 0.4s ease;
}
.team-card:hover {
transform: translateY(-10px);
box-shadow: 0 20px 40px rgba(102, 126, 234, 0.25);
}
.team-card:hover::before {
transform: scaleX(1);
}
.team-image-wrapper {
width: 150px;
height: 150px;
margin: 0 auto 25px;
position: relative;
}
.team-image {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
border: 5px solid #667eea;
transition: all 0.4s ease;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8px 20px rgba(102, 126, 234, 0.3);
}
.team-card:hover .team-image {
transform: scale(1.1) rotate(5deg);
border-color: #764ba2;
}
.team-image img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.team-image i {
font-size: 4rem;
color: white;
}
.team-name {
font-size: 1.5rem;
font-weight: 700;
color: #2d3748;
margin-bottom: 8px;
transition: color 0.3s ease;
}
.team-card:hover .team-name {
color: #667eea;
}
.team-position {
font-size: 1.125rem;
color: #667eea;
font-weight: 600;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 0.5px;
font-size: 0.875rem;
}
.team-bio {
font-size: 1rem;
color: #718096;
line-height: 1.7;
margin-bottom: 0;
}
/* Responsive Design */
@media (max-width: 768px) {
.team-section {
padding: 60px 0;
}
.section-title {
font-size: 2rem;
}
.team-grid {
grid-template-columns: 1fr;
gap: 30px;
}
.team-card {
padding: 30px 20px;
}
.team-image-wrapper {
width: 120px;
height: 120px;
}
}
</style>
<footer class="footer">
<div class="container">
<div class="footer-grid">
@@ -245,8 +425,167 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Load about page content from API
async function loadAboutContent() {
try {
const response = await fetch("/api/pages/about");
const data = await response.json();
if (data.success && data.page) {
const contentDiv = document.getElementById("aboutContent");
// Check if content is Quill Delta format (JSON)
if (data.page.content) {
try {
const delta = JSON.parse(data.page.content);
// Convert Delta to HTML
contentDiv.innerHTML = convertDeltaToHTML(delta);
} catch {
// If not JSON, treat as plain HTML
contentDiv.innerHTML = data.page.content;
}
} else {
contentDiv.innerHTML = "<p>Content not available.</p>";
}
// Update meta tags if available
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
} else {
document.getElementById("aboutContent").innerHTML =
"<p>Unable to load content.</p>";
}
} catch (error) {
console.error("Error loading about content:", error);
document.getElementById("aboutContent").innerHTML =
"<p>Error loading content.</p>";
}
}
// Convert Quill Delta to HTML
function convertDeltaToHTML(delta) {
if (!delta || !delta.ops) return "";
let html = "";
let currentBlock = "";
delta.ops.forEach((op) => {
if (typeof op.insert === "string") {
let text = op.insert;
// Apply text formatting
if (op.attributes) {
if (op.attributes.bold) text = `<strong>${text}</strong>`;
if (op.attributes.italic) text = `<em>${text}</em>`;
if (op.attributes.underline) text = `<u>${text}</u>`;
if (op.attributes.strike) text = `<s>${text}</s>`;
if (op.attributes.code) text = `<code>${text}</code>`;
if (op.attributes.link)
text = `<a href="${op.attributes.link}" target="_blank">${text}</a>`;
if (op.attributes.color)
text = `<span style="color: ${op.attributes.color}">${text}</span>`;
if (op.attributes.background)
text = `<span style="background-color: ${op.attributes.background}">${text}</span>`;
}
// Handle line breaks
const lines = text.split("\n");
lines.forEach((line, index) => {
currentBlock += line;
if (index < lines.length - 1) {
// New paragraph
if (currentBlock.trim()) {
html += `<p>${currentBlock}</p>`;
}
currentBlock = "";
}
});
}
});
// Add remaining content
if (currentBlock.trim()) {
html += `<p>${currentBlock}</p>`;
}
return html || "<p>Content not available.</p>";
}
// Load team members
async function loadTeamMembers() {
try {
const response = await fetch("/api/team-members");
const data = await response.json();
if (data.success && data.teamMembers && data.teamMembers.length > 0) {
displayTeamMembers(data.teamMembers);
document.getElementById("teamSection").style.display = "block";
}
} catch (error) {
console.error("Error loading team members:", error);
}
}
// Display team members
function displayTeamMembers(members) {
const grid = document.getElementById("teamMembersGrid");
grid.innerHTML = members
.map(
(member) => `
<div class="team-card">
<div class="team-image-wrapper">
<div class="team-image">
${
member.image_url
? `<img src="${member.image_url}" alt="${escapeHtml(
member.name
)}" />`
: `<i class="bi bi-person-circle"></i>`
}
</div>
</div>
<h3 class="team-name">${escapeHtml(member.name)}</h3>
<div class="team-position">${escapeHtml(member.position)}</div>
${
member.bio
? `<p class="team-bio">${escapeHtml(member.bio)}</p>`
: ""
}
</div>
`
)
.join("");
}
// Escape HTML to prevent XSS
function escapeHtml(text) {
if (!text) return "";
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
// Load content when page loads
document.addEventListener("DOMContentLoaded", function () {
loadAboutContent();
loadTeamMembers();
});
</script>
</body>
</html>

View File

@@ -227,7 +227,9 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>

View File

@@ -174,246 +174,46 @@
</section>
<!-- Business Contact Information -->
<section style="padding: 60px 0 40px; background: white">
<section
style="padding: 60px 0 40px; background: white"
id="contactInfoSection"
>
<div class="container" style="max-width: 1000px">
<div style="text-align: center; margin-bottom: 48px">
<h2
style="
font-size: 2rem;
font-weight: 700;
color: #2d3436;
margin-bottom: 12px;
"
>
Our Contact Information
</h2>
<p style="font-size: 1rem; color: #636e72">
Reach out to us through any of these channels
</p>
</div>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 32px;
"
>
<!-- Phone -->
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
background: #f8f9fa;
padding: 32px 24px;
border-radius: 12px;
text-align: center;
border: 2px solid #e1e8ed;
transition: all 0.3s;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
onmouseover="this.style.borderColor='#667eea'; this.style.transform='translateY(-4px)';"
onmouseout="this.style.borderColor='#e1e8ed'; this.style.transform='translateY(0)';"
>
<div
style="
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
"
>
<i
class="bi bi-telephone"
style="font-size: 28px; color: white"
></i>
</div>
<h3
style="
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: #2d3436;
"
>
Phone
</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">
Give us a call
</p>
<a
href="tel:+1234567890"
style="
color: #667eea;
font-weight: 600;
text-decoration: none;
font-size: 16px;
"
>+1 (234) 567-8900</a
>
</div>
<!-- Email -->
<div
style="
background: #f8f9fa;
padding: 32px 24px;
border-radius: 12px;
text-align: center;
border: 2px solid #e1e8ed;
transition: all 0.3s;
"
onmouseover="this.style.borderColor='#667eea'; this.style.transform='translateY(-4px)';"
onmouseout="this.style.borderColor='#e1e8ed'; this.style.transform='translateY(0)';"
>
<div
style="
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
"
>
<i
class="bi bi-envelope"
style="font-size: 28px; color: white"
></i>
</div>
<h3
style="
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: #2d3436;
"
>
Email
</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">
Send us an email
</p>
<a
href="mailto:support@skyartshop.com"
style="
color: #667eea;
font-weight: 600;
text-decoration: none;
font-size: 16px;
"
>support@skyartshop.com</a
>
</div>
<!-- Location -->
<div
style="
background: #f8f9fa;
padding: 32px 24px;
border-radius: 12px;
text-align: center;
border: 2px solid #e1e8ed;
transition: all 0.3s;
"
onmouseover="this.style.borderColor='#667eea'; this.style.transform='translateY(-4px)';"
onmouseout="this.style.borderColor='#e1e8ed'; this.style.transform='translateY(0)';"
>
<div
style="
width: 64px;
height: 64px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
"
>
<i
class="bi bi-geo-alt"
style="font-size: 28px; color: white"
></i>
</div>
<h3
style="
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
color: #2d3436;
"
>
Location
</h3>
<p style="color: #636e72; margin: 0 0 8px 0; font-size: 15px">
Visit our shop
</p>
<p
style="
color: #667eea;
font-weight: 600;
margin: 0;
font-size: 16px;
line-height: 1.6;
"
>
123 Creative Street<br />Art District, CA 90210
</p>
</div>
</div>
<!-- Business Hours -->
<div
style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
padding: 32px;
margin-top: 40px;
text-align: center;
color: white;
"
>
<h3 style="font-size: 20px; font-weight: 600; margin-bottom: 16px">
Business Hours
</h3>
<div
style="
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
max-width: 600px;
margin: 0 auto;
"
>
<div>
<p style="margin: 0; font-weight: 500; opacity: 0.9">
Monday - Friday
</p>
<p style="margin: 4px 0 0 0; font-size: 18px; font-weight: 600">
9:00 AM - 6:00 PM
</p>
</div>
<div>
<p style="margin: 0; font-weight: 500; opacity: 0.9">Saturday</p>
<p style="margin: 4px 0 0 0; font-size: 18px; font-weight: 600">
10:00 AM - 4:00 PM
</p>
</div>
<div>
<p style="margin: 0; font-weight: 500; opacity: 0.9">Sunday</p>
<p style="margin: 4px 0 0 0; font-size: 18px; font-weight: 600">
Closed
</p>
</div>
</div>
></div>
<p>Loading contact information...</p>
</div>
</div>
</section>
<style>
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Contact card hover effects */
#contactInfoSection [style*="border: 2px solid"]:hover {
border-color: #667eea !important;
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.2);
}
</style>
<!-- Contact Form Section -->
<section
class="contact-section"
@@ -725,7 +525,9 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
@@ -792,6 +594,47 @@
}, 5000);
}
});
// Load contact information from API
async function loadContactInfo() {
try {
const response = await fetch("/api/pages/contact");
const data = await response.json();
if (data.success && data.page) {
const section = document.getElementById("contactInfoSection");
section.innerHTML = `
<div class="container" style="max-width: 1000px">
${data.page.content}
</div>
`;
// Update meta tags
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
}
} catch (error) {
console.error("Error loading contact info:", error);
document.getElementById("contactInfoSection").innerHTML =
'<div class="container"><p style="text-align:center;">Error loading contact information.</p></div>';
}
}
// Load content when page loads
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadContactInfo);
} else {
loadContactInfo();
}
</script>
</body>
</html>

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<rect width="100" height="100" fill="#7c3aed"/>
<text x="50" y="70" font-size="60" font-family="Arial, sans-serif" font-weight="bold" text-anchor="middle" fill="white">S</text>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -145,20 +145,24 @@
</nav>
<!-- Hero Section -->
<section class="hero">
<div class="hero-content">
<h2>Welcome to Sky Art Shop</h2>
<p>Your destination for creative stationery and supplies</p>
<div class="hero-description">
<section class="hero" id="heroSection">
<div class="hero-content" id="heroContent">
<h2 id="heroHeadline">Welcome to Sky Art Shop</h2>
<p id="heroSubheading">
Your destination for creative stationery and supplies
</p>
<div class="hero-description" id="heroDescription">
<p>
Discover our curated collection of scrapbooking, journaling,
cardmaking, and collaging supplies. Express your creativity and
bring your artistic vision to life.
</p>
</div>
<a href="/shop.html" class="btn btn-primary">Shop Now</a>
<a href="/shop.html" class="btn btn-primary" id="heroCtaBtn"
>Shop Now</a
>
</div>
<div class="hero-image">
<div class="hero-image" id="heroImageContainer">
<img
src="/assets/images/hero-image.jpg"
alt="Sky Art Shop"
@@ -168,12 +172,12 @@
</div>
</section>
<!-- Inspiration Section -->
<section class="inspiration">
<!-- Promotion/Inspiration Section -->
<section class="inspiration" id="promotionSection">
<div class="container">
<h2>Get Inspired</h2>
<div class="inspiration-content">
<div class="inspiration-text">
<h2 id="promotionTitle">Get Inspired</h2>
<div class="inspiration-content" id="promotionContent">
<div class="inspiration-text" id="promotionText">
<p>
At Sky Art Shop, we believe in the power of creativity to
transform and inspire. Whether you're an experienced crafter or
@@ -186,7 +190,7 @@
beautiful and meaningful.
</p>
</div>
<div class="inspiration-image">
<div class="inspiration-image" id="promotionImage">
<img
src="/assets/images/inspiration.jpg"
alt="Creative Inspiration"
@@ -199,11 +203,13 @@
</div>
</section>
<!-- Featured Products Section -->
<section class="collection">
<!-- Featured Products / Portfolio Section -->
<section class="collection" id="portfolioSection">
<div class="container">
<h2>Featured Products</h2>
<p class="section-subtitle">Discover our most popular items</p>
<h2 id="portfolioTitle">Featured Products</h2>
<p class="section-subtitle" id="portfolioDescription">
Discover our most popular items
</p>
<div class="products-grid" id="featuredProducts">
<div class="product-card">
<div class="product-image">
@@ -274,9 +280,150 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/cart.js"></script>
<script>
// Load homepage settings
async function loadHomepageSettings() {
try {
const response = await fetch("/api/public/homepage/settings");
if (response.ok) {
const data = await response.json();
if (data.success && data.settings) {
applyHomepageSettings(data.settings);
}
}
} catch (error) {
console.log("Using default homepage settings");
}
}
function applyHomepageSettings(settings) {
// Apply Hero Section
if (settings.hero) {
const heroSection = document.getElementById("heroSection");
const heroContent = document.getElementById("heroContent");
if (!settings.hero.enabled) {
heroSection.style.display = "none";
return;
}
if (settings.hero.headline) {
document.getElementById("heroHeadline").textContent =
settings.hero.headline;
}
if (settings.hero.subheading) {
document.getElementById("heroSubheading").textContent =
settings.hero.subheading;
}
if (settings.hero.description) {
document.getElementById("heroDescription").innerHTML =
settings.hero.description;
}
if (settings.hero.ctaText && settings.hero.ctaLink) {
const ctaBtn = document.getElementById("heroCtaBtn");
ctaBtn.textContent = settings.hero.ctaText;
ctaBtn.href = settings.hero.ctaLink;
}
if (settings.hero.backgroundUrl) {
const isVideo =
settings.hero.backgroundUrl.match(/\.(mp4|webm|ogg)$/i);
const heroImageContainer =
document.getElementById("heroImageContainer");
if (isVideo) {
heroImageContainer.innerHTML = `
<video autoplay muted loop playsinline style="width: 100%; height: 100%; object-fit: cover;">
<source src="${settings.hero.backgroundUrl}" type="video/mp4">
</video>
`;
} else {
heroImageContainer.innerHTML = `<img src="${settings.hero.backgroundUrl}" alt="Hero Background" loading="lazy" />`;
}
}
// Apply layout
if (settings.hero.layout) {
heroContent.style.textAlign = settings.hero.layout.replace(
"text-",
""
);
}
}
// Apply Promotion Section
if (settings.promotion) {
const promotionSection = document.getElementById("promotionSection");
if (!settings.promotion.enabled) {
promotionSection.style.display = "none";
} else {
if (settings.promotion.title) {
document.getElementById("promotionTitle").textContent =
settings.promotion.title;
}
if (settings.promotion.description) {
document.getElementById("promotionText").innerHTML =
settings.promotion.description;
}
if (settings.promotion.imageUrl) {
const promotionImage = document.getElementById("promotionImage");
promotionImage.innerHTML = `<img src="${
settings.promotion.imageUrl
}" alt="${
settings.promotion.title || "Promotion"
}" loading="lazy" />`;
}
// Apply text alignment
if (settings.promotion.textAlignment) {
document.getElementById("promotionText").style.textAlign =
settings.promotion.textAlignment;
}
// Apply image position (you can customize CSS classes for this)
const promotionContent =
document.getElementById("promotionContent");
if (settings.promotion.imagePosition === "right") {
promotionContent.style.flexDirection = "row-reverse";
} else if (settings.promotion.imagePosition === "center") {
promotionContent.style.flexDirection = "column";
}
}
}
// Apply Portfolio Section
if (settings.portfolio) {
const portfolioSection = document.getElementById("portfolioSection");
if (!settings.portfolio.enabled) {
portfolioSection.style.display = "none";
} else {
if (settings.portfolio.title) {
document.getElementById("portfolioTitle").textContent =
settings.portfolio.title;
}
if (settings.portfolio.description) {
const descEl = document.getElementById("portfolioDescription");
if (descEl) {
descEl.innerHTML = settings.portfolio.description;
}
}
// Portfolio count is handled by existing featured products logic
}
}
}
// Load site settings
async function loadSiteSettings() {
try {
@@ -355,8 +502,10 @@
// Initialize
loadSiteSettings();
loadHomepageSettings();
loadFeaturedProducts();
</script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/shopping.js"></script>
</body>
</html>

323
website/public/page.html Normal file
View File

@@ -0,0 +1,323 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title id="pageTitle">Loading... - Sky Art Shop</title>
<meta name="description" id="pageDescription" content="Sky Art Shop" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
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="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<style>
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.page-header {
text-align: center;
margin-bottom: 40px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.page-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: #333;
margin-bottom: 10px;
}
.page-content {
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
line-height: 1.8;
font-size: 1.1rem;
}
.page-content h1,
.page-content h2,
.page-content h3,
.page-content h4,
.page-content h5,
.page-content h6 {
margin-top: 1.5em;
margin-bottom: 0.5em;
font-weight: 600;
color: #333;
}
.page-content h1 {
font-size: 2rem;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 10px;
}
.page-content h2 {
font-size: 1.75rem;
}
.page-content h3 {
font-size: 1.5rem;
}
.page-content p {
margin-bottom: 1.2em;
color: #555;
}
.page-content ul,
.page-content ol {
margin-bottom: 1.5em;
padding-left: 30px;
}
.page-content li {
margin-bottom: 0.5em;
}
.page-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 20px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.page-content blockquote {
border-left: 4px solid #667eea;
padding-left: 20px;
margin: 20px 0;
font-style: italic;
color: #666;
background: #f8f9fa;
padding: 15px 20px;
border-radius: 4px;
}
.page-content a {
color: #667eea;
text-decoration: none;
transition: color 0.3s;
}
.page-content a:hover {
color: #5568d3;
text-decoration: underline;
}
.page-content code {
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
font-size: 0.9em;
}
.page-content pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
margin: 20px 0;
}
.page-content pre code {
background: none;
padding: 0;
color: inherit;
}
.loading-container {
text-align: center;
padding: 100px 20px;
}
.loading-spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.error-container {
text-align: center;
padding: 100px 20px;
}
.error-container i {
font-size: 4rem;
color: #e74c3c;
margin-bottom: 20px;
}
.error-container h2 {
color: #333;
margin-bottom: 10px;
}
.error-container p {
color: #666;
margin-bottom: 30px;
}
</style>
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<a href="/shop.html" class="btn-cart">
<i class="bi bi-cart3"></i>
<span class="cart-count">0</span>
</a>
</div>
</div>
</nav>
<div class="page-container" id="pageContainer">
<div class="loading-container">
<div class="loading-spinner"></div>
<p>Loading page...</p>
</div>
</div>
<!-- Footer -->
<footer class="site-footer">
<div class="footer-content">
<div class="footer-section">
<h4>Sky Art Shop</h4>
<p>
Quality scrapbooking, journaling, and crafting supplies for creative
minds.
</p>
</div>
<div class="footer-section">
<h4>Quick Links</h4>
<ul>
<li><a href="/home.html">Home</a></li>
<li><a href="/shop.html">Shop</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/about.html">About</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-section">
<h4>Follow Us</h4>
<div class="social-links">
<a href="#"><i class="bi bi-facebook"></i></a>
<a href="#"><i class="bi bi-instagram"></i></a>
<a href="#"><i class="bi bi-pinterest"></i></a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</footer>
<script>
// Get slug from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const pageSlug = urlParams.get("slug");
if (!pageSlug) {
showError("No page specified");
} else {
loadPage(pageSlug);
}
async function loadPage(slug) {
try {
const response = await fetch(`/api/pages/${slug}`);
const data = await response.json();
if (data.success && data.page) {
displayPage(data.page);
} else {
showError("Page not found");
}
} catch (error) {
console.error("Failed to load page:", error);
showError("Failed to load page");
}
}
function displayPage(page) {
// Update page title and meta
document.getElementById("pageTitle").textContent =
page.metatitle || page.title + " - Sky Art Shop";
document.getElementById("pageDescription").content =
page.metadescription || page.title;
// Display page content
const container = document.getElementById("pageContainer");
container.innerHTML = `
<div class="page-header">
<h1>${escapeHtml(page.title)}</h1>
</div>
<div class="page-content">
${page.content || "<p>No content available.</p>"}
</div>
`;
}
function showError(message) {
const container = document.getElementById("pageContainer");
container.innerHTML = `
<div class="error-container">
<i class="bi bi-exclamation-triangle"></i>
<h2>Oops! Something went wrong</h2>
<p>${escapeHtml(message)}</p>
<a href="/home.html" class="btn btn-primary">
<i class="bi bi-house"></i> Back to Home
</a>
</div>
`;
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
</script>
</body>
</html>

View File

@@ -233,55 +233,190 @@
</div>
</footer>
<!-- Project Modal -->
<div
id="projectModal"
style="
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 9999;
overflow: hidden;
padding: 0;
"
>
<div
style="
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 900px;
max-height: 90vh;
background: white;
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
display: flex;
flex-direction: column;
"
>
<button
onclick="closeProjectModal()"
style="
position: absolute;
top: 20px;
right: 20px;
background: white;
border: none;
width: 44px;
height: 44px;
border-radius: 50%;
cursor: pointer;
font-size: 20px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
transition: all 0.2s;
"
onmouseover="this.style.transform='scale(1.1)'; this.style.background='#f8f9fa';"
onmouseout="this.style.transform='scale(1)'; this.style.background='white';"
>
<i class="bi bi-x-lg"></i>
</button>
<div
id="modalContent"
style="
overflow-y: auto;
overflow-x: hidden;
flex: 1;
scroll-behavior: smooth;
"
></div>
</div>
</div>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
let portfolioProjects = [];
// Open project modal
function openProjectModal(projectId) {
const project = portfolioProjects.find((p) => p.id === projectId);
if (!project) return;
const modal = document.getElementById("projectModal");
const modalContent = document.getElementById("modalContent");
modalContent.innerHTML = `
<div class="project-image" style="width: 100%; height: 450px; overflow: hidden; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); flex-shrink: 0;">
<img src="${project.imageurl || "/assets/images/placeholder.jpg"}"
alt="${project.title}"
style="width: 100%; height: 100%; object-fit: cover;" />
</div>
<div style="padding: 40px; background: white;">
${
project.category
? `<span style="display: inline-block; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 8px 18px; border-radius: 24px; font-size: 13px; font-weight: 600; margin-bottom: 24px; letter-spacing: 0.5px; text-transform: uppercase;">${project.category}</span>`
: ""
}
<h2 style="font-size: 36px; font-weight: 700; margin: 0 0 24px 0; color: #1a1a1a; line-height: 1.2;">${
project.title
}</h2>
<div style="color: #555; font-size: 17px; line-height: 1.9; margin-bottom: 32px; font-weight: 400;">
${project.description || "No description available."}
</div>
<div style="padding-top: 24px; border-top: 2px solid #f0f0f0; color: #888; font-size: 15px; display: flex; align-items: center; gap: 8px;">
<i class="bi bi-calendar3" style="font-size: 18px;"></i>
<span style="font-weight: 500;">Created on ${new Date(
project.createdat
).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}</span>
</div>
</div>
`;
modal.style.display = "block";
modalContent.scrollTop = 0;
document.body.style.overflow = "hidden";
}
// Close project modal
function closeProjectModal() {
document.getElementById("projectModal").style.display = "none";
document.body.style.overflow = "auto";
}
// Close modal on outside click
document.addEventListener("click", (e) => {
const modal = document.getElementById("projectModal");
if (e.target === modal) {
closeProjectModal();
}
});
// Close modal on Escape key
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
closeProjectModal();
}
});
// Load portfolio projects from API
async function loadPortfolio() {
try {
const response = await fetch("/api/portfolio/projects");
if (response.ok) {
const data = await response.json();
const projects = data.projects || [];
portfolioProjects = data.projects || [];
document.getElementById("loadingMessage").style.display = "none";
if (projects.length === 0) {
if (portfolioProjects.length === 0) {
document.getElementById("noProjects").style.display = "block";
return;
}
const grid = document.getElementById("portfolioGrid");
grid.innerHTML = projects
grid.innerHTML = portfolioProjects
.map(
(project) => `
<div class="product-card" style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.3s;">
<div class="product-card" onclick="openProjectModal('${
project.id
}')" style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: all 0.3s; cursor: pointer;">
<div class="product-image" style="position: relative; padding-top: 100%; overflow: hidden; background: #f5f5f5;">
<img src="${
project.imageurl || "/assets/images/placeholder.jpg"
}"
alt="${project.title}"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;"
loading="lazy" />
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; transition: transform 0.3s;"
loading="lazy"
onmouseover="this.style.transform='scale(1.05)'"
onmouseout="this.style.transform='scale(1)'" />
${
project.category
? `<span style="position: absolute; top: 10px; right: 10px; background: rgba(102, 126, 234, 0.9); color: white; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 500;">${project.category}</span>`
: ""
}
</div>
<div style="padding: 20px;">
<h3 style="font-size: 18px; font-weight: 600; margin-bottom: 10px; color: #333;">${
<div style="padding: 20px; text-align: center;">
<h3 style="font-size: 18px; font-weight: 600; margin: 0; color: #333;">${
project.title
}</h3>
<p style="color: #666; font-size: 14px; line-height: 1.6; margin: 0;">${
project.description || ""
}</p>
<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; color: #999; font-size: 12px;">
<i class="bi bi-calendar"></i> ${new Date(
project.createdat
).toLocaleDateString()}
</div>
</div>
</div>
`

334
website/public/privacy.html Normal file
View File

@@ -0,0 +1,334 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Privacy Policy - Sky Art Shop</title>
<meta name="description" content="Sky Art Shop Privacy Policy" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
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="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<style>
.privacy-hero {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 80px 0 60px;
color: white;
text-align: center;
}
.privacy-hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
font-weight: 700;
}
.privacy-hero p {
font-size: 1.1rem;
color: rgba(255, 255, 255, 0.9);
max-width: 600px;
margin: 0 auto;
}
.privacy-content {
padding: 60px 0;
background: white;
}
.privacy-text {
max-width: 900px;
margin: 0 auto;
background: #fff;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
line-height: 1.8;
}
.privacy-text h2 {
color: #333;
margin-top: 30px;
margin-bottom: 15px;
font-weight: 600;
}
.privacy-text h3 {
color: #555;
margin-top: 25px;
margin-bottom: 12px;
font-weight: 600;
}
.privacy-text p {
color: #666;
margin-bottom: 15px;
}
.privacy-text ul {
margin-bottom: 20px;
padding-left: 30px;
}
.privacy-text li {
margin-bottom: 8px;
color: #666;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<div class="action-item wishlist-dropdown-wrapper">
<button
class="action-btn"
id="wishlistToggle"
aria-label="Wishlist"
>
<i class="bi bi-heart"></i>
<span class="action-badge" id="wishlistCount">0</span>
</button>
<div class="action-dropdown wishlist-dropdown" id="wishlistPanel">
<div class="dropdown-head">
<h3>My Wishlist</h3>
<button class="dropdown-close" id="wishlistClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="wishlistContent">
<p class="empty-state">Your wishlist is empty</p>
</div>
<div class="dropdown-foot">
<a href="/shop.html" class="btn-outline">Continue Shopping</a>
</div>
</div>
</div>
<div class="action-item cart-dropdown-wrapper">
<button
class="action-btn"
id="cartToggle"
aria-label="Shopping Cart"
>
<i class="bi bi-cart3"></i>
<span class="action-badge" id="cartCount">0</span>
</button>
<div class="action-dropdown cart-dropdown" id="cartPanel">
<div class="dropdown-head">
<h3>Shopping Cart</h3>
<button class="dropdown-close" id="cartClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="cartContent">
<p class="empty-state">Your cart is empty</p>
</div>
<div class="dropdown-foot">
<div class="cart-summary">
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
<button class="mobile-toggle" id="mobileMenuToggle" aria-label="Menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
<span class="toggle-line"></span>
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
<li><a href="/shop.html" class="mobile-link">Shop</a></li>
<li><a href="/portfolio.html" class="mobile-link">Portfolio</a></li>
<li><a href="/about.html" class="mobile-link">About</a></li>
<li><a href="/blog.html" class="mobile-link">Blog</a></li>
<li><a href="/contact.html" class="mobile-link">Contact</a></li>
</ul>
</div>
</nav>
<section class="privacy-hero">
<div class="container">
<h1>Privacy Policy</h1>
<p>Your privacy is important to us</p>
</div>
</section>
<section class="privacy-content">
<div class="container">
<div class="privacy-text" id="privacyContent">
<div style="text-align: center; padding: 40px">
<div
class="loading-spinner"
style="
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
"
></div>
<p>Loading privacy policy...</p>
</div>
</div>
</div>
</section>
<footer class="footer">
<div class="container">
<div class="footer-grid">
<div class="footer-col">
<h3 class="footer-title">Sky Art Shop</h3>
<p class="footer-text">
Your destination for unique art pieces and creative supplies.
</p>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
<a href="#" class="social-link"
><i class="bi bi-instagram"></i
></a>
<a href="#" class="social-link"><i class="bi bi-twitter"></i></a>
<a href="#" class="social-link"
><i class="bi bi-pinterest"></i
></a>
</div>
</div>
<div class="footer-col">
<h4 class="footer-heading">Shop</h4>
<ul class="footer-links">
<li><a href="/shop.html">All Products</a></li>
<li><a href="/shop.html?category=paintings">Paintings</a></li>
<li><a href="/shop.html?category=prints">Prints</a></li>
<li><a href="/shop.html?category=supplies">Art Supplies</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">About</h4>
<ul class="footer-links">
<li><a href="/about.html">Our Story</a></li>
<li><a href="/portfolio.html">Portfolio</a></li>
<li><a href="/blog.html">Blog</a></li>
<li><a href="/contact.html">Contact</a></li>
</ul>
</div>
<div class="footer-col">
<h4 class="footer-heading">Customer Service</h4>
<ul class="footer-links">
<li><a href="#">Shipping Info</a></li>
<li><a href="#">Returns</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="/privacy.html">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 Sky Art Shop. All rights reserved.</p>
</div>
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Load privacy policy content from API
async function loadPrivacyContent() {
try {
const response = await fetch("/api/pages/privacy");
const data = await response.json();
if (data.success && data.page) {
const contentDiv = document.getElementById("privacyContent");
contentDiv.innerHTML =
data.page.content || "<p>Content not available.</p>";
// Update meta tags if available
if (data.page.metatitle) {
document.title = data.page.metatitle;
}
if (data.page.metadescription) {
const metaDesc = document.querySelector(
'meta[name="description"]'
);
if (metaDesc) {
metaDesc.content = data.page.metadescription;
}
}
} else {
document.getElementById("privacyContent").innerHTML =
"<p>Unable to load content.</p>";
}
} catch (error) {
console.error("Error loading privacy content:", error);
document.getElementById("privacyContent").innerHTML =
"<p>Error loading content.</p>";
}
}
// Load content when page loads
document.addEventListener("DOMContentLoaded", loadPrivacyContent);
</script>
</body>
</html>

View File

@@ -1,50 +1,78 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Details - Sky Art Shop</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap" 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="/assets/css/main.css" />
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Details - Sky Art Shop</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;600;700&display=swap"
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="/assets/css/main.css" />
<link rel="stylesheet" href="/assets/css/navbar.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
<link rel="stylesheet" href="/assets/css/shopping.css" />
</head>
<link rel="stylesheet" href="/assets/css/shopping.css" />
</head>
<body>
<!-- Modern Navigation -->
<nav class="modern-navbar">
<div class="navbar-wrapper">
<div class="navbar-brand">
<a href="/home.html" class="brand-link">
<img src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg" alt="Sky Art Shop Logo" class="brand-logo" />
<img
src="/uploads/images/8ba675b9-c4e6-41e6-8f14-382b9ee1d019.jpg"
alt="Sky Art Shop Logo"
class="brand-logo"
/>
<span class="brand-name">Sky Art Shop</span>
</a>
</div>
<div class="navbar-menu">
<ul class="nav-menu-list">
<li class="nav-item"><a href="/home.html" class="nav-link">Home</a></li>
<li class="nav-item"><a href="/shop.html" class="nav-link">Shop</a></li>
<li class="nav-item"><a href="/portfolio.html" class="nav-link">Portfolio</a></li>
<li class="nav-item"><a href="/about.html" class="nav-link">About</a></li>
<li class="nav-item"><a href="/blog.html" class="nav-link">Blog</a></li>
<li class="nav-item"><a href="/contact.html" class="nav-link">Contact</a></li>
<li class="nav-item">
<a href="/home.html" class="nav-link">Home</a>
</li>
<li class="nav-item">
<a href="/shop.html" class="nav-link">Shop</a>
</li>
<li class="nav-item">
<a href="/portfolio.html" class="nav-link">Portfolio</a>
</li>
<li class="nav-item">
<a href="/about.html" class="nav-link">About</a>
</li>
<li class="nav-item">
<a href="/blog.html" class="nav-link">Blog</a>
</li>
<li class="nav-item">
<a href="/contact.html" class="nav-link">Contact</a>
</li>
</ul>
</div>
<div class="navbar-actions">
<div class="action-item wishlist-dropdown-wrapper">
<button class="action-btn" id="wishlistToggle" aria-label="Wishlist">
<button
class="action-btn"
id="wishlistToggle"
aria-label="Wishlist"
>
<i class="bi bi-heart"></i>
<span class="action-badge" id="wishlistCount">0</span>
</button>
<div class="action-dropdown wishlist-dropdown" id="wishlistPanel">
<div class="dropdown-head">
<h3>My Wishlist</h3>
<button class="dropdown-close" id="wishlistClose"><i class="bi bi-x-lg"></i></button>
<button class="dropdown-close" id="wishlistClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="wishlistContent">
<p class="empty-state">Your wishlist is empty</p>
@@ -54,16 +82,22 @@
</div>
</div>
</div>
<div class="action-item cart-dropdown-wrapper">
<button class="action-btn" id="cartToggle" aria-label="Shopping Cart">
<button
class="action-btn"
id="cartToggle"
aria-label="Shopping Cart"
>
<i class="bi bi-cart3"></i>
<span class="action-badge" id="cartCount">0</span>
</button>
<div class="action-dropdown cart-dropdown" id="cartPanel">
<div class="dropdown-head">
<h3>Shopping Cart</h3>
<button class="dropdown-close" id="cartClose"><i class="bi bi-x-lg"></i></button>
<button class="dropdown-close" id="cartClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<div class="dropdown-body" id="cartContent">
<p class="empty-state">Your cart is empty</p>
@@ -73,12 +107,14 @@
<span class="summary-label">Subtotal:</span>
<span class="summary-value" id="cartSubtotal">$0.00</span>
</div>
<a href="/checkout.html" class="btn-primary-full">Proceed to Checkout</a>
<a href="/checkout.html" class="btn-primary-full"
>Proceed to Checkout</a
>
<a href="/shop.html" class="btn-text">Continue Shopping</a>
</div>
</div>
</div>
<button class="mobile-toggle" id="mobileMenuToggle" aria-label="Menu">
<span class="toggle-line"></span>
<span class="toggle-line"></span>
@@ -86,11 +122,13 @@
</button>
</div>
</div>
<div class="mobile-menu" id="mobileMenu">
<div class="mobile-menu-header">
<span class="mobile-brand">Sky Art Shop</span>
<button class="mobile-close" id="mobileMenuClose"><i class="bi bi-x-lg"></i></button>
<button class="mobile-close" id="mobileMenuClose">
<i class="bi bi-x-lg"></i>
</button>
</div>
<ul class="mobile-menu-list">
<li><a href="/home.html" class="mobile-link">Home</a></li>
@@ -102,36 +140,193 @@
</ul>
</div>
</nav>
<div id="loading" style="text-align: center; padding: 100px 20px; font-size: 18px; color: #6b7280;">
<i class="bi bi-hourglass-split" style="font-size: 48px; display: block; margin-bottom: 20px;"></i>
Loading product...
</div>
<div id="productDetail" style="display: none;"></div>
<script src="/assets/js/shopping.js"></script>
<script>
async function loadProduct() {
const params = new URLSearchParams(window.location.search);
const productId = params.get('id');
if (!productId) {
document.getElementById('loading').innerHTML = '<p>Product not found</p><a href="/shop.html">Back to Shop</a>';
return;
}
try {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
if (!data.success || !data.product) {
throw new Error('Product not found');
<div
id="loading"
style="
text-align: center;
padding: 100px 20px;
font-size: 18px;
color: #6b7280;
"
>
<i
class="bi bi-hourglass-split"
style="font-size: 48px; display: block; margin-bottom: 20px"
></i>
Loading product...
</div>
<div id="productDetail" style="display: none"></div>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script src="/assets/js/shopping.js"></script>
<script>
// Function to change primary image
function changePrimaryImage(imageUrl) {
const primaryImg = document.getElementById("primaryImage");
if (primaryImg) {
primaryImg.src = imageUrl;
}
const product = data.product;
document.title = `${product.name} - Sky Art Shop`;
document.getElementById('productDetail').innerHTML = `
// Update gallery thumbnails border
const galleryImages = document.querySelectorAll(
'[onclick^="changePrimaryImage"]'
);
galleryImages.forEach((img) => {
if (img.src.includes(imageUrl)) {
img.style.border = "3px solid #6b46c1";
} else {
img.style.border = "1px solid #e5e7eb";
}
});
}
async function loadProduct() {
const params = new URLSearchParams(window.location.search);
const productId = params.get("id");
if (!productId) {
document.getElementById("loading").innerHTML =
'<p>Product not found</p><a href="/shop.html">Back to Shop</a>';
return;
}
try {
const response = await fetch(`/api/products/${productId}`);
const data = await response.json();
if (!data.success || !data.product) {
throw new Error("Product not found");
}
const product = data.product;
document.title = `${product.name} - Sky Art Shop`;
// Get primary image or first image from images array
let primaryImage = "/assets/images/placeholder.jpg";
let imageGallery = [];
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
// Find primary image
const primary = product.images.find((img) => img.is_primary);
if (primary) {
primaryImage = primary.image_url;
} else {
primaryImage = product.images[0].image_url;
}
imageGallery = product.images;
}
// Build image gallery HTML
let galleryHTML = "";
if (imageGallery.length > 0) {
galleryHTML = `
<div style="display: flex; gap: 12px; margin-top: 16px; overflow-x: auto; padding: 8px 0;">
${imageGallery
.map(
(img, idx) => `
<img src="${img.image_url}"
alt="${img.alt_text || product.name}"
onclick="changePrimaryImage('${img.image_url}')"
style="width: 80px; height: 80px; object-fit: cover; border-radius: 8px; cursor: pointer; border: ${
img.image_url === primaryImage
? "3px solid #6b46c1"
: "1px solid #e5e7eb"
};"
onerror="this.src='/assets/images/placeholder.jpg'" />
`
)
.join("")}
</div>
`;
}
// Build product details HTML
let detailsHTML = "";
if (
product.sku ||
product.weight ||
product.dimensions ||
product.material
) {
detailsHTML = `
<div style="margin-bottom: 24px; padding: 20px; background: #f9fafb; border-radius: 8px;">
<h3 style="font-size: 16px; font-weight: 600; color: #1a1a1a; margin-bottom: 16px;">Product Details</h3>
${
product.sku
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<span style="font-weight: 500;">SKU:</span> ${product.sku}
</p>
`
: ""
}
${
product.weight
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<span style="font-weight: 500;">Weight:</span> ${product.weight}
</p>
`
: ""
}
${
product.dimensions
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<span style="font-weight: 500;">Dimensions:</span> ${product.dimensions}
</p>
`
: ""
}
${
product.material
? `
<p style="margin-bottom: 8px; color: #6b7280;">
<span style="font-weight: 500;">Material:</span> ${product.material}
</p>
`
: ""
}
</div>
`;
}
// Build badges HTML
let badgesHTML = "";
if (product.isfeatured || product.isbestseller) {
badgesHTML = `
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
${
product.isfeatured
? `
<span style="display: inline-block; padding: 6px 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 6px; font-size: 12px; font-weight: 600;">
<i class="bi bi-star-fill"></i> Featured
</span>
`
: ""
}
${
product.isbestseller
? `
<span style="display: inline-block; padding: 6px 12px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; border-radius: 6px; font-size: 12px; font-weight: 600;">
<i class="bi bi-trophy-fill"></i> Best Seller
</span>
`
: ""
}
</div>
`;
}
document.getElementById("productDetail").innerHTML = `
<div style="font-family: 'Roboto', sans-serif;">
<nav style="background: white; padding: 16px 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.1);">
<div style="max-width: 1200px; margin: 0 auto; display: flex; align-items: center; gap: 20px;">
@@ -147,56 +342,106 @@
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 60px; margin-bottom: 60px;">
<div>
<div style="background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<img src="${product.imageurl || '/assets/images/placeholder.jpg'}"
<img id="primaryImage"
src="${primaryImage}"
alt="${product.name}"
style="width: 100%; height: auto; display: block;"
onerror="this.src='/assets/images/placeholder.jpg'" />
</div>
${galleryHTML}
${
imageGallery.length > 0 &&
imageGallery.some((img) => img.color_variant)
? `
<div style="margin-top: 16px;">
<p style="font-size: 14px; font-weight: 500; color: #6b7280; margin-bottom: 8px;">Available Colors:</p>
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
${imageGallery
.filter((img) => img.color_variant)
.map(
(img) => `
<span style="display: inline-block; padding: 6px 12px; background: #f3f4f6; border-radius: 6px; font-size: 13px; color: #1a1a1a;">
${img.color_variant}
</span>
`
)
.join("")}
</div>
</div>
`
: ""
}
</div>
<div style="padding: 20px 0;">
<h1 style="font-size: 36px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0; line-height: 1.2;">${product.name}</h1>
${badgesHTML}
<h1 style="font-size: 36px; font-weight: 700; color: #1a1a1a; margin: 0 0 16px 0; line-height: 1.2;">${
product.name
}</h1>
<div style="display: flex; align-items: baseline; gap: 16px; margin-bottom: 24px;">
<p style="font-size: 36px; font-weight: 700; color: #6b46c1; margin: 0;">$${parseFloat(product.price).toFixed(2)}</p>
${product.stockquantity > 0 ?
`<span style="color: #10b981; font-weight: 500;">In Stock (${product.stockquantity} available)</span>` :
`<span style="color: #ef4444; font-weight: 500;">Out of Stock</span>`
<p style="font-size: 36px; font-weight: 700; color: #6b46c1; margin: 0;">$${parseFloat(
product.price
).toFixed(2)}</p>
${
product.stockquantity > 0
? `<span style="color: #10b981; font-weight: 500;">In Stock (${product.stockquantity} available)</span>`
: `<span style="color: #ef4444; font-weight: 500;">Out of Stock</span>`
}
</div>
${product.shortdescription ? `
${
product.shortdescription
? `
<p style="font-size: 18px; color: #4b5563; line-height: 1.6; margin-bottom: 24px;">${product.shortdescription}</p>
` : ''}
`
: ""
}
${product.description ? `
<div style="margin-bottom: 32px;">
${
product.description
? `
<div style="margin-bottom: 24px;">
<h3 style="font-size: 18px; font-weight: 600; color: #1a1a1a; margin-bottom: 12px;">Description</h3>
<p style="color: #6b7280; line-height: 1.7;">${product.description}</p>
<div style="color: #6b7280; line-height: 1.7;">${product.description}</div>
</div>
` : ''}
`
: ""
}
${product.category ? `
${
product.category
? `
<p style="margin-bottom: 16px;">
<span style="font-weight: 500; color: #6b7280;">Category:</span>
<span style="display: inline-block; margin-left: 8px; padding: 4px 12px; background: #f3f4f6; border-radius: 6px; font-size: 14px;">${product.category}</span>
</p>
` : ''}
`
: ""
}
${product.color ? `
<p style="margin-bottom: 24px;">
<span style="font-weight: 500; color: #6b7280;">Color:</span>
<span style="margin-left: 8px;">${product.color}</span>
</p>
` : ''}
${detailsHTML}
<div style="display: flex; gap: 12px; margin-top: 32px;">
<button onclick="addToCart()"
style="flex: 1; padding: 16px 32px; background: #6b46c1; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px;"
onmouseover="this.style.background='#5936a3'"
onmouseout="this.style.background='#6b46c1'">
${product.stockquantity <= 0 ? "disabled" : ""}
style="flex: 1; padding: 16px 32px; background: ${
product.stockquantity <= 0 ? "#9ca3af" : "#6b46c1"
}; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: ${
product.stockquantity <= 0 ? "not-allowed" : "pointer"
}; transition: background 0.2s; display: flex; align-items: center; justify-content: center; gap: 8px;"
onmouseover="if(${
product.stockquantity > 0
}) this.style.background='#5936a3'"
onmouseout="if(${
product.stockquantity > 0
}) this.style.background='#6b46c1'">
<i class="bi bi-cart-plus" style="font-size: 20px;"></i>
Add to Cart
${
product.stockquantity <= 0
? "Out of Stock"
: "Add to Cart"
}
</button>
<button onclick="addToWishlist()"
style="width: 56px; padding: 16px; background: transparent; color: #6b46c1; border: 2px solid #6b46c1; border-radius: 8px; font-size: 20px; cursor: pointer; transition: all 0.2s;"
@@ -214,32 +459,32 @@
</div>
</div>
`;
document.getElementById('loading').style.display = 'none';
document.getElementById('productDetail').style.display = 'block';
// Store product data
window.currentProduct = product;
} catch (error) {
console.error('Error loading product:', error);
document.getElementById('loading').innerHTML = '<p style="color: #ef4444;">Error loading product</p><a href="/shop.html" style="color: #6b46c1; text-decoration: none; font-weight: 500;">Back to Shop</a>';
document.getElementById("loading").style.display = "none";
document.getElementById("productDetail").style.display = "block";
// Store product data
window.currentProduct = product;
} catch (error) {
console.error("Error loading product:", error);
document.getElementById("loading").innerHTML =
'<p style="color: #ef4444;">Error loading product</p><a href="/shop.html" style="color: #6b46c1; text-decoration: none; font-weight: 500;">Back to Shop</a>';
}
}
}
function addToCart() {
if (window.currentProduct && window.shoppingManager) {
shoppingManager.addToCart(window.currentProduct, 1);
function addToCart() {
if (window.currentProduct && window.shoppingManager) {
shoppingManager.addToCart(window.currentProduct, 1);
}
}
}
function addToWishlist() {
if (window.currentProduct && window.shoppingManager) {
shoppingManager.addToWishlist(window.currentProduct);
function addToWishlist() {
if (window.currentProduct && window.shoppingManager) {
shoppingManager.addToWishlist(window.currentProduct);
}
}
}
loadProduct();
</script>
</body>
loadProduct();
</script>
</body>
</html>

View File

@@ -216,6 +216,7 @@
border-radius: 12px;
overflow: hidden;
transition: all 0.3s;
position: relative;
}
.product-card:hover {
@@ -223,6 +224,42 @@
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
.product-badges {
position: absolute;
top: 12px;
left: 12px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 6px;
}
.badge-featured {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.badge-bestseller {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 6px 10px;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.product-link {
display: block;
text-decoration: none;
@@ -636,7 +673,9 @@
</div>
</footer>
<script src="/assets/js/page-transitions.js"></script>
<script src="/assets/js/main.js"></script>
<script src="/assets/js/navigation.js"></script>
<script src="/assets/js/cart.js"></script>
<script>
// Mobile Menu Toggle (Same as other pages)
@@ -693,58 +732,90 @@
noProducts.style.display = "none";
grid.innerHTML = products
.map(
(product) => `
.map((product) => {
// Get the primary image from images array
let productImage = "/assets/images/placeholder.jpg";
if (
product.images &&
Array.isArray(product.images) &&
product.images.length > 0
) {
// Find primary image or use first one
const primaryImg = product.images.find((img) => img.is_primary);
productImage = primaryImg
? primaryImg.image_url
: product.images[0].image_url;
} else if (product.imageurl) {
// Fallback to old imageurl field
productImage = product.imageurl;
}
// Build badges HTML
let badges = "";
if (product.isfeatured) {
badges +=
'<span class="badge-featured"><i class="bi bi-star-fill"></i> Featured</span>';
}
if (product.isbestseller) {
badges +=
'<span class="badge-bestseller"><i class="bi bi-trophy-fill"></i> Best Seller</span>';
}
return `
<div class="product-card">
<a href="/product.html?id=${
product.productid || product.id
}" class="product-link">
${badges ? `<div class="product-badges">${badges}</div>` : ""}
<a href="/product.html?id=${product.id}" class="product-link">
<div class="product-image">
<img src="${
product.imageurl || "/assets/images/placeholder.jpg"
}" alt="${
<img src="${productImage}" alt="${
product.name
}" loading="lazy" onerror="this.src='/assets/images/placeholder.jpg'" />
</div>
<h3>${product.name}</h3>
${
product.color
? `<span class="product-color-badge">${product.color}</span>`
: ""
}
${
product.shortdescription || product.description
? `<div class="product-description">${
product.shortdescription ||
product.description.substring(0, 100) + "..."
(product.description
? product.description.substring(0, 100) + "..."
: "")
}</div>`
: ""
}
<p class="price">$${parseFloat(product.price).toFixed(2)}</p>
${
product.stockquantity <= 0
? '<p style="color: #ef4444; font-size: 12px; margin: 8px 16px;">Out of Stock</p>'
: ""
}
</a>
<div style="display: flex; gap: 0.5rem; margin-top: 0.5rem;">
<div style="display: flex; gap: 0.5rem; margin: 0 16px 16px; padding-top: 8px;">
<button class="btn btn-small btn-icon"
onclick="addToWishlist('${
product.productid || product.id
onclick="event.stopPropagation(); addToWishlist('${
product.id
}', '${product.name.replace(/'/g, "\\'")}', ${
product.price
}, '${product.imageurl}')"
}, '${productImage.replace(/'/g, "\\'")}')"
aria-label="Add to wishlist">
<i class="bi bi-heart"></i>
</button>
<button class="btn btn-small btn-icon"
onclick="addToCart('${
product.productid || product.id
onclick="event.stopPropagation(); addToCart('${
product.id
}', '${product.name.replace(/'/g, "\\'")}', ${
product.price
}, '${product.imageurl}')"
aria-label="Add to cart">
}, '${productImage.replace(/'/g, "\\'")}')"
aria-label="Add to cart"
${
product.stockquantity <= 0
? 'disabled style="opacity: 0.5; cursor: not-allowed;"'
: ""
}>
<i class="bi bi-cart-plus"></i>
</button>
</div>
</div>
`
)
`;
})
.join("");
}

View File

@@ -0,0 +1,269 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Pages 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: 40px;
background: #f8f9fa;
}
.test-card {
background: white;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.test-result {
padding: 15px;
border-radius: 6px;
margin-top: 15px;
font-family: monospace;
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.page-list {
list-style: none;
padding: 0;
}
.page-list li {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.page-list li:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">
<i class="bi bi-clipboard-check"></i> Custom Pages System Test
</h1>
<div class="test-card">
<h3><i class="bi bi-list-ul"></i> Available Custom Pages</h3>
<p class="text-muted">
These pages are published and visible on the frontend:
</p>
<ul class="page-list" id="pagesList">
<li class="text-center"><em>Loading...</em></li>
</ul>
</div>
<div class="test-card">
<h3><i class="bi bi-link-45deg"></i> Quick Links</h3>
<div class="d-grid gap-2">
<a href="/admin/pages.html" class="btn btn-primary" target="_blank">
<i class="bi bi-gear"></i> Open Admin Pages Manager
</a>
<button class="btn btn-success" onclick="createTestPage()">
<i class="bi bi-plus-circle"></i> Create Test Page
</button>
<button class="btn btn-info" onclick="loadPages()">
<i class="bi bi-arrow-clockwise"></i> Refresh Page List
</button>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-terminal"></i> API Response</h3>
<div
id="apiResponse"
class="test-result success"
style="display: none"
></div>
</div>
</div>
<script>
let pagesData = [];
document.addEventListener("DOMContentLoaded", function () {
loadPages();
});
async function loadPages() {
try {
const response = await fetch("/api/pages");
const data = await response.json();
if (data.success && data.pages) {
pagesData = data.pages;
displayPages(data.pages);
showResult(
"API Response: " + JSON.stringify(data, null, 2),
"success"
);
} else {
showResult(
"Failed to load pages: " + JSON.stringify(data),
"error"
);
}
} catch (error) {
showResult("Error loading pages: " + error.message, "error");
}
}
function displayPages(pages) {
const list = document.getElementById("pagesList");
if (pages.length === 0) {
list.innerHTML =
'<li class="text-center text-muted"><em>No published pages found</em></li>';
return;
}
list.innerHTML = pages
.map(
(page) => `
<li>
<div>
<strong>${escapeHtml(page.title)}</strong>
<br>
<small class="text-muted">Slug: ${escapeHtml(
page.slug
)} | Created: ${new Date(
page.createdat
).toLocaleDateString()}</small>
</div>
<div>
<a href="/page.html?slug=${encodeURIComponent(
page.slug
)}" class="btn btn-sm btn-outline-primary" target="_blank">
<i class="bi bi-eye"></i> View
</a>
</div>
</li>
`
)
.join("");
}
async function createTestPage() {
const title = "Test Page " + Date.now();
const slug = "test-page-" + Date.now();
const testContent = {
ops: [
{ insert: "Welcome to the Test Page", attributes: { header: 1 } },
{ insert: "\n\nThis is a test page created automatically. " },
{
insert: "It contains formatted text",
attributes: { bold: true },
},
{ insert: " with " },
{ insert: "different styles", attributes: { italic: true } },
{ insert: ".\n\n" },
{ insert: "Key Features:", attributes: { header: 2 } },
{ insert: "\n" },
{
insert: "Rich text editing with Quill",
attributes: { list: "bullet" },
},
{ insert: "\n" },
{ insert: "Create and edit pages", attributes: { list: "bullet" } },
{ insert: "\n" },
{ insert: "Delete pages", attributes: { list: "bullet" } },
{ insert: "\n" },
{ insert: "Display on frontend", attributes: { list: "bullet" } },
{ insert: "\n" },
],
};
const testHTML = `
<h1>Welcome to the Test Page</h1>
<p>This is a test page created automatically. <strong>It contains formatted text</strong> with <em>different styles</em>.</p>
<h2>Key Features:</h2>
<ul>
<li>Rich text editing with Quill</li>
<li>Create and edit pages</li>
<li>Delete pages</li>
<li>Display on frontend</li>
</ul>
`;
try {
// Note: This will fail without authentication
const response = await fetch("/api/admin/pages", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
title: title,
slug: slug,
content: JSON.stringify(testContent),
contenthtml: testHTML,
metatitle: title,
metadescription: "This is a test page",
ispublished: true,
}),
});
const data = await response.json();
if (data.success) {
showResult(
"Test page created successfully! ID: " + data.page.id,
"success"
);
loadPages();
} else {
showResult(
"Failed to create test page. You may need to be logged in as admin. Error: " +
(data.message || "Unknown error"),
"error"
);
}
} catch (error) {
showResult(
"Error creating test page: " +
error.message +
". Make sure you are logged in as admin.",
"error"
);
}
}
function showResult(message, type) {
const result = document.getElementById("apiResponse");
result.textContent = message;
result.className = "test-result " + type;
result.style.display = "block";
}
function escapeHtml(text) {
const map = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#039;",
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
</script>
</body>
</html>

View File

@@ -0,0 +1,249 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Backend-Frontend Data Sync Test</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: 40px;
background: #f8f9fa;
}
.test-card {
background: white;
border-radius: 8px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-info {
background: #d1ecf1;
color: #0c5460;
}
.preview-box {
background: #f8f9fa;
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-top: 15px;
max-height: 400px;
overflow-y: auto;
}
.step {
padding: 15px;
margin: 10px 0;
border-left: 4px solid #667eea;
background: #f8f9fa;
}
</style>
</head>
<body>
<div class="container">
<h1 class="mb-4">
<i class="bi bi-arrow-repeat"></i> Backend-Frontend Sync Test
</h1>
<div class="test-card">
<h3>
<i class="bi bi-check-circle-fill text-success"></i>
Data Communication Status
</h3>
<p class="text-muted mb-3">
Testing the connection between admin panel edits and frontend display
</p>
<div class="step">
<strong>Step 1:</strong> Open Admin Panel →
<a
href="/admin/pages.html"
target="_blank"
class="btn btn-sm btn-primary"
>
<i class="bi bi-gear"></i> Open Pages Admin
</a>
</div>
<div class="step">
<strong>Step 2:</strong> Click Edit on any page (About, Contact, or
Privacy)
</div>
<div class="step">
<strong>Step 3:</strong> Make a small change (e.g., update phone
number, add text)
</div>
<div class="step">
<strong>Step 4:</strong> Click "Save Page" in the admin modal
</div>
<div class="step">
<strong>Step 5:</strong> Return to this test page and click the
buttons below to verify
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-eye"></i> Live Page Previews</h3>
<p class="text-muted">
View current content from database (click to refresh)
</p>
<div class="row g-3">
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('about')"
>
<i class="bi bi-file-text"></i> Test About Page
</button>
<a href="/about.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('contact')"
>
<i class="bi bi-envelope"></i> Test Contact Page
</button>
<a href="/contact.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
<div class="col-md-4">
<button
class="btn btn-outline-primary w-100"
onclick="testPage('privacy')"
>
<i class="bi bi-shield-check"></i> Test Privacy Page
</button>
<a href="/privacy.html" target="_blank" class="btn btn-link w-100"
>View Live →</a
>
</div>
</div>
<div id="previewContainer" style="display: none">
<hr class="my-4" />
<h4 id="previewTitle">Content Preview</h4>
<span class="status-badge status-success mb-3">
<i class="bi bi-check-circle"></i> Loaded from Database
</span>
<div class="preview-box" id="previewContent"></div>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-clipboard-data"></i> Test Results</h3>
<div id="testResults">
<p class="text-muted">
<i class="bi bi-info-circle"></i>
Click a test button above to check if data is syncing correctly
</p>
</div>
</div>
<div class="test-card">
<h3><i class="bi bi-lightbulb"></i> What Should Happen</h3>
<ul>
<li>
<strong>Edit in Admin</strong>: Changes saved to database
immediately
</li>
<li>
<strong>View on Frontend</strong>: Refresh page shows updated
content
</li>
<li>
<strong>No Cache Issues</strong>: Changes appear within seconds
</li>
<li>
<strong>All Sections Updated</strong>: Headers, paragraphs, lists
all reflect edits
</li>
</ul>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle-fill"></i>
<strong>Pro Tip:</strong> Keep this test page and the frontend page
open side-by-side. Edit in admin, save, then refresh the frontend page
to see changes instantly.
</div>
</div>
</div>
<script>
async function testPage(slug) {
const previewContainer = document.getElementById("previewContainer");
const previewTitle = document.getElementById("previewTitle");
const previewContent = document.getElementById("previewContent");
const testResults = document.getElementById("testResults");
previewContainer.style.display = "block";
previewTitle.textContent = `Loading ${slug} page...`;
previewContent.innerHTML =
'<div class="text-center"><div class="spinner-border" role="status"></div></div>';
try {
const response = await fetch(`/api/pages/${slug}`);
const data = await response.json();
if (data.success && data.page) {
previewTitle.textContent = `${data.page.title} - Content Preview`;
previewContent.innerHTML = data.page.content;
testResults.innerHTML = `
<div class="alert alert-success">
<h5><i class="bi bi-check-circle-fill"></i> ✓ Communication Working!</h5>
<p><strong>Page:</strong> ${data.page.title}</p>
<p><strong>Slug:</strong> ${data.page.slug}</p>
<p><strong>Content Length:</strong> ${data.page.content.length} characters</p>
<p class="mb-0"><strong>Status:</strong> Data successfully loaded from database</p>
<hr>
<small class="text-muted">
<i class="bi bi-info-circle"></i>
Any edits you make in the admin panel will be reflected here after saving and refreshing.
</small>
</div>
`;
} else {
throw new Error("Page not found");
}
} catch (error) {
previewContent.innerHTML = `
<div class="alert alert-danger">
<i class="bi bi-x-circle-fill"></i> Error loading content: ${error.message}
</div>
`;
testResults.innerHTML = `
<div class="alert alert-danger">
<h5><i class="bi bi-x-circle-fill"></i> ✗ Communication Error</h5>
<p>${error.message}</p>
</div>
`;
}
}
</script>
</body>
</html>

View File

@@ -0,0 +1,324 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Page Structured Fields Test</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: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.test-container {
max-width: 1400px;
margin: 0 auto;
}
.test-card {
background: white;
border-radius: 16px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}
.success-badge {
display: inline-block;
background: #d4edda;
color: #155724;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
margin: 5px;
}
.step {
background: #f8f9fa;
border-left: 4px solid #667eea;
padding: 15px;
margin: 10px 0;
border-radius: 4px;
}
.step strong {
color: #667eea;
}
.split-view {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-top: 20px;
}
@media (max-width: 968px) {
.split-view {
grid-template-columns: 1fr;
}
}
.preview-frame {
border: 3px solid #667eea;
border-radius: 8px;
min-height: 600px;
background: white;
}
h1,
h2,
h3 {
color: #2d3436;
}
.instruction-badge {
background: #fff3cd;
color: #856404;
padding: 12px 20px;
border-radius: 8px;
border-left: 4px solid #ffc107;
margin: 15px 0;
}
</style>
</head>
<body>
<div class="test-container">
<div class="test-card text-center">
<h1 class="mb-3">
<i class="bi bi-check-circle-fill text-success"></i>
Contact Page Structured Fields
</h1>
<p class="lead">
Test the new structured editing system that prevents layout breaking
</p>
<div class="mt-3">
<span class="success-badge">✓ Layout Protected</span>
<span class="success-badge">✓ Data Separated</span>
<span class="success-badge">✓ User-Friendly</span>
<span class="success-badge">✓ No Errors</span>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-list-check"></i> Testing Steps</h2>
<p class="text-muted mb-4">
Follow these steps to see the structured fields in action
</p>
<div class="step">
<strong>Step 1:</strong> Open the admin panel in a new tab →
<a
href="/admin/pages.html"
target="_blank"
class="btn btn-sm btn-primary ms-2"
>
<i class="bi bi-box-arrow-up-right"></i> Open Admin Panel
</a>
</div>
<div class="step">
<strong>Step 2:</strong> Find the "Contact" page in the list and click
the <strong>Edit</strong> button (pencil icon)
</div>
<div class="step">
<strong>Step 3:</strong> Notice you DON'T see a Quill rich text
editor. Instead, you see:
<ul class="mt-2">
<li>
<strong>Header Section Card</strong> - Title and subtitle fields
</li>
<li>
<strong>Contact Information Card</strong> - Phone, email, address
fields
</li>
<li>
<strong>Business Hours Card</strong> - Multiple time slot fields
with add/remove buttons
</li>
</ul>
</div>
<div class="step">
<strong>Step 4:</strong> Make a change:
<ul class="mt-2">
<li>Change phone number to <code>+1 (555) 999-8888</code></li>
<li>
Or update the header title to <code>Contact Sky Art Shop</code>
</li>
<li>Or add a new business hour slot</li>
</ul>
</div>
<div class="step">
<strong>Step 5:</strong> Click <strong>"Save Page"</strong> button at
the bottom of the modal
</div>
<div class="step">
<strong>Step 6:</strong> Return to this page and click the button
below to refresh the preview:
<button
class="btn btn-sm btn-success mt-2"
onclick="refreshPreview()"
>
<i class="bi bi-arrow-clockwise"></i> Refresh Contact Page Preview
</button>
</div>
<div class="instruction-badge">
<i class="bi bi-lightbulb-fill"></i>
<strong>What to Expect:</strong> The contact page will show your
updated data but the beautiful gradient layout, icons, and styling
will remain perfectly intact!
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-split"></i> Live Comparison</h2>
<p class="text-muted mb-3">
Compare admin interface with frontend result
</p>
<div class="split-view">
<div>
<h4 class="mb-3"><i class="bi bi-gear"></i> Admin Panel</h4>
<iframe
id="adminFrame"
src="/admin/pages.html"
class="preview-frame w-100"
title="Admin Panel"
>
</iframe>
</div>
<div>
<h4 class="mb-3">
<i class="bi bi-eye"></i> Frontend Contact Page
<button
class="btn btn-sm btn-outline-primary"
onclick="refreshPreview()"
>
<i class="bi bi-arrow-clockwise"></i>
</button>
</h4>
<iframe
id="contactFrame"
src="/contact.html"
class="preview-frame w-100"
title="Contact Page"
>
</iframe>
</div>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-shield-check"></i> What's Different?</h2>
<div class="row mt-4">
<div class="col-md-6">
<div class="alert alert-danger">
<h5><i class="bi bi-x-circle"></i> Before (Problem)</h5>
<ul>
<li>Single rich text editor for entire page</li>
<li>User could type anything (e.g., "5")</li>
<li>Would replace entire beautiful layout</li>
<li>Lost gradient cards, icons, styling</li>
<li>Required HTML knowledge to maintain</li>
</ul>
</div>
</div>
<div class="col-md-6">
<div class="alert alert-success">
<h5><i class="bi bi-check-circle"></i> After (Solution)</h5>
<ul>
<li>Structured input fields for each section</li>
<li>Can only enter data, not HTML</li>
<li>JavaScript generates formatted HTML</li>
<li>Layout template is protected</li>
<li>No HTML knowledge needed</li>
</ul>
</div>
</div>
</div>
</div>
<div class="test-card">
<h2><i class="bi bi-database"></i> Technical Details</h2>
<h4 class="mt-4">Database Structure</h4>
<pre class="bg-light p-3 rounded"><code>{
"header": {
"title": "Our Contact Information",
"subtitle": "Reach out to us..."
},
"contactInfo": {
"phone": "+1 (555) 123-4567",
"email": "contact@skyartshop.com",
"address": "123 Art Street..."
},
"businessHours": [
{ "days": "Monday - Friday", "hours": "9:00 AM - 6:00 PM" },
{ "days": "Saturday", "hours": "10:00 AM - 4:00 PM" }
]
}</code></pre>
<h4 class="mt-4">How It Works</h4>
<ol>
<li>
<strong>Admin edits fields</strong> → Structured data collected
</li>
<li>
<strong>JavaScript function</strong> → Generates formatted HTML from
template
</li>
<li>
<strong>Save to database</strong> → Stores both structured data
(JSON) and generated HTML
</li>
<li><strong>Frontend displays</strong> → Shows the generated HTML</li>
<li><strong>Result</strong> → Data changes, layout stays perfect!</li>
</ol>
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle-fill"></i>
<strong>Note:</strong> Other pages (About, Privacy) still use the rich
text editor because they don't have a fixed layout requirement. The
system automatically detects which editor to show.
</div>
</div>
<div class="test-card text-center">
<h3 class="mb-3">Quick Links</h3>
<a href="/admin/pages.html" target="_blank" class="btn btn-primary m-2">
<i class="bi bi-gear"></i> Admin Panel
</a>
<a href="/contact.html" target="_blank" class="btn btn-success m-2">
<i class="bi bi-envelope"></i> Contact Page
</a>
<a href="/test-data-sync.html" target="_blank" class="btn btn-info m-2">
<i class="bi bi-arrow-repeat"></i> Data Sync Test
</a>
</div>
</div>
<script>
function refreshPreview() {
const contactFrame = document.getElementById("contactFrame");
contactFrame.src = contactFrame.src; // Reload iframe
// Show feedback
const btn = event.target.closest("button");
const originalHTML = btn.innerHTML;
btn.innerHTML = '<i class="bi bi-check-circle"></i> Refreshed!';
btn.classList.remove("btn-outline-primary", "btn-success");
btn.classList.add("btn-success");
setTimeout(() => {
btn.innerHTML = originalHTML;
btn.classList.remove("btn-success");
btn.classList.add("btn-outline-primary");
}, 2000);
}
</script>
</body>
</html>