updateweb
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
})();
|
||||
|
||||
@@ -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;">×</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, "'")}')">
|
||||
<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 = {
|
||||
"&": "&",
|
||||
@@ -224,10 +484,3 @@ function formatDate(dateString) {
|
||||
day: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function showSuccess(message) {
|
||||
alert(message);
|
||||
}
|
||||
function showError(message) {
|
||||
alert("Error: " + message);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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, "'")}')">
|
||||
<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 = {
|
||||
"&": "&",
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
266
website/admin/js/team-members.js
Normal file
266
website/admin/js/team-members.js
Normal 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;
|
||||
}
|
||||
@@ -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 = {
|
||||
"&": "&",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
359
website/admin/team-members.html
Normal file
359
website/admin/team-members.html
Normal 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>
|
||||
182
website/admin/test-logout-fix.html
Normal file
182
website/admin/test-logout-fix.html
Normal 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>
|
||||
125
website/admin/test-products-button.html
Normal file
125
website/admin/test-products-button.html
Normal 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>
|
||||
257
website/admin/test-toast.html
Normal file
257
website/admin/test-toast.html
Normal 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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
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>
|
||||
79
website/assets/js/navigation.js
Normal file
79
website/assets/js/navigation.js
Normal 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();
|
||||
}
|
||||
})();
|
||||
143
website/assets/js/page-transitions.js
Normal file
143
website/assets/js/page-transitions.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
4
website/public/favicon.svg
Normal file
4
website/public/favicon.svg
Normal 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 |
@@ -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
323
website/public/page.html
Normal 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>© 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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
334
website/public/privacy.html
Normal 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>© 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
|
||||
269
website/public/test-custom-pages.html
Normal file
269
website/public/test-custom-pages.html
Normal 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 = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
'"': """,
|
||||
"'": "'",
|
||||
};
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
249
website/public/test-data-sync.html
Normal file
249
website/public/test-data-sync.html
Normal 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>
|
||||
324
website/public/test-structured-fields.html
Normal file
324
website/public/test-structured-fields.html
Normal 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>
|
||||
Reference in New Issue
Block a user