webupdate

This commit is contained in:
Local Server
2026-01-18 02:22:05 -06:00
parent 6fc159051a
commit 2a2a3d99e5
135 changed files with 54897 additions and 9825 deletions

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -18,10 +18,11 @@
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<link rel="stylesheet" href="/admin/css/media-library.css" />
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
@@ -37,9 +38,7 @@
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog" class="active"
@@ -65,6 +64,11 @@
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
@@ -156,115 +160,272 @@
></button>
</div>
</div>
<div class="modal-body">
<div class="modal-body" style="max-height: 75vh; overflow-y: auto">
<form id="postForm">
<input type="hidden" id="postId" />
<div class="mb-3">
<label for="postTitle" class="form-label">Title *</label>
<input
type="text"
class="form-control"
id="postTitle"
required
/>
</div>
<div class="mb-3">
<label for="postSlug" class="form-label">Slug *</label>
<input
type="text"
class="form-control"
id="postSlug"
required
/>
<small class="text-muted"
>URL-friendly version (auto-generated from title)</small
>
</div>
<div class="mb-3">
<label for="postExcerpt" class="form-label">Excerpt</label>
<textarea
class="form-control"
id="postExcerpt"
rows="2"
></textarea>
<small class="text-muted">Brief summary for listings</small>
</div>
<div class="mb-3">
<label for="postContent" class="form-label">Content *</label>
<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>
<!-- Basic Info Section -->
<div class="blog-section">
<h6 class="blog-section-title">
<i class="bi bi-info-circle"></i> Basic Information
</h6>
<div class="row">
<div class="col-md-8 mb-3">
<label for="postTitle" class="form-label">Title *</label>
<input
type="text"
class="form-control"
id="postTitle"
required
placeholder="Enter blog title"
/>
</div>
<div class="col-md-4 mb-3">
<label for="postSlug" class="form-label">Slug *</label>
<input
type="text"
class="form-control"
id="postSlug"
required
placeholder="url-friendly-slug"
/>
<small class="text-muted">Auto-generated from title</small>
</div>
</div>
<input type="hidden" id="postContent" />
</div>
<div class="mb-3">
<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">
<div class="col-md-6 mb-3">
<label for="postMetaTitle" class="form-label"
>Meta Title (SEO)</label
<div class="mb-3">
<label for="postExcerpt" class="form-label"
>Short Description / Excerpt</label
>
<input type="text" class="form-control" id="postMetaTitle" />
<textarea
class="form-control"
id="postExcerpt"
rows="2"
placeholder="Brief summary for listings and previews"
></textarea>
</div>
<div class="col-md-6 mb-3">
<label for="postMetaDescription" class="form-label"
>Meta Description (SEO)</label
</div>
<!-- Content Section -->
<div class="blog-section">
<h6 class="blog-section-title">
<i class="bi bi-file-text"></i> Content
</h6>
<div class="mb-3">
<div
id="postContentEditor"
style="
height: 350px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
"
>
<style>
#postContentEditor .ql-container {
height: calc(350px - 42px);
overflow-y: auto;
font-size: 16px;
}
#postContentEditor .ql-editor {
min-height: 100%;
}
</style>
</div>
<input type="hidden" id="postContent" />
</div>
</div>
<!-- Media Section -->
<div class="blog-section">
<h6 class="blog-section-title">
<i class="bi bi-images"></i> Media
</h6>
<div class="row">
<!-- Featured Image -->
<div class="col-md-6 mb-3">
<label class="form-label">Featured Image</label>
<input type="hidden" id="postFeaturedImage" />
<div
id="featuredImagePreview"
class="media-preview-box"
></div>
<button
type="button"
class="btn btn-outline-primary btn-sm mt-2"
onclick="openMediaLibraryForFeaturedImage()"
>
<i class="bi bi-image"></i> Select Featured Image
</button>
</div>
<!-- Gallery Images -->
<div class="col-md-6 mb-3">
<label class="form-label">Image Gallery</label>
<input type="hidden" id="postImages" />
<div
id="galleryImagesPreview"
class="gallery-preview-box"
></div>
<button
type="button"
class="btn btn-outline-primary btn-sm mt-2"
onclick="openMediaLibraryForGallery()"
>
<i class="bi bi-images"></i> Add Images to Gallery
</button>
<small class="text-muted d-block mt-1"
>Add multiple images for slideshow</small
>
</div>
</div>
<!-- Video -->
<div class="mb-3">
<label class="form-label">Video</label>
<input type="hidden" id="postVideoUrl" />
<div id="videoPreview" class="video-preview-box"></div>
<button
type="button"
class="btn btn-outline-primary btn-sm mt-2"
onclick="openMediaLibraryForVideo()"
>
<i class="bi bi-camera-video"></i> Select Video
</button>
<small class="text-muted d-block mt-1"
>Or paste a YouTube/Vimeo URL below</small
>
<input
type="text"
class="form-control"
id="postMetaDescription"
class="form-control form-control-sm mt-2"
id="postExternalVideo"
placeholder="https://youtube.com/watch?v=..."
/>
</div>
</div>
<div class="mb-3">
<!-- Poll Section -->
<div class="blog-section">
<h6 class="blog-section-title">
<i class="bi bi-bar-chart"></i> Poll (Optional)
</h6>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
type="checkbox"
id="enablePoll"
onchange="togglePollSection()"
/>
<label class="form-check-label" for="enablePoll"
>Enable Poll</label
>
</div>
<div id="pollSection" style="display: none">
<div class="mb-3">
<label for="pollQuestion" class="form-label"
>Poll Question</label
>
<input
type="text"
class="form-control"
id="pollQuestion"
placeholder="What's your favorite...?"
/>
</div>
<div class="mb-3">
<label class="form-label">Poll Options</label>
<div id="pollOptionsContainer">
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">1</span>
<input
type="text"
class="form-control poll-option-input"
placeholder="Option 1"
/>
</div>
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">2</span>
<input
type="text"
class="form-control poll-option-input"
placeholder="Option 2"
/>
</div>
</div>
<button
type="button"
class="btn btn-outline-secondary btn-sm"
onclick="addPollOption()"
>
<i class="bi bi-plus"></i> Add Option
</button>
</div>
</div>
</div>
<!-- SEO Section -->
<div class="blog-section">
<h6 class="blog-section-title">
<i class="bi bi-search"></i> SEO Settings
</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label for="postMetaTitle" class="form-label"
>Meta Title</label
>
<input
type="text"
class="form-control"
id="postMetaTitle"
placeholder="SEO title for search engines"
/>
</div>
<div class="col-md-6 mb-3">
<label for="postMetaDescription" class="form-label"
>Meta Description</label
>
<input
type="text"
class="form-control"
id="postMetaDescription"
placeholder="SEO description for search engines"
/>
</div>
</div>
</div>
<!-- Publish Settings -->
<div
class="blog-section"
style="
background: #f0fdf4;
border: 2px solid #22c55e;
border-radius: 12px;
padding: 16px;
"
>
<h6 class="blog-section-title" style="color: #16a34a">
<i class="bi bi-globe"></i> Publish Settings
</h6>
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="postPublished"
checked
style="width: 3em; height: 1.5em"
/>
<label class="form-check-label" for="postPublished">
Published (visible on website)
<label
class="form-check-label"
for="postPublished"
style="font-size: 1.1rem"
>
<strong>Published</strong>
<span style="color: #64748b">(visible on website)</span>
</label>
</div>
<p class="text-muted small mt-2 mb-0">
<i class="bi bi-info-circle"></i> Uncheck to save as draft
(won't appear on website)
</p>
</div>
</form>
</div>
@@ -284,10 +445,101 @@
</div>
</div>
<style>
.blog-section {
background: #f8fafc;
border-radius: 10px;
padding: 1.25rem;
margin-bottom: 1.25rem;
border: 1px solid #e2e8f0;
}
.blog-section-title {
color: #334155;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 8px;
}
.blog-section-title i {
color: #6366f1;
}
.media-preview-box,
.video-preview-box {
min-height: 100px;
border: 2px dashed #e2e8f0;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
overflow: hidden;
}
.media-preview-box img {
max-width: 100%;
max-height: 150px;
object-fit: contain;
}
.gallery-preview-box {
min-height: 100px;
border: 2px dashed #e2e8f0;
border-radius: 8px;
padding: 10px;
display: flex;
flex-wrap: wrap;
gap: 8px;
background: #fff;
}
.gallery-preview-box .gallery-thumb {
width: 80px;
height: 80px;
border-radius: 6px;
overflow: hidden;
position: relative;
}
.gallery-preview-box .gallery-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-preview-box .gallery-thumb .remove-btn {
position: absolute;
top: 2px;
right: 2px;
width: 20px;
height: 20px;
background: rgba(239, 68, 68, 0.9);
border: none;
border-radius: 50%;
color: white;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.video-preview-box video {
max-width: 100%;
max-height: 200px;
}
.video-preview-box .video-placeholder {
text-align: center;
color: #94a3b8;
padding: 20px;
}
.video-preview-box .video-placeholder i {
font-size: 2rem;
margin-bottom: 8px;
display: block;
}
</style>
<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?v=8.0"></script>
<script src="/admin/js/admin-utils.js"></script>
<script src="/admin/js/media-library.js?v=9.1"></script>
<script src="/admin/js/blog.js?v=9.1"></script>
</body>
</html>

View File

@@ -221,6 +221,27 @@ body {
background-color: #f8f9fa;
}
/* Table Action Buttons */
.table .btn-sm {
padding: 6px 10px;
font-size: 0.8rem;
border-radius: 6px;
margin-right: 4px;
}
.table .btn-sm:last-child {
margin-right: 0;
}
.table .btn-sm i {
font-size: 0.85rem;
}
/* Actions Column */
.table td:last-child {
white-space: nowrap;
}
/* Buttons */
.btn {
padding: 10px 20px;
@@ -229,18 +250,23 @@ body {
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
text-decoration: none;
}
.btn-primary {
background: var(--primary-gradient);
background: var(--primary-color);
border: none;
color: white;
}
.btn-primary:hover {
background: #5a6fd6;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
color: white;
}
.btn-secondary {
@@ -249,30 +275,99 @@ body {
color: white;
}
.btn-secondary:hover {
background: #5a6268;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
color: white;
}
.btn-success {
background: var(--success-color);
border: none;
color: white;
}
.btn-success:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(40, 167, 69, 0.4);
color: white;
}
.btn-danger {
background: var(--danger-color);
border: none;
color: white;
}
.btn-danger:hover {
background: #c82333;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
color: white;
}
.btn-warning {
background: var(--warning-color);
border: none;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.4);
color: #212529;
}
.btn-info {
background: var(--info-color);
border: none;
color: white;
}
.btn-info:hover {
background: #138496;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(23, 162, 184, 0.4);
color: white;
}
/* Outline Button Variants */
.btn-outline-primary {
background: transparent;
border: 2px solid var(--primary-color);
color: var(--primary-color);
}
.btn-outline-primary:hover {
background: var(--primary-color);
color: white;
}
.btn-outline-secondary {
background: transparent;
border: 2px solid #6c757d;
color: #6c757d;
}
.btn-outline-secondary:hover {
background: #6c757d;
color: white;
}
.btn-outline-danger {
background: transparent;
border: 2px solid var(--danger-color);
color: var(--danger-color);
}
.btn-outline-danger:hover {
background: var(--danger-color);
color: white;
}
.btn-logout {
background: #dc3545;
color: white;
@@ -1010,6 +1105,56 @@ body.dark-mode .btn-primary:hover {
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
body.dark-mode .btn-secondary {
background: #4a5568;
color: #ffffff;
}
body.dark-mode .btn-secondary:hover {
background: #5a6a7d;
box-shadow: 0 4px 12px rgba(74, 85, 104, 0.4);
}
body.dark-mode .btn-success {
background: #38a169;
color: #ffffff;
}
body.dark-mode .btn-success:hover {
background: #2f855a;
box-shadow: 0 4px 12px rgba(56, 161, 105, 0.4);
}
body.dark-mode .btn-danger {
background: #e53e3e;
color: #ffffff;
}
body.dark-mode .btn-danger:hover {
background: #c53030;
box-shadow: 0 4px 12px rgba(229, 62, 62, 0.4);
}
body.dark-mode .btn-warning {
background: #d69e2e;
color: #1a202c;
}
body.dark-mode .btn-warning:hover {
background: #b7791f;
box-shadow: 0 4px 12px rgba(214, 158, 46, 0.4);
}
body.dark-mode .btn-info {
background: #3182ce;
color: #ffffff;
}
body.dark-mode .btn-info:hover {
background: #2b6cb0;
box-shadow: 0 4px 12px rgba(49, 130, 206, 0.4);
}
body.dark-mode .card {
background: #2d3748;
border-color: #4a5568;
@@ -1070,3 +1215,76 @@ body.dark-mode hr {
body.dark-mode .card-body {
color: #f0f0f0;
}
/* ============================================
QUILL RICH TEXT EDITOR STYLES
============================================ */
.ql-toolbar.ql-snow {
border-radius: 8px 8px 0 0;
background: #f8f9fa;
border-color: #ced4da;
}
.ql-container.ql-snow {
border-radius: 0 0 8px 8px;
border-color: #ced4da;
font-size: 15px;
}
.ql-editor {
min-height: 150px;
max-height: 350px;
overflow-y: auto;
}
.ql-editor::-webkit-scrollbar {
width: 8px;
}
.ql-editor::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.ql-editor::-webkit-scrollbar-thumb {
background: #667eea;
border-radius: 4px;
}
.ql-editor::-webkit-scrollbar-thumb:hover {
background: #5a6fd6;
}
/* Larger editor for blog posts and custom pages */
.modal-xl .ql-editor {
min-height: 300px;
max-height: 500px;
}
/* Dark mode support for Quill */
body.dark-mode .ql-toolbar.ql-snow {
background: #374151;
border-color: #4a5568;
}
body.dark-mode .ql-toolbar.ql-snow .ql-stroke {
stroke: #f0f0f0;
}
body.dark-mode .ql-toolbar.ql-snow .ql-fill {
fill: #f0f0f0;
}
body.dark-mode .ql-toolbar.ql-snow .ql-picker {
color: #f0f0f0;
}
body.dark-mode .ql-container.ql-snow {
background: #2d3748;
border-color: #4a5568;
color: #f0f0f0;
}
body.dark-mode .ql-editor.ql-blank::before {
color: #9ca3af;
}

View File

@@ -0,0 +1,809 @@
/**
* Modern Media Library Styles
* Clean, professional design with smooth animations
*/
/* Modal Styles */
.media-library-modal .modal-content {
border: none;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
.media-library-modal .modal-xl {
max-width: 1200px;
}
/* Header */
.media-library-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 1.5rem;
border: none;
display: flex;
justify-content: space-between;
align-items: center;
}
.media-library-header .header-left {
display: flex;
flex-direction: row;
align-items: center;
gap: 1rem;
}
.media-library-header .header-left .back-btn {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.35rem 0.75rem;
font-size: 0.85rem;
border-color: rgba(255, 255, 255, 0.5);
color: white;
transition: all 0.2s ease;
}
.media-library-header .header-left .back-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: white;
}
.media-library-header .title-breadcrumb {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.media-library-header .modal-title {
font-size: 1.25rem;
font-weight: 600;
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0;
}
.media-library-header .modal-title i {
font-size: 1.4rem;
}
.media-library-header .breadcrumb-nav .breadcrumb {
font-size: 0.8rem;
}
.media-library-header .breadcrumb-item a {
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
}
.media-library-header .breadcrumb-item a:hover {
color: white;
}
.media-library-header .breadcrumb-item.active {
color: rgba(255, 255, 255, 0.6);
}
.media-library-header .breadcrumb-item + .breadcrumb-item::before {
color: rgba(255, 255, 255, 0.5);
}
.media-library-header .header-right {
display: flex;
align-items: center;
gap: 1rem;
}
.media-library-header .view-toggle .btn {
color: rgba(255, 255, 255, 0.8);
border-color: rgba(255, 255, 255, 0.3);
background: transparent;
}
.media-library-header .view-toggle .btn:hover,
.media-library-header .view-toggle .btn.active {
color: white;
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
}
.media-library-header .btn-close {
filter: brightness(0) invert(1);
opacity: 0.8;
}
.media-library-header .btn-close:hover {
opacity: 1;
}
/* Body */
.media-library-body {
padding: 0;
background: #f8fafc;
min-height: 500px;
max-height: 70vh;
overflow-y: auto;
}
/* Toolbar */
.media-library-toolbar {
background: white;
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
position: sticky;
top: 0;
z-index: 10;
}
.media-library-toolbar .toolbar-left,
.media-library-toolbar .toolbar-right {
display: flex;
align-items: center;
gap: 0.75rem;
}
.media-library-toolbar .divider {
width: 1px;
height: 24px;
background: #e2e8f0;
}
.media-library-toolbar .search-box {
position: relative;
width: 250px;
}
.media-library-toolbar .search-box i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.media-library-toolbar .search-box input {
width: 100%;
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.875rem;
transition: all 0.2s;
}
.media-library-toolbar .search-box input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.media-library-toolbar .selected-count {
background: #667eea;
color: white;
padding: 0.375rem 0.875rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.25rem;
}
/* Upload Drop Zone */
.upload-drop-zone {
margin: 1rem 1.5rem;
padding: 2rem;
border: 2px dashed #cbd5e1;
border-radius: 12px;
text-align: center;
transition: all 0.3s;
cursor: pointer;
background: white;
}
.upload-drop-zone:hover,
.upload-drop-zone.dragover {
border-color: #667eea;
background: #f0f4ff;
}
.upload-drop-zone .drop-zone-content i {
font-size: 3rem;
color: #667eea;
margin-bottom: 0.5rem;
}
.upload-drop-zone .drop-zone-content p {
margin: 0.5rem 0;
color: #64748b;
}
.upload-drop-zone .browse-link {
color: #667eea;
font-weight: 600;
cursor: pointer;
}
.upload-drop-zone .drop-zone-content small {
color: #94a3b8;
}
/* Upload Progress */
.upload-progress {
margin: 1rem 1.5rem;
padding: 1rem;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.upload-progress .progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
color: #64748b;
}
.upload-progress .progress-percent {
font-weight: 600;
color: #667eea;
}
.upload-progress .progress {
height: 8px;
border-radius: 4px;
}
.upload-progress .progress-bar {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Media Content */
.media-content {
padding: 1rem 1.5rem;
}
.media-content.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
}
.media-content.list-view {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
/* Media Item - Grid View */
.media-content.grid-view .media-item {
background: white;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.media-content.grid-view .media-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.media-content.grid-view .media-item.selected {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.media-content.grid-view .media-item .item-checkbox {
position: absolute;
top: 8px;
left: 8px;
z-index: 2;
}
.media-content.grid-view .media-item .item-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.media-content.grid-view .folder-item {
position: relative;
height: 160px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
}
.media-content.grid-view .folder-item .item-icon {
margin-bottom: 0.5rem;
}
.media-content.grid-view .folder-item .item-icon i {
font-size: 3rem;
color: #667eea;
}
.media-content.grid-view .folder-item .item-info {
text-align: center;
}
.media-content.grid-view .folder-item .item-name {
font-size: 0.875rem;
font-weight: 500;
color: #334155;
display: block;
max-width: 130px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-content.grid-view .folder-item .item-meta {
font-size: 0.75rem;
color: #94a3b8;
}
.media-content.grid-view .file-item {
position: relative;
display: flex;
flex-direction: column;
}
.media-content.grid-view .file-item .item-preview {
height: 120px;
background: #f1f5f9;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.media-content.grid-view .file-item .item-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.video-preview-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e293b 0%, #334155 100%);
color: #fff;
}
.video-preview-placeholder i {
font-size: 2.5rem;
margin-bottom: 0.5rem;
color: #60a5fa;
}
.video-preview-placeholder span {
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
color: #94a3b8;
}
.media-content.grid-view .file-item .item-info {
padding: 0.75rem;
background: white;
border-top: 1px solid #f1f5f9;
}
.media-content.grid-view .file-item .item-name {
font-size: 0.8rem;
font-weight: 500;
color: #334155;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-content.grid-view .file-item .item-meta {
font-size: 0.7rem;
color: #94a3b8;
}
.media-content.grid-view .media-item .item-actions {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.media-content.grid-view .media-item:hover .item-actions {
opacity: 1;
}
/* Media Item - List View */
.media-content.list-view .media-item {
background: white;
border-radius: 8px;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 1rem;
cursor: pointer;
transition: all 0.2s;
border: 2px solid transparent;
}
.media-content.list-view .media-item:hover {
background: #f8fafc;
}
.media-content.list-view .media-item.selected {
border-color: #667eea;
background: #f0f4ff;
}
.media-content.list-view .media-item .item-checkbox input {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.media-content.list-view .folder-item .item-icon i {
font-size: 1.75rem;
color: #667eea;
}
.media-content.list-view .file-item .item-preview {
width: 48px;
height: 48px;
border-radius: 6px;
overflow: hidden;
background: #f1f5f9;
flex-shrink: 0;
}
.media-content.list-view .file-item .item-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.media-content.list-view .media-item .item-info {
flex: 1;
min-width: 0;
}
.media-content.list-view .media-item .item-name {
font-weight: 500;
color: #334155;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.media-content.list-view .media-item .item-meta {
font-size: 0.8rem;
color: #94a3b8;
}
.media-content.list-view .media-item .item-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.media-content.list-view .media-item:hover .item-actions {
opacity: 1;
}
/* Action Buttons */
.btn-action {
width: 32px;
height: 32px;
border: none;
border-radius: 6px;
background: rgba(255, 255, 255, 0.9);
color: #64748b;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
backdrop-filter: blur(4px);
}
.btn-action:hover {
background: white;
color: #667eea;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.btn-action.btn-danger:hover {
color: #ef4444;
}
/* Empty State */
.media-content .empty-state {
text-align: center;
padding: 3rem;
color: #94a3b8;
grid-column: 1 / -1;
}
.media-content .empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.media-content .empty-state h5 {
color: #64748b;
margin-bottom: 0.5rem;
}
/* Drag Over States */
.media-item.dragging {
opacity: 0.5;
}
.folder-item.drag-over {
background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%) !important;
border: 2px dashed #667eea !important;
}
/* Footer */
.media-library-footer {
background: white;
padding: 1rem 1.5rem;
border-top: 1px solid #e2e8f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.media-library-footer .footer-left,
.media-library-footer .footer-right {
display: flex;
gap: 0.5rem;
}
/* Image Preview Overlay */
.image-preview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.95);
z-index: 10100;
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
}
.image-preview-overlay.active {
display: flex;
}
.image-preview-overlay .preview-close {
position: absolute;
top: 20px;
right: 20px;
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 50%;
font-size: 1.5rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.image-preview-overlay .preview-close:hover {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.image-preview-overlay img {
max-width: 90%;
max-height: 80%;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.image-preview-overlay .preview-info {
margin-top: 1rem;
padding: 0.75rem 1.5rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 25px;
display: flex;
gap: 1rem;
color: white;
backdrop-filter: blur(10px);
}
.image-preview-overlay .preview-filename {
font-weight: 500;
}
.image-preview-overlay .preview-size {
color: rgba(255, 255, 255, 0.7);
}
/* Toast Notifications */
.media-toast {
position: fixed;
bottom: 20px;
right: 20px;
padding: 1rem 1.5rem;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
display: flex;
align-items: center;
gap: 0.75rem;
z-index: 10200;
transform: translateX(120%);
transition: transform 0.3s ease;
}
.media-toast.show {
transform: translateX(0);
}
.media-toast.success {
border-left: 4px solid #10b981;
}
.media-toast.success i {
color: #10b981;
}
.media-toast.error {
border-left: 4px solid #ef4444;
}
.media-toast.error i {
color: #ef4444;
}
.media-toast.info {
border-left: 4px solid #667eea;
}
.media-toast.info i {
color: #667eea;
}
.media-toast i {
font-size: 1.25rem;
}
/* Rename & Create Folder Modals */
#renameModal .modal-content,
#createFolderModal .modal-content {
border-radius: 12px;
border: none;
}
#renameModal .modal-header,
#createFolderModal .modal-header {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
#renameModal .modal-title,
#createFolderModal .modal-title {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
}
#renameModal .modal-title i,
#createFolderModal .modal-title i {
color: #667eea;
}
#renameInput,
#newFolderInput {
border-radius: 8px;
padding: 0.75rem 1rem;
}
#renameInput:focus,
#newFolderInput:focus {
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
/* Loading Spinner */
.media-content .loading-spinner {
display: flex;
align-items: center;
justify-content: center;
padding: 4rem;
grid-column: 1 / -1;
}
/* Button Styles */
.media-library-toolbar .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.media-library-toolbar .btn-primary:hover {
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%);
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
}
.media-library-footer .btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
.media-library-footer .btn-primary:hover {
background: linear-gradient(135deg, #5a6fd6 0%, #6a4190 100%);
}
.media-library-footer .btn-primary:disabled {
background: #94a3b8;
opacity: 0.6;
}
/* Responsive */
@media (max-width: 768px) {
.media-library-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.media-library-toolbar {
flex-direction: column;
align-items: stretch;
}
.media-library-toolbar .toolbar-left,
.media-library-toolbar .toolbar-right {
flex-wrap: wrap;
}
.media-library-toolbar .search-box {
width: 100%;
}
.media-content.grid-view {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.media-library-footer {
flex-direction: column;
gap: 0.75rem;
}
}

View File

@@ -0,0 +1,940 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Customer Management - 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"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
.stats-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 1.25rem;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease;
}
.stat-card:hover {
transform: translateY(-3px);
}
.stat-card i {
font-size: 1.75rem;
margin-bottom: 0.5rem;
}
.stat-card .stat-number {
font-size: 1.75rem;
font-weight: 700;
color: #333;
}
.stat-card .stat-label {
color: #666;
font-size: 0.85rem;
}
.stat-card.total i {
color: #9c27b0;
}
.stat-card.verified i {
color: #2196f3;
}
.stat-card.newsletter i {
color: #e91e63;
}
.stat-card.active i {
color: #4caf50;
}
.filter-bar {
background: white;
border-radius: 12px;
padding: 1rem 1.25rem;
margin-bottom: 1rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.filter-bar .search-box {
flex: 1;
min-width: 200px;
}
.content-card {
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
.content-card table {
margin-bottom: 0;
}
.content-card th {
background: #f8f9fa;
font-weight: 600;
color: #333;
border-bottom: 2px solid #e0e0e0;
font-size: 0.9rem;
}
.content-card td {
vertical-align: middle;
font-size: 0.9rem;
}
.customer-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #e91e63 0%, #9c27b0 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
.badge-newsletter {
font-size: 0.75rem;
}
.pagination-bar {
padding: 1rem;
background: #f8f9fa;
border-top: 1px solid #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
}
.empty-state {
text-align: center;
padding: 3rem 2rem;
color: #666;
}
.empty-state i {
font-size: 3rem;
color: #e0e0e0;
margin-bottom: 1rem;
}
.btn-export {
background: linear-gradient(135deg, #e91e63 0%, #9c27b0 100%);
border: none;
color: white;
}
.btn-export:hover {
color: white;
opacity: 0.9;
}
@media (max-width: 992px) {
.stats-row {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 576px) {
.stats-row {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers" class="active"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
<!-- Main Content -->
<div class="main-content">
<!-- Top Bar -->
<div class="top-bar">
<div>
<h3>Customer Management</h3>
<p class="mb-0 text-muted">View and manage registered customers</p>
</div>
<div>
<button class="btn btn-export me-2" onclick="exportNewsletterList()">
<i class="bi bi-download"></i> Export Newsletter
</button>
<button class="btn-logout" id="btnLogout">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<!-- Stats Cards -->
<div class="stats-row">
<div class="stat-card total">
<i class="bi bi-people"></i>
<div class="stat-number" id="statTotal">-</div>
<div class="stat-label">Total Customers</div>
</div>
<div class="stat-card verified">
<i class="bi bi-patch-check"></i>
<div class="stat-number" id="statVerified">-</div>
<div class="stat-label">Verified</div>
</div>
<div class="stat-card newsletter">
<i class="bi bi-envelope-heart"></i>
<div class="stat-number" id="statNewsletter">-</div>
<div class="stat-label">Newsletter</div>
</div>
<div class="stat-card active">
<i class="bi bi-person-check"></i>
<div class="stat-number" id="statActive">-</div>
<div class="stat-label">Active</div>
</div>
</div>
<!-- Filter Bar -->
<div class="filter-bar">
<div class="search-box">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input
type="text"
class="form-control"
id="searchInput"
placeholder="Search by name or email..."
/>
</div>
</div>
<select class="form-select" id="statusFilter" style="width: auto">
<option value="all">All Status</option>
<option value="verified">Verified</option>
<option value="unverified">Unverified</option>
</select>
<select class="form-select" id="newsletterFilter" style="width: auto">
<option value="all">All Customers</option>
<option value="subscribed">Newsletter Subscribed</option>
<option value="unsubscribed">Not Subscribed</option>
</select>
<button class="btn btn-outline-secondary" onclick="loadCustomers()">
<i class="bi bi-funnel"></i> Filter
</button>
<span class="text-muted ms-auto" id="resultCount">Loading...</span>
</div>
<!-- Customers Table -->
<div class="content-card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th style="width: 50px"></th>
<th>Name</th>
<th>Email</th>
<th class="text-center">Verified</th>
<th class="text-center">Newsletter</th>
<th class="text-center">Cart</th>
<th class="text-center">Wishlist</th>
<th class="text-center">Logins</th>
<th>Joined</th>
<th class="text-center">Actions</th>
</tr>
</thead>
<tbody id="customersTableBody">
<tr>
<td colspan="10" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination-bar">
<span class="text-muted" id="paginationInfo">Showing 0 of 0</span>
<nav>
<ul
class="pagination pagination-sm mb-0"
id="paginationControls"
></ul>
</nav>
</div>
</div>
</div>
<!-- Customer Detail Modal -->
<div class="modal fade" id="customerModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Customer Details</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4 text-center">
<div
class="customer-avatar mx-auto mb-2"
style="width: 80px; height: 80px; font-size: 2rem"
id="modalAvatar"
>
?
</div>
<h5 id="modalName">-</h5>
<p class="text-muted" id="modalEmail">-</p>
<div class="d-flex justify-content-center gap-3 mb-3">
<div>
<div class="fw-bold" id="modalLogins">0</div>
<small class="text-muted">Logins</small>
</div>
<div>
<div id="modalNewsletter">-</div>
<small class="text-muted">Newsletter</small>
</div>
<div>
<div id="modalStatus">-</div>
<small class="text-muted">Status</small>
</div>
</div>
<p class="small text-muted mb-1">
Member Since: <span id="modalJoined">-</span>
</p>
<p class="small text-muted">
Last Login: <span id="modalLastLogin">-</span>
</p>
</div>
<div class="col-md-8">
<ul class="nav nav-tabs mb-3" id="customerTabs">
<li class="nav-item">
<a
class="nav-link active"
data-bs-toggle="tab"
href="#tabCart"
>Cart (<span id="cartCount">0</span>)</a
>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tabWishlist"
>Wishlist (<span id="wishlistCount">0</span>)</a
>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="tabCart">
<div
id="cartItems"
class="list-group list-group-flush"
style="max-height: 300px; overflow-y: auto"
>
<p class="text-muted text-center py-3">
No items in cart
</p>
</div>
</div>
<div class="tab-pane fade" id="tabWishlist">
<div
id="wishlistItems"
class="list-group list-group-flush"
style="max-height: 300px; overflow-y: auto"
>
<p class="text-muted text-center py-3">
No items in wishlist
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Close
</button>
<button
type="button"
class="btn btn-warning"
id="modalToggleBtn"
onclick="toggleCustomerStatus()"
>
Deactivate
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// State
let currentPage = 1;
let totalPages = 1;
let currentCustomerId = null;
let currentCustomerActive = true;
// Initialize
document.addEventListener("DOMContentLoaded", function () {
loadStats();
loadCustomers();
// Search on Enter
document
.getElementById("searchInput")
.addEventListener("keyup", (e) => {
if (e.key === "Enter") loadCustomers();
});
// Filter change
document
.getElementById("newsletterFilter")
.addEventListener("change", loadCustomers);
document
.getElementById("statusFilter")
.addEventListener("change", loadCustomers);
// Logout button
document.getElementById("btnLogout").addEventListener("click", logout);
});
// Logout function
async function logout() {
try {
await fetch("/api/admin/logout", { method: "POST" });
window.location.href = "/admin/login";
} catch (error) {
console.error("Logout failed:", error);
window.location.href = "/admin/login";
}
}
// Load statistics
async function loadStats() {
try {
const response = await fetch("/api/admin/customers/stats/overview");
const data = await response.json();
if (data.success) {
document.getElementById("statTotal").textContent = data.stats.total;
document.getElementById("statVerified").textContent =
data.stats.verified;
document.getElementById("statNewsletter").textContent =
data.stats.newsletterSubscribed;
document.getElementById("statActive").textContent =
data.stats.active;
}
} catch (error) {
console.error("Failed to load stats:", error);
}
}
// Load customers
async function loadCustomers() {
const search = document.getElementById("searchInput").value;
const newsletter = document.getElementById("newsletterFilter").value;
const status = document.getElementById("statusFilter").value;
const params = new URLSearchParams({
page: currentPage,
limit: 20,
newsletter,
status,
search,
});
try {
const response = await fetch(`/api/admin/customers?${params}`);
const data = await response.json();
if (data.success) {
renderCustomers(data.customers);
updatePagination(data.pagination);
document.getElementById("resultCount").textContent =
`${data.pagination.total} customers`;
}
} catch (error) {
console.error("Failed to load customers:", error);
document.getElementById("customersTableBody").innerHTML = `
<tr>
<td colspan="10">
<div class="empty-state">
<i class="bi bi-exclamation-triangle"></i>
<h5>Failed to load customers</h5>
<p>Please try refreshing the page.</p>
</div>
</td>
</tr>
`;
}
}
// Render customers table
function renderCustomers(customers) {
const tbody = document.getElementById("customersTableBody");
if (customers.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="10">
<div class="empty-state">
<i class="bi bi-people"></i>
<h5>No customers found</h5>
<p>No customers match your search criteria.</p>
</div>
</td>
</tr>
`;
return;
}
tbody.innerHTML = customers
.map(
(customer) => `
<tr>
<td>
<div class="customer-avatar">
${customer.first_name.charAt(0).toUpperCase()}
</div>
</td>
<td>
<strong>${escapeHtml(customer.first_name)} ${escapeHtml(
customer.last_name,
)}</strong>
</td>
<td>
<a href="mailto:${escapeHtml(customer.email)}">${escapeHtml(
customer.email,
)}</a>
</td>
<td class="text-center">
${
customer.email_verified
? '<span class="badge bg-success"><i class="bi bi-check-circle"></i></span>'
: '<span class="badge bg-warning text-dark"><i class="bi bi-clock"></i></span>'
}
</td>
<td class="text-center">
${
customer.newsletter_subscribed
? '<span class="badge bg-success badge-newsletter"><i class="bi bi-check"></i></span>'
: '<span class="badge bg-secondary badge-newsletter"><i class="bi bi-x"></i></span>'
}
</td>
<td class="text-center"><span class="badge bg-info">${
customer.cart_count || 0
}</span></td>
<td class="text-center"><span class="badge bg-pink">${
customer.wishlist_count || 0
}</span></td>
<td class="text-center">${customer.login_count || 0}</td>
<td>${formatDate(customer.created_at)}</td>
<td class="text-center">
<button class="btn btn-sm btn-outline-primary" onclick="viewCustomer('${
customer.id
}')">
<i class="bi bi-eye"></i>
</button>
</td>
</tr>
`,
)
.join("");
}
// Update pagination
function updatePagination(pagination) {
totalPages = pagination.totalPages || 1;
const start =
pagination.total > 0
? (pagination.page - 1) * pagination.limit + 1
: 0;
const end = Math.min(
pagination.page * pagination.limit,
pagination.total,
);
document.getElementById("paginationInfo").textContent =
`Showing ${start}-${end} of ${pagination.total}`;
const controls = document.getElementById("paginationControls");
let html = "";
if (totalPages > 1) {
html += `
<li class="page-item ${pagination.page === 1 ? "disabled" : ""}">
<a class="page-link" href="#" onclick="goToPage(${
pagination.page - 1
}); return false;">
<i class="bi bi-chevron-left"></i>
</a>
</li>
`;
for (let i = 1; i <= totalPages; i++) {
if (
i === 1 ||
i === totalPages ||
(i >= pagination.page - 1 && i <= pagination.page + 1)
) {
html += `
<li class="page-item ${i === pagination.page ? "active" : ""}">
<a class="page-link" href="#" onclick="goToPage(${i}); return false;">${i}</a>
</li>
`;
} else if (i === pagination.page - 2 || i === pagination.page + 2) {
html += `<li class="page-item disabled"><span class="page-link">...</span></li>`;
}
}
html += `
<li class="page-item ${
pagination.page === totalPages ? "disabled" : ""
}">
<a class="page-link" href="#" onclick="goToPage(${
pagination.page + 1
}); return false;">
<i class="bi bi-chevron-right"></i>
</a>
</li>
`;
}
controls.innerHTML = html;
}
// Go to page
function goToPage(page) {
if (page < 1 || page > totalPages) return;
currentPage = page;
loadCustomers();
}
// View customer details
async function viewCustomer(id) {
try {
const response = await fetch(`/api/admin/customers/${id}`);
const data = await response.json();
if (data.success) {
const customer = data.customer;
currentCustomerId = customer.id;
currentCustomerActive = customer.is_active;
document.getElementById("modalAvatar").textContent =
customer.first_name.charAt(0).toUpperCase();
document.getElementById("modalName").textContent =
`${customer.first_name} ${customer.last_name}`;
document.getElementById("modalEmail").textContent = customer.email;
document.getElementById("modalLogins").textContent =
customer.login_count || 0;
document.getElementById("modalNewsletter").innerHTML =
customer.newsletter_subscribed
? '<span class="text-success"><i class="bi bi-check-circle"></i></span>'
: '<span class="text-muted"><i class="bi bi-x-circle"></i></span>';
document.getElementById("modalStatus").innerHTML =
customer.is_active
? '<span class="text-success">Active</span>'
: '<span class="text-danger">Inactive</span>';
document.getElementById("modalJoined").textContent = formatDate(
customer.created_at,
true,
);
document.getElementById("modalLastLogin").textContent =
customer.last_login
? formatDate(customer.last_login, true)
: "Never";
document.getElementById("modalToggleBtn").textContent =
customer.is_active ? "Deactivate" : "Activate";
document.getElementById("modalToggleBtn").className =
customer.is_active ? "btn btn-warning" : "btn btn-success";
// Load cart and wishlist
loadCustomerCart(id);
loadCustomerWishlist(id);
new bootstrap.Modal(
document.getElementById("customerModal"),
).show();
}
} catch (error) {
console.error("Failed to load customer:", error);
alert("Failed to load customer details");
}
}
// Load customer cart
async function loadCustomerCart(customerId) {
try {
const response = await fetch(
`/api/admin/customers/${customerId}/cart`,
);
const data = await response.json();
const container = document.getElementById("cartItems");
const countEl = document.getElementById("cartCount");
if (data.success && data.items && data.items.length > 0) {
countEl.textContent = data.items.length;
container.innerHTML = data.items
.map(
(item) => `
<div class="list-group-item d-flex align-items-center">
<img src="${
item.image || "/assets/images/products/placeholder.jpg"
}"
alt="${escapeHtml(item.name)}"
style="width: 50px; height: 50px; object-fit: cover; border-radius: 8px;"
class="me-3">
<div class="flex-grow-1">
<div class="fw-semibold">${escapeHtml(item.name)}</div>
<small class="text-muted">Qty: ${item.quantity}</small>
</div>
<div class="text-end">
<div class="fw-bold">$${(item.price * item.quantity).toFixed(
2,
)}</div>
</div>
</div>
`,
)
.join("");
} else {
countEl.textContent = "0";
container.innerHTML =
'<p class="text-muted text-center py-3">No items in cart</p>';
}
} catch (error) {
console.error("Failed to load cart:", error);
}
}
// Load customer wishlist
async function loadCustomerWishlist(customerId) {
try {
const response = await fetch(
`/api/admin/customers/${customerId}/wishlist`,
);
const data = await response.json();
const container = document.getElementById("wishlistItems");
const countEl = document.getElementById("wishlistCount");
if (data.success && data.items && data.items.length > 0) {
countEl.textContent = data.items.length;
container.innerHTML = data.items
.map(
(item) => `
<div class="list-group-item d-flex align-items-center">
<img src="${
item.image || "/assets/images/products/placeholder.jpg"
}"
alt="${escapeHtml(item.name)}"
style="width: 50px; height: 50px; object-fit: cover; border-radius: 8px;"
class="me-3">
<div class="flex-grow-1">
<div class="fw-semibold">${escapeHtml(item.name)}</div>
<small class="text-muted">Added: ${formatDate(
item.added_at,
)}</small>
</div>
<div class="text-end">
<div class="fw-bold">$${parseFloat(item.price).toFixed(
2,
)}</div>
</div>
</div>
`,
)
.join("");
} else {
countEl.textContent = "0";
container.innerHTML =
'<p class="text-muted text-center py-3">No items in wishlist</p>';
}
} catch (error) {
console.error("Failed to load wishlist:", error);
}
}
// Toggle customer status
async function toggleCustomerStatus() {
if (!currentCustomerId) return;
const newStatus = !currentCustomerActive;
const action = newStatus ? "activate" : "deactivate";
if (!confirm(`Are you sure you want to ${action} this customer?`))
return;
try {
const response = await fetch(
`/api/admin/customers/${currentCustomerId}/status`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ is_active: newStatus }),
},
);
const data = await response.json();
if (data.success) {
bootstrap.Modal.getInstance(
document.getElementById("customerModal"),
).hide();
loadCustomers();
loadStats();
} else {
alert(data.message || "Failed to update status");
}
} catch (error) {
console.error("Failed to update status:", error);
alert("Failed to update customer status");
}
}
// Export newsletter list
async function exportNewsletterList() {
try {
const response = await fetch(
"/api/admin/customers/export/newsletter",
);
const data = await response.json();
if (data.success) {
const headers = ["First Name", "Last Name", "Email"];
const rows = data.customers.map((c) => [
c.first_name,
c.last_name,
c.email,
]);
let csv = headers.join(",") + "\n";
csv += rows.map((r) => r.map((v) => `"${v}"`).join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `newsletter-subscribers-${
new Date().toISOString().split("T")[0]
}.csv`;
a.click();
window.URL.revokeObjectURL(url);
alert(`Exported ${data.count} newsletter subscribers`);
}
} catch (error) {
console.error("Failed to export:", error);
alert("Failed to export newsletter list");
}
}
// Format date
function formatDate(dateStr, full = false) {
if (!dateStr) return "-";
const date = new Date(dateStr);
if (full) {
return date.toLocaleString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
}
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}
// Escape HTML
function escapeHtml(str) {
if (!str) return "";
return str
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
</script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -13,6 +13,7 @@
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>
:root {
--primary-gradient: #202023;
@@ -27,7 +28,8 @@
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background-color: #f8f9fa;
overflow-x: hidden;
@@ -406,7 +408,9 @@
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"><i class="bi bi-file-text"></i> Pages</a>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
@@ -422,6 +426,11 @@
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -13,11 +13,13 @@
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" />
<link rel="stylesheet" href="/admin/css/media-library.css" />
<!-- Quill Rich Text Editor -->
<link
href="https://cdn.jsdelivr.net/npm/quill@1.3.6/dist/quill.snow.css"
href="https://cdn.quilljs.com/1.3.7/quill.snow.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
.section-builder {
background: white;
@@ -33,11 +35,6 @@
border-color: #667eea;
}
.section-builder.disabled {
opacity: 0.6;
background: #f8f9fa;
}
.section-header {
display: flex;
justify-content: space-between;
@@ -59,10 +56,60 @@
align-items: center;
}
/* Slide Management */
.slides-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.slide-card {
background: #f8f9fa;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
position: relative;
transition: all 0.3s ease;
}
.slide-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.slide-card.active {
border-color: #28a745;
background: #f8fff9;
}
.slide-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #e9ecef;
}
.slide-number {
font-weight: 700;
color: #667eea;
font-size: 1.1rem;
}
.slide-actions {
display: flex;
gap: 8px;
}
.slide-actions .btn {
padding: 5px 10px;
font-size: 0.85rem;
}
.image-preview {
width: 100%;
max-width: 400px;
height: 200px;
height: 150px;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
@@ -83,30 +130,23 @@
color: #6c757d;
}
.alignment-selector {
.add-slide-btn {
border: 2px dashed #667eea;
background: transparent;
color: #667eea;
padding: 20px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 10px;
}
.alignment-btn {
flex: 1;
padding: 10px;
border: 2px solid #e9ecef;
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.alignment-btn:hover {
border-color: #667eea;
background: #f8f9fa;
}
.alignment-btn.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.add-slide-btn:hover {
background: #667eea;
color: white;
}
@@ -118,34 +158,99 @@
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;
.drag-handle {
cursor: grab;
color: #adb5bd;
font-size: 1.2rem;
}
.ql-toolbar {
.drag-handle:hover {
color: #667eea;
}
/* Featured Products Section */
.count-selector {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.count-btn {
padding: 8px 16px;
border: 2px solid #e9ecef;
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
font-weight: 600;
}
.count-btn:hover {
border-color: #667eea;
}
.count-btn.active {
border-color: #667eea;
background: #667eea;
color: white;
}
/* Info box */
.info-box {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-left: 4px solid #2196f3;
padding: 15px;
border-radius: 0 8px 8px 0;
margin-bottom: 20px;
}
.info-box i {
color: #2196f3;
}
/* Quill Editor Styles */
.quill-container {
background: white;
border-radius: 8px;
margin-bottom: 10px;
}
.quill-container .ql-toolbar {
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background: #f8f9fa;
border-color: #ced4da;
}
.ql-editor {
min-height: 150px;
font-size: 15px;
line-height: 1.6;
.quill-container .ql-container {
border-bottom-left-radius: 8px;
border-bottom-right-radius: 8px;
border-color: #ced4da;
min-height: 120px;
font-size: 14px;
}
.ql-editor.ql-blank::before {
color: #adb5bd;
font-style: italic;
.quill-container .ql-editor {
min-height: 100px;
}
.quill-container .ql-editor.ql-blank::before {
font-style: normal;
color: #6c757d;
}
.slide-card .quill-container .ql-container {
min-height: 80px;
}
.slide-card .quill-container .ql-editor {
min-height: 60px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
@@ -161,9 +266,7 @@
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
@@ -187,6 +290,11 @@
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
@@ -197,7 +305,7 @@
<p class="mb-0 text-muted">Customize your homepage sections</p>
</div>
<div>
<a href="/index.html" target="_blank" class="btn btn-info me-2">
<a href="/home" target="_blank" class="btn btn-info me-2">
<i class="bi bi-eye"></i> Preview
</a>
<button class="btn-logout" onclick="logout()">
@@ -207,234 +315,122 @@
</div>
<div id="sectionsContainer">
<!-- Hero Section -->
<div class="section-builder" id="heroSection">
<!-- Hero Slider Section -->
<div class="section-builder" id="heroSliderSection">
<div class="section-header">
<h5><i class="bi bi-stars"></i> Hero Section</h5>
<h5><i class="bi bi-collection-play"></i> Hero Slider</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="heroEnabled"
checked
onchange="toggleSection('hero')"
/>
<label class="form-check-label" for="heroEnabled"
>Enabled</label
>
</div>
<span class="badge bg-primary" id="slideCount">0 slides</span>
</div>
</div>
<div class="info-box">
<p class="mb-2">
<i class="bi bi-info-circle me-2"></i>
<strong>Hero Slider:</strong> Create multiple slides with
background images, titles, descriptions, and call-to-action
buttons. Slides will auto-rotate every 10 seconds on the frontend.
</p>
<p
class="mb-0"
style="
background: #fff3cd;
padding: 10px;
border-radius: 6px;
border-left: 4px solid #ffc107;
"
>
<i class="bi bi-image me-2" style="color: #856404"></i>
<strong style="color: #856404">Recommended Image Size:</strong>
<code
style="
background: #ffeeba;
padding: 2px 8px;
border-radius: 4px;
"
>1920 x 600 pixels</code
>
(width x height). Use landscape images for best results. The image
will cover the entire slider area as a background.
</p>
</div>
<div class="section-content">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Headline *</label>
<input
type="text"
class="form-control"
id="heroHeadline"
placeholder="Welcome to Sky Art Shop"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Subheading</label>
<input
type="text"
class="form-control"
id="heroSubheading"
placeholder="Your creative destination"
/>
</div>
<div class="slides-container" id="slidesContainer">
<!-- Slides will be rendered here -->
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="heroDescription"
style="background: white; min-height: 150px"
></div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">CTA Button Text</label>
<input
type="text"
class="form-control"
id="heroCtaText"
placeholder="Shop Now"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">CTA Button Link</label>
<input
type="text"
class="form-control"
id="heroCtaLink"
placeholder="/shop"
/>
</div>
</div>
<div class="mb-3">
<label class="form-label">Background Image/Video</label>
<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">
<label class="form-label">Layout</label>
<div class="alignment-selector">
<button
class="alignment-btn active"
onclick="setLayout('hero', 'text-left')"
>
<i class="bi bi-align-start"></i> Text Left
</button>
<button
class="alignment-btn"
onclick="setLayout('hero', 'text-center')"
>
<i class="bi bi-align-center"></i> Text Center
</button>
<button
class="alignment-btn"
onclick="setLayout('hero', 'text-right')"
>
<i class="bi bi-align-end"></i> Text Right
</button>
</div>
</div>
<button
type="button"
class="add-slide-btn w-100 mt-3"
onclick="addNewSlide()"
>
<i class="bi bi-plus-circle"></i> Add New Slide
</button>
</div>
</div>
<!-- Promotion Section -->
<div class="section-builder" id="promotionSection">
<!-- Featured Products Section -->
<div class="section-builder" id="featuredProductsSection">
<div class="section-header">
<h5><i class="bi bi-gift"></i> Promotion Section</h5>
<h5><i class="bi bi-star"></i> Featured Products</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="promotionEnabled"
id="featuredEnabled"
checked
onchange="toggleSection('promotion')"
/>
<label class="form-check-label" for="promotionEnabled"
>Enabled</label
<label class="form-check-label" for="featuredEnabled"
>Show on Homepage</label
>
</div>
</div>
</div>
<div class="info-box">
<p class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Products marked as "Featured" in the Products section will appear
here automatically.
</p>
</div>
<div class="section-content">
<div class="mb-3">
<label class="form-label">Section Title</label>
<input
type="text"
class="form-control"
id="promotionTitle"
placeholder="Special Offers"
/>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="promotionDescription"
style="background: white; min-height: 150px"
></div>
</div>
<div class="mb-3">
<label class="form-label">Section Image</label>
<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">
<div class="col-md-6 mb-3">
<label class="form-label">Image Position</label>
<div class="alignment-selector">
<button
class="alignment-btn active"
onclick="setImagePosition('promotion', 'left')"
>
<i class="bi bi-arrow-left"></i> Left
</button>
<button
class="alignment-btn"
onclick="setImagePosition('promotion', 'center')"
>
<i class="bi bi-arrow-down"></i> Center
</button>
<button
class="alignment-btn"
onclick="setImagePosition('promotion', 'right')"
>
<i class="bi bi-arrow-right"></i> Right
</button>
</div>
<label class="form-label">Section Title</label>
<input
type="text"
class="form-control"
id="featuredTitle"
value="Featured Products"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text Alignment</label>
<div class="alignment-selector">
<label class="form-label">Number of Products to Display</label>
<div class="count-selector">
<button
class="alignment-btn active"
onclick="setTextAlignment('promotion', 'left')"
class="count-btn"
data-count="4"
onclick="setFeaturedCount(4)"
>
<i class="bi bi-text-left"></i> Left
4
</button>
<button
class="alignment-btn"
onclick="setTextAlignment('promotion', 'center')"
class="count-btn active"
data-count="8"
onclick="setFeaturedCount(8)"
>
<i class="bi bi-text-center"></i> Center
8
</button>
<button
class="alignment-btn"
onclick="setTextAlignment('promotion', 'right')"
class="count-btn"
data-count="12"
onclick="setFeaturedCount(12)"
>
<i class="bi bi-text-right"></i> Right
12
</button>
</div>
</div>
@@ -442,21 +438,81 @@
</div>
</div>
<!-- Portfolio Showcase Section -->
<div class="section-builder" id="portfolioSection">
<!-- Get Inspired / Blog Section -->
<div class="section-builder" id="blogSection">
<div class="section-header">
<h5><i class="bi bi-easel"></i> Portfolio Showcase</h5>
<h5><i class="bi bi-lightbulb"></i> Get Inspired (Blog Posts)</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="portfolioEnabled"
id="blogEnabled"
checked
onchange="toggleSection('portfolio')"
/>
<label class="form-check-label" for="portfolioEnabled"
>Enabled</label
<label class="form-check-label" for="blogEnabled"
>Show on Homepage</label
>
</div>
</div>
</div>
<div class="info-box">
<p class="mb-0">
<i class="bi bi-info-circle me-2"></i>
Latest blog posts will be displayed automatically from the Blog
section.
</p>
</div>
<div class="section-content">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Section Title</label>
<input
type="text"
class="form-control"
id="blogTitle"
value="Get Inspired"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Number of Posts to Display</label>
<div class="count-selector">
<button
class="count-btn active"
data-count="3"
onclick="setBlogCount(3)"
>
3
</button>
<button
class="count-btn"
data-count="6"
onclick="setBlogCount(6)"
>
6
</button>
</div>
</div>
</div>
</div>
</div>
<!-- About Preview Section -->
<div class="section-builder" id="aboutSection">
<div class="section-header">
<h5><i class="bi bi-info-square"></i> About Preview</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="aboutEnabled"
checked
/>
<label class="form-check-label" for="aboutEnabled"
>Show on Homepage</label
>
</div>
</div>
@@ -464,33 +520,33 @@
<div class="section-content">
<div class="mb-3">
<label class="form-label">Section Title</label>
<label class="form-label">About Title</label>
<input
type="text"
class="form-control"
id="portfolioTitle"
placeholder="Our Work"
id="aboutTitle"
value="About Sky Art Shop"
/>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="portfolioDescription"
style="background: white; min-height: 150px"
></div>
<label class="form-label">About Description</label>
<div class="quill-container">
<div id="aboutDescriptionEditor"></div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Number of Projects to Display</label>
<input
type="number"
class="form-control"
id="portfolioCount"
value="6"
min="3"
max="12"
/>
<label class="form-label">About Image</label>
<input type="hidden" id="aboutImageUrl" />
<button
type="button"
class="btn btn-outline-primary w-100"
onclick="openMediaLibrary('about')"
>
<i class="bi bi-folder2-open"></i> Choose from Media Library
</button>
<div class="image-preview empty" id="aboutPreview">
<i class="bi bi-image" style="font-size: 2rem"></i>
</div>
</div>
</div>
</div>
@@ -505,8 +561,10 @@
</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="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script src="/admin/js/auth.js"></script>
<script src="/admin/js/admin-utils.js"></script>
<script src="/admin/js/media-library.js"></script>
<script src="/admin/js/homepage.js"></script>
</body>
</html>

View File

@@ -0,0 +1,512 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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"
/>
<link
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 {
background: white;
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
border: 2px solid #e9ecef;
transition: all 0.3s ease;
}
.section-builder:hover {
border-color: #667eea;
}
.section-builder.disabled {
opacity: 0.6;
background: #f8f9fa;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
}
.section-header h5 {
margin: 0;
color: #2c3e50;
font-weight: 700;
}
.section-controls {
display: flex;
gap: 10px;
align-items: center;
}
.image-preview {
width: 100%;
max-width: 400px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
overflow: hidden;
background: #f8f9fa;
}
.image-preview img {
max-width: 100%;
max-height: 100%;
object-fit: cover;
}
.image-preview.empty {
color: #6c757d;
}
.alignment-selector {
display: flex;
gap: 10px;
margin-top: 10px;
}
.alignment-btn {
flex: 1;
padding: 10px;
border: 2px solid #e9ecef;
background: white;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
}
.alignment-btn:hover {
border-color: #667eea;
background: #f8f9fa;
}
.alignment-btn.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.save-button {
position: fixed;
bottom: 30px;
right: 30px;
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>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage" class="active"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
</ul>
</div>
<div class="main-content">
<div class="top-bar">
<div>
<h3>Homepage Editor</h3>
<p class="mb-0 text-muted">Customize your homepage sections</p>
</div>
<div>
<a href="/index.html" target="_blank" class="btn btn-info me-2">
<i class="bi bi-eye"></i> Preview
</a>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<div id="sectionsContainer">
<!-- Hero Section -->
<div class="section-builder" id="heroSection">
<div class="section-header">
<h5><i class="bi bi-stars"></i> Hero Section</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="heroEnabled"
checked
onchange="toggleSection('hero')"
/>
<label class="form-check-label" for="heroEnabled"
>Enabled</label
>
</div>
</div>
</div>
<div class="section-content">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Headline *</label>
<input
type="text"
class="form-control"
id="heroHeadline"
placeholder="Welcome to Sky Art Shop"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Subheading</label>
<input
type="text"
class="form-control"
id="heroSubheading"
placeholder="Your creative destination"
/>
</div>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="heroDescription"
style="background: white; min-height: 150px"
></div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">CTA Button Text</label>
<input
type="text"
class="form-control"
id="heroCtaText"
placeholder="Shop Now"
/>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">CTA Button Link</label>
<input
type="text"
class="form-control"
id="heroCtaLink"
placeholder="/shop"
/>
</div>
</div>
<div class="mb-3">
<label class="form-label">Background Image/Video</label>
<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">
<label class="form-label">Layout</label>
<div class="alignment-selector">
<button
class="alignment-btn active"
onclick="setLayout('hero', 'text-left')"
>
<i class="bi bi-align-start"></i> Text Left
</button>
<button
class="alignment-btn"
onclick="setLayout('hero', 'text-center')"
>
<i class="bi bi-align-center"></i> Text Center
</button>
<button
class="alignment-btn"
onclick="setLayout('hero', 'text-right')"
>
<i class="bi bi-align-end"></i> Text Right
</button>
</div>
</div>
</div>
</div>
<!-- Promotion Section -->
<div class="section-builder" id="promotionSection">
<div class="section-header">
<h5><i class="bi bi-gift"></i> Promotion Section</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="promotionEnabled"
checked
onchange="toggleSection('promotion')"
/>
<label class="form-check-label" for="promotionEnabled"
>Enabled</label
>
</div>
</div>
</div>
<div class="section-content">
<div class="mb-3">
<label class="form-label">Section Title</label>
<input
type="text"
class="form-control"
id="promotionTitle"
placeholder="Special Offers"
/>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="promotionDescription"
style="background: white; min-height: 150px"
></div>
</div>
<div class="mb-3">
<label class="form-label">Section Image</label>
<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">
<div class="col-md-6 mb-3">
<label class="form-label">Image Position</label>
<div class="alignment-selector">
<button
class="alignment-btn active"
onclick="setImagePosition('promotion', 'left')"
>
<i class="bi bi-arrow-left"></i> Left
</button>
<button
class="alignment-btn"
onclick="setImagePosition('promotion', 'center')"
>
<i class="bi bi-arrow-down"></i> Center
</button>
<button
class="alignment-btn"
onclick="setImagePosition('promotion', 'right')"
>
<i class="bi bi-arrow-right"></i> Right
</button>
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Text Alignment</label>
<div class="alignment-selector">
<button
class="alignment-btn active"
onclick="setTextAlignment('promotion', 'left')"
>
<i class="bi bi-text-left"></i> Left
</button>
<button
class="alignment-btn"
onclick="setTextAlignment('promotion', 'center')"
>
<i class="bi bi-text-center"></i> Center
</button>
<button
class="alignment-btn"
onclick="setTextAlignment('promotion', 'right')"
>
<i class="bi bi-text-right"></i> Right
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Portfolio Showcase Section -->
<div class="section-builder" id="portfolioSection">
<div class="section-header">
<h5><i class="bi bi-easel"></i> Portfolio Showcase</h5>
<div class="section-controls">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="portfolioEnabled"
checked
onchange="toggleSection('portfolio')"
/>
<label class="form-check-label" for="portfolioEnabled"
>Enabled</label
>
</div>
</div>
</div>
<div class="section-content">
<div class="mb-3">
<label class="form-label">Section Title</label>
<input
type="text"
class="form-control"
id="portfolioTitle"
placeholder="Our Work"
/>
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<div
id="portfolioDescription"
style="background: white; min-height: 150px"
></div>
</div>
<div class="mb-3">
<label class="form-label">Number of Projects to Display</label>
<input
type="number"
class="form-control"
id="portfolioCount"
value="6"
min="3"
max="12"
/>
</div>
</div>
</div>
</div>
<button
class="btn btn-lg btn-primary save-button"
onclick="saveHomepage()"
>
<i class="bi bi-save"></i> Save All Changes
</button>
</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>
</html>

View File

@@ -0,0 +1,329 @@
// =====================================================
// Admin Utilities - Shared Functions for Admin Panel
// =====================================================
/**
* Show a custom confirmation dialog instead of browser confirm()
* @param {string} message - The confirmation message
* @param {Function} onConfirm - Callback when confirmed
* @param {Object} options - Optional configuration
*/
function showDeleteConfirm(message, onConfirm, options = {}) {
const {
title = "Confirm Delete",
confirmText = "Delete",
cancelText = "Cancel",
type = "danger",
} = options;
// Check if modal already exists, if not create it
let modal = document.getElementById("adminConfirmModal");
if (!modal) {
modal = document.createElement("div");
modal.id = "adminConfirmModal";
modal.className = "admin-confirm-modal";
modal.innerHTML = `
<div class="admin-confirm-overlay"></div>
<div class="admin-confirm-dialog">
<div class="admin-confirm-header">
<div class="admin-confirm-icon ${type}">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
<h3 class="admin-confirm-title">${title}</h3>
</div>
<div class="admin-confirm-body">
<p class="admin-confirm-message">${message}</p>
</div>
<div class="admin-confirm-footer">
<button type="button" class="admin-confirm-btn cancel">${cancelText}</button>
<button type="button" class="admin-confirm-btn confirm ${type}">${confirmText}</button>
</div>
</div>
`;
document.body.appendChild(modal);
// Add styles if not already present
if (!document.getElementById("adminConfirmStyles")) {
const styles = document.createElement("style");
styles.id = "adminConfirmStyles";
styles.textContent = `
.admin-confirm-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 10000;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.admin-confirm-modal.show {
opacity: 1;
visibility: visible;
}
.admin-confirm-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
}
.admin-confirm-dialog {
position: relative;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 400px;
width: 90%;
transform: scale(0.9) translateY(-20px);
transition: transform 0.2s ease;
overflow: hidden;
}
.admin-confirm-modal.show .admin-confirm-dialog {
transform: scale(1) translateY(0);
}
.admin-confirm-header {
padding: 24px 24px 16px;
text-align: center;
}
.admin-confirm-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px;
font-size: 28px;
}
.admin-confirm-icon.danger {
background: #fee2e2;
color: #dc2626;
}
.admin-confirm-icon.warning {
background: #fef3c7;
color: #d97706;
}
.admin-confirm-icon.info {
background: #dbeafe;
color: #2563eb;
}
.admin-confirm-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1a1a2e;
}
.admin-confirm-body {
padding: 0 24px 20px;
text-align: center;
}
.admin-confirm-message {
margin: 0;
color: #666;
font-size: 0.95rem;
line-height: 1.5;
}
.admin-confirm-footer {
display: flex;
gap: 12px;
padding: 16px 24px 24px;
justify-content: center;
}
.admin-confirm-btn {
padding: 10px 24px;
border-radius: 8px;
font-weight: 500;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
border: none;
}
.admin-confirm-btn.cancel {
background: #f3f4f6;
color: #374151;
}
.admin-confirm-btn.cancel:hover {
background: #e5e7eb;
}
.admin-confirm-btn.confirm {
color: white;
}
.admin-confirm-btn.confirm.danger {
background: #dc2626;
}
.admin-confirm-btn.confirm.danger:hover {
background: #b91c1c;
}
.admin-confirm-btn.confirm.warning {
background: #d97706;
}
.admin-confirm-btn.confirm.warning:hover {
background: #b45309;
}
.admin-confirm-btn.confirm.info {
background: #2563eb;
}
.admin-confirm-btn.confirm.info:hover {
background: #1d4ed8;
}
`;
document.head.appendChild(styles);
}
} else {
// Update existing modal content
modal.querySelector(
".admin-confirm-icon"
).className = `admin-confirm-icon ${type}`;
modal.querySelector(".admin-confirm-title").textContent = title;
modal.querySelector(".admin-confirm-message").textContent = message;
modal.querySelector(".admin-confirm-btn.confirm").textContent = confirmText;
modal.querySelector(
".admin-confirm-btn.confirm"
).className = `admin-confirm-btn confirm ${type}`;
modal.querySelector(".admin-confirm-btn.cancel").textContent = cancelText;
}
// Show modal
requestAnimationFrame(() => {
modal.classList.add("show");
});
// Get buttons
const confirmBtn = modal.querySelector(".admin-confirm-btn.confirm");
const cancelBtn = modal.querySelector(".admin-confirm-btn.cancel");
const overlay = modal.querySelector(".admin-confirm-overlay");
// Close function
const closeModal = () => {
modal.classList.remove("show");
};
// Remove old listeners by cloning
const newConfirmBtn = confirmBtn.cloneNode(true);
const newCancelBtn = cancelBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
// Add new listeners
newConfirmBtn.addEventListener("click", () => {
closeModal();
onConfirm();
});
newCancelBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", closeModal);
// Escape key to close
const escHandler = (e) => {
if (e.key === "Escape") {
closeModal();
document.removeEventListener("keydown", escHandler);
}
};
document.addEventListener("keydown", escHandler);
}
/**
* Show a success notification toast
* @param {string} message - The success message
*/
function showAdminToast(message, type = "success") {
// Create toast container if it doesn't exist
let container = document.getElementById("adminToastContainer");
if (!container) {
container = document.createElement("div");
container.id = "adminToastContainer";
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
z-index: 10001;
display: flex;
flex-direction: column;
gap: 10px;
`;
document.body.appendChild(container);
}
const toast = document.createElement("div");
toast.style.cssText = `
display: flex;
align-items: center;
gap: 12px;
padding: 14px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
transform: translateX(120%);
transition: transform 0.3s ease;
min-width: 280px;
border-left: 4px solid ${
type === "success" ? "#10b981" : type === "error" ? "#ef4444" : "#3b82f6"
};
`;
const icons = {
success:
'<i class="bi bi-check-circle-fill" style="color: #10b981; font-size: 1.25rem;"></i>',
error:
'<i class="bi bi-x-circle-fill" style="color: #ef4444; font-size: 1.25rem;"></i>',
info: '<i class="bi bi-info-circle-fill" style="color: #3b82f6; font-size: 1.25rem;"></i>',
};
toast.innerHTML = `
${icons[type] || icons.info}
<span style="flex: 1; color: #374151; font-size: 0.9rem;">${message}</span>
`;
container.appendChild(toast);
// Animate in
requestAnimationFrame(() => {
toast.style.transform = "translateX(0)";
});
// Auto remove after 4 seconds
setTimeout(() => {
toast.style.transform = "translateX(120%)";
setTimeout(() => {
toast.remove();
}, 300);
}, 4000);
}
/**
* Notify frontend that data has changed
* This updates a timestamp in localStorage that frontend pages check
* @param {string} dataType - Type of data changed (products, pages, settings, etc.)
*/
function notifyFrontendChange(dataType = "all") {
const timestamp = Date.now();
const changeKey = `skyartshop_change_${dataType}`;
// Store in localStorage for cross-tab communication
localStorage.setItem(changeKey, timestamp.toString());
localStorage.setItem("skyartshop_last_change", timestamp.toString());
// Also broadcast to any open frontend tabs via BroadcastChannel
try {
const channel = new BroadcastChannel("skyartshop_updates");
channel.postMessage({ type: dataType, timestamp });
channel.close();
} catch (e) {
// BroadcastChannel not supported in some browsers
}
console.log(`[Admin] Notified frontend of ${dataType} change`);
}
// Export for use in other files
window.showDeleteConfirm = showDeleteConfirm;
window.showAdminToast = showAdminToast;
window.notifyFrontendChange = notifyFrontendChange;

View File

@@ -103,98 +103,70 @@ function initializeQuillEditor() {
});
}
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;
`;
// Initialize media library
let blogMediaLibrary = null;
let galleryImages = [];
// 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 initBlogMediaLibrary() {
blogMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: false,
onSelect: function (media) {
if (media && media.path) {
document.getElementById("postFeaturedImage").value = media.path;
updateFeaturedImagePreview(media.path);
showToast("Featured image selected", "success");
}
},
});
}
function closeMediaLibrary() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
function openMediaLibraryForFeaturedImage() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = false;
blogMediaLibrary.options.onSelect = function (media) {
if (media && media.path) {
document.getElementById("postFeaturedImage").value = media.path;
updateFeaturedImagePreview(media.path);
showToast("Featured image selected", "success");
}
};
blogMediaLibrary.open();
}
function openMediaLibraryForGallery() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = true;
blogMediaLibrary.options.onSelect = function (mediaList) {
const items = Array.isArray(mediaList) ? mediaList : [mediaList];
items.forEach((media) => {
if (media && media.path && !galleryImages.includes(media.path)) {
galleryImages.push(media.path);
}
});
updateGalleryPreview();
showToast(`${items.length} image(s) added to gallery`, "success");
};
blogMediaLibrary.open();
}
function openMediaLibraryForVideo() {
if (!blogMediaLibrary) {
initBlogMediaLibrary();
}
blogMediaLibrary.options.multiple = false;
blogMediaLibrary.options.onSelect = function (media) {
if (media && media.path) {
document.getElementById("postVideoUrl").value = media.path;
updateVideoPreview(media.path);
showToast("Video selected", "success");
}
};
blogMediaLibrary.open();
}
function updateFeaturedImagePreview(url) {
@@ -202,21 +174,157 @@ function updateFeaturedImagePreview(url) {
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;" />
<img src="${url}" style="max-width: 100%; max-height: 150px; border-radius: 8px;" />
<button type="button" onclick="removeFeaturedImage()" style="position: absolute; top: -8px; right: -8px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 24px; height: 24px; cursor: pointer; font-size: 14px;">&times;</button>
</div>
`;
} else {
preview.innerHTML = "";
preview.innerHTML =
'<div class="text-muted text-center p-3"><i class="bi bi-image" style="font-size: 2rem;"></i><br><small>No image selected</small></div>';
}
}
function updateGalleryPreview() {
const preview = document.getElementById("galleryImagesPreview");
if (galleryImages.length === 0) {
preview.innerHTML =
'<div class="text-muted text-center p-3 w-100"><i class="bi bi-images" style="font-size: 2rem;"></i><br><small>No gallery images</small></div>';
return;
}
preview.innerHTML = galleryImages
.map(
(img, idx) => `
<div class="gallery-thumb">
<img src="${img}" alt="Gallery ${idx + 1}" />
<button type="button" class="remove-btn" onclick="removeGalleryImage(${idx})">&times;</button>
</div>
`,
)
.join("");
}
function removeGalleryImage(index) {
galleryImages.splice(index, 1);
updateGalleryPreview();
showToast("Image removed from gallery", "info");
}
function updateVideoPreview(url) {
const preview = document.getElementById("videoPreview");
if (url) {
const isVideo = url.match(/\.(mp4|webm|mov|avi|mkv)$/i);
if (isVideo) {
preview.innerHTML = `
<div style="position: relative; width: 100%;">
<video controls style="max-width: 100%; max-height: 200px;">
<source src="${url}" type="video/mp4">
Your browser does not support video.
</video>
<button type="button" onclick="removeVideo()" style="position: absolute; top: 5px; right: 5px; background: #dc3545; color: white; border: none; border-radius: 50%; width: 28px; height: 28px; cursor: pointer;">&times;</button>
</div>
`;
} else {
preview.innerHTML = `<div class="video-placeholder"><i class="bi bi-link-45deg"></i>${url}</div>`;
}
} else {
preview.innerHTML =
'<div class="video-placeholder"><i class="bi bi-camera-video"></i>No video selected</div>';
}
}
function removeVideo() {
document.getElementById("postVideoUrl").value = "";
document.getElementById("postExternalVideo").value = "";
updateVideoPreview("");
showToast("Video removed", "info");
}
function removeFeaturedImage() {
document.getElementById("postFeaturedImage").value = "";
updateFeaturedImagePreview("");
showToast("Featured image removed", "info");
}
// Poll functions
function togglePollSection() {
const pollSection = document.getElementById("pollSection");
const enabled = document.getElementById("enablePoll").checked;
pollSection.style.display = enabled ? "block" : "none";
}
function addPollOption() {
const container = document.getElementById("pollOptionsContainer");
const count = container.querySelectorAll(".poll-option-row").length + 1;
const row = document.createElement("div");
row.className = "input-group mb-2 poll-option-row";
row.innerHTML = `
<span class="input-group-text">${count}</span>
<input type="text" class="form-control poll-option-input" placeholder="Option ${count}" />
<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)">
<i class="bi bi-trash"></i>
</button>
`;
container.appendChild(row);
}
function removePollOption(btn) {
const row = btn.closest(".poll-option-row");
row.remove();
// Re-number options
const container = document.getElementById("pollOptionsContainer");
container.querySelectorAll(".poll-option-row").forEach((row, idx) => {
row.querySelector(".input-group-text").textContent = idx + 1;
});
}
function getPollData() {
if (!document.getElementById("enablePoll").checked) {
return null;
}
const question = document.getElementById("pollQuestion").value.trim();
const options = Array.from(document.querySelectorAll(".poll-option-input"))
.map((input) => input.value.trim())
.filter((v) => v);
if (!question || options.length < 2) {
return null;
}
return { question, options, votes: options.map(() => 0) };
}
function loadPollData(poll) {
if (poll && typeof poll === "object") {
document.getElementById("enablePoll").checked = true;
document.getElementById("pollSection").style.display = "block";
document.getElementById("pollQuestion").value = poll.question || "";
const container = document.getElementById("pollOptionsContainer");
container.innerHTML = "";
(poll.options || []).forEach((opt, idx) => {
const row = document.createElement("div");
row.className = "input-group mb-2 poll-option-row";
row.innerHTML = `
<span class="input-group-text">${idx + 1}</span>
<input type="text" class="form-control poll-option-input" value="${opt}" />
${idx >= 2 ? '<button type="button" class="btn btn-outline-danger" onclick="removePollOption(this)"><i class="bi bi-trash"></i></button>' : ""}
`;
container.appendChild(row);
});
} else {
document.getElementById("enablePoll").checked = false;
document.getElementById("pollSection").style.display = "none";
document.getElementById("pollQuestion").value = "";
document.getElementById("pollOptionsContainer").innerHTML = `
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">1</span>
<input type="text" class="form-control poll-option-input" placeholder="Option 1" />
</div>
<div class="input-group mb-2 poll-option-row">
<span class="input-group-text">2</span>
<input type="text" class="form-control poll-option-input" placeholder="Option 2" />
</div>
`;
}
}
async function loadPosts() {
try {
const response = await fetch("/api/admin/blog", { credentials: "include" });
@@ -277,17 +385,17 @@ function renderPosts(posts) {
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPost('${escapeHtml(
String(p.id)
String(p.id),
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePost('${escapeHtml(
String(p.id)
String(p.id),
)}', '${escapeHtml(p.title).replace(/'/g, "&#39;")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
</tr>`,
)
.join("");
}
@@ -297,7 +405,7 @@ function filterPosts() {
const filtered = postsData.filter(
(p) =>
p.title.toLowerCase().includes(searchTerm) ||
p.slug.toLowerCase().includes(searchTerm)
p.slug.toLowerCase().includes(searchTerm),
);
renderPosts(filtered);
}
@@ -306,9 +414,15 @@ function showCreatePost() {
document.getElementById("modalTitle").textContent = "Create Blog Post";
document.getElementById("postForm").reset();
document.getElementById("postId").value = "";
document.getElementById("postPublished").checked = false;
document.getElementById("postPublished").checked = true; // Default to published
document.getElementById("postFeaturedImage").value = "";
document.getElementById("postVideoUrl").value = "";
document.getElementById("postExternalVideo").value = "";
galleryImages = [];
updateFeaturedImagePreview("");
updateGalleryPreview();
updateVideoPreview("");
loadPollData(null);
if (quillEditor) {
quillEditor.setContents([]);
}
@@ -340,6 +454,28 @@ async function editPost(id) {
document.getElementById("postFeaturedImage").value = featuredImage;
updateFeaturedImagePreview(featuredImage);
// Set gallery images
try {
galleryImages = post.images ? JSON.parse(post.images) : [];
} catch (e) {
galleryImages = [];
}
updateGalleryPreview();
// Set video
const videoUrl = post.videourl || "";
document.getElementById("postVideoUrl").value = videoUrl;
document.getElementById("postExternalVideo").value = "";
updateVideoPreview(videoUrl);
// Set poll
try {
const poll = post.poll ? JSON.parse(post.poll) : null;
loadPollData(poll);
} catch (e) {
loadPollData(null);
}
document.getElementById("postMetaTitle").value = post.metatitle || "";
document.getElementById("postMetaDescription").value =
post.metadescription || "";
@@ -359,12 +495,24 @@ async function savePost() {
// Get content from Quill editor
const content = quillEditor ? quillEditor.root.innerHTML : "";
// Get video URL (prefer uploaded, then external)
let videoUrl = document.getElementById("postVideoUrl").value;
if (!videoUrl) {
videoUrl = document.getElementById("postExternalVideo").value;
}
// Get poll data
const poll = getPollData();
const formData = {
title: document.getElementById("postTitle").value,
slug: document.getElementById("postSlug").value,
excerpt: document.getElementById("postExcerpt").value,
content: content,
featuredimage: document.getElementById("postFeaturedImage").value,
images: JSON.stringify(galleryImages),
videourl: videoUrl,
poll: poll ? JSON.stringify(poll) : null,
metatitle: document.getElementById("postMetaTitle").value,
metadescription: document.getElementById("postMetaDescription").value,
ispublished: document.getElementById("postPublished").checked,
@@ -389,7 +537,7 @@ async function savePost() {
if (data.success) {
showToast(
id ? "Post updated successfully" : "Post created successfully",
"success"
"success",
);
postModal.hide();
loadPosts();
@@ -403,23 +551,30 @@ async function savePost() {
}
async function deletePost(id, title) {
if (!confirm(`Are you sure you want to delete "${title}"?`)) return;
try {
const response = await fetch(`/api/admin/blog/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
showToast("Post deleted successfully", "success");
loadPosts();
} else {
showToast(data.message || "Failed to delete post", "error");
}
} catch (error) {
console.error("Failed to delete post:", error);
showToast("Failed to delete post", "error");
}
showDeleteConfirm(
`Are you sure you want to delete "${title}"? This action cannot be undone.`,
async () => {
try {
const response = await fetch(`/api/admin/blog/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
// Immediately remove from local array and re-render
postsData = postsData.filter((p) => String(p.id) !== String(id));
renderPosts(postsData);
showToast("Post deleted successfully", "success");
} else {
showToast(data.message || "Failed to delete post", "error");
}
} catch (error) {
console.error("Failed to delete post:", error);
showToast("Failed to delete post", "error");
}
},
{ title: "Delete Blog Post", confirmText: "Delete Post" },
);
}
function showToast(message, type = "info") {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,582 @@
// 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", {
credentials: "include",
});
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 =
homepageData.hero.enabled !== false;
document.getElementById("heroHeadline").value =
homepageData.hero.headline || "";
document.getElementById("heroSubheading").value =
homepageData.hero.subheading || "";
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");
}
// Promotion Section
if (homepageData.promotion) {
document.getElementById("promotionEnabled").checked =
homepageData.promotion.enabled !== false;
document.getElementById("promotionTitle").value =
homepageData.promotion.title || "";
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");
}
// Portfolio Section
if (homepageData.portfolio) {
document.getElementById("portfolioEnabled").checked =
homepageData.portfolio.enabled !== false;
document.getElementById("portfolioTitle").value =
homepageData.portfolio.title || "";
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) {
const enabled = document.getElementById(`${sectionName}Enabled`).checked;
const section = document.getElementById(`${sectionName}Section`);
const content = section.querySelector(".section-content");
if (enabled) {
section.classList.remove("disabled");
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, button, select").forEach((el) => {
el.disabled = true;
});
// Disable Quill editor
if (quillEditors[sectionName]) {
quillEditors[sectionName].disable();
}
}
}
// Open media library in a modal
function openMediaLibrary(section, field) {
currentMediaPicker = { section, field };
// 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 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: 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: 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: quillEditors.portfolio.root.innerHTML,
count: parseInt(document.getElementById("portfolioCount").value) || 6,
},
};
try {
const response = await fetch("/api/admin/homepage/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(settings),
});
const data = await response.json();
if (data.success) {
showSuccess(
"Homepage settings saved successfully! Changes are now live on the frontend."
);
homepageData = settings;
} else {
showError(data.message || "Failed to save homepage settings");
}
} catch (error) {
console.error("Failed to save homepage:", error);
showError("Failed to save homepage settings");
}
}
function showSuccess(message) {
const alert = document.createElement("div");
alert.className =
"alert alert-success alert-dismissible fade show position-fixed";
alert.style.cssText =
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(alert);
setTimeout(() => alert.remove(), 5000);
}
function showError(message) {
const alert = document.createElement("div");
alert.className =
"alert alert-danger alert-dismissible fade show position-fixed";
alert.style.cssText =
"top: 20px; right: 20px; z-index: 9999; min-width: 300px;";
alert.innerHTML = `
${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

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,29 @@ let pageModal;
let quillEditor;
let aboutContentEditor;
let aboutTeamMembers = [];
let deletedTeamMemberIds = []; // Track deleted team members for database deletion
let currentMediaPicker = null;
let pagesMediaLibrary = null;
// Initialize pages media library
function initPagesMediaLibrary() {
if (typeof MediaLibrary !== "undefined" && !pagesMediaLibrary) {
pagesMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: false,
onSelect: function (media) {
handleMediaSelection(media);
},
});
}
}
document.addEventListener("DOMContentLoaded", function () {
pageModal = new bootstrap.Modal(document.getElementById("pageModal"));
initializeQuillEditor();
initializeAboutEditor();
initPagesMediaLibrary();
checkAuth().then((authenticated) => {
if (authenticated) {
loadPages();
@@ -30,13 +47,6 @@ document.addEventListener("DOMContentLoaded", function () {
});
});
// Media Library Selection Handler
window.addEventListener("message", function (event) {
if (event.data.type === "mediaSelected" && currentMediaPicker) {
handleMediaSelection(event.data.media);
}
});
function initializeQuillEditor() {
quillEditor = new Quill("#pageContentEditor", {
theme: "snow",
@@ -57,6 +67,31 @@ function initializeQuillEditor() {
],
},
});
// Custom image handler to use media library
const toolbar = quillEditor.getModule("toolbar");
toolbar.addHandler("image", function () {
openMediaLibraryForPageEditor();
});
}
// Open media library for main page editor image insertion
function openMediaLibraryForPageEditor() {
if (!pagesMediaLibrary) {
initPagesMediaLibrary();
}
currentMediaPicker = "pageEditorImage";
pagesMediaLibrary.show();
}
// Handle media selection for main page editor
function handlePageEditorImageSelection(media) {
if (quillEditor && media && media.path) {
const range = quillEditor.getSelection(true);
quillEditor.insertEmbed(range.index, "image", media.path);
quillEditor.setSelection(range.index + 1);
}
}
function initializeAboutEditor() {
@@ -75,6 +110,31 @@ function initializeAboutEditor() {
},
placeholder: "Write your page content here...",
});
// Custom image handler to use media library
const toolbar = aboutContentEditor.getModule("toolbar");
toolbar.addHandler("image", function () {
openMediaLibraryForAboutEditor();
});
}
// Open media library for About editor image insertion
function openMediaLibraryForAboutEditor() {
if (!pagesMediaLibrary) {
initPagesMediaLibrary();
}
currentMediaPicker = "aboutEditorImage";
pagesMediaLibrary.show();
}
// Handle media selection for About editor
function handleAboutEditorImageSelection(media) {
if (aboutContentEditor && media && media.path) {
const range = aboutContentEditor.getSelection(true);
aboutContentEditor.insertEmbed(range.index, "image", media.path);
aboutContentEditor.setSelection(range.index + 1);
}
}
async function loadPages() {
@@ -120,17 +180,17 @@ function renderPages(pages) {
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editPage('${escapeHtml(
p.id
p.id,
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deletePage('${escapeHtml(
p.id
p.id,
)}', '${escapeHtml(p.title).replace(/'/g, "\\'")}')">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
</tr>`,
)
.join("");
}
@@ -140,7 +200,7 @@ function filterPages() {
const filtered = pagesData.filter(
(p) =>
p.title.toLowerCase().includes(searchTerm) ||
p.slug.toLowerCase().includes(searchTerm)
p.slug.toLowerCase().includes(searchTerm),
);
renderPages(filtered);
}
@@ -155,6 +215,7 @@ function showCreatePage() {
// Show regular editor by default
document.getElementById("contactStructuredFields").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("privacyContentSection").style.display = "none";
document.getElementById("regularContentEditor").style.display = "block";
pageModal.show();
@@ -203,6 +264,40 @@ async function editPage(id) {
) {
// Show About page with team members
await showAboutWithTeamFields(page);
} else if (
page.slug === "privacy" ||
page.slug === "page-privacy" ||
page.slug.includes("privacy") ||
page.slug === "shipping" ||
page.slug === "shipping-info" ||
page.slug.includes("shipping") ||
page.slug === "returns" ||
page.slug.includes("return")
) {
// Show Privacy/Shipping/Returns page with structured fields
if (page.pagedata) {
showPrivacyStructuredFields(page.pagedata);
} else {
// No pagedata, use regular editor
document.getElementById("contactStructuredFields").style.display =
"none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("privacyContentSection").style.display =
"none";
document.getElementById("regularContentEditor").style.display =
"block";
if (page.content) {
try {
const delta = JSON.parse(page.content);
quillEditor.setContents(delta);
} catch {
quillEditor.clipboard.dangerouslyPasteHTML(page.content);
}
} else {
quillEditor.setContents([]);
}
}
} else {
// Use regular Quill editor for all other pages (privacy, etc)
document.getElementById("contactStructuredFields").style.display =
@@ -286,7 +381,7 @@ function renderBusinessHours(hours) {
</button>
</div>
</div>
`
`,
)
.join("");
}
@@ -321,6 +416,124 @@ function removeBusinessHour(index) {
}
}
// Privacy Policy Page Functions
let privacySectionEditors = []; // Array to store Quill editors for each section
function showPrivacyStructuredFields(pagedata) {
// Hide regular editor, show privacy fields
document.getElementById("regularContentEditor").style.display = "none";
document.getElementById("aboutWithTeamFields").style.display = "none";
document.getElementById("contactStructuredFields").style.display = "none";
document.getElementById("privacyContentSection").style.display = "block";
// Populate header fields
if (pagedata.header) {
document.getElementById("privacyHeaderTitle").value =
pagedata.header.title || "";
}
// Populate last updated
document.getElementById("privacyLastUpdated").value =
pagedata.lastUpdated || "";
// Populate sections
if (pagedata.sections && pagedata.sections.length > 0) {
renderPrivacySections(pagedata.sections);
} else {
// Start with one empty section
privacySectionEditors = [];
document.getElementById("privacySectionsContainer").innerHTML = "";
}
}
function renderPrivacySections(sections) {
const container = document.getElementById("privacySectionsContainer");
privacySectionEditors = []; // Reset editors array
container.innerHTML = "";
sections.forEach((section, index) => {
addPrivacySectionWithData(section, index);
});
}
function addPrivacySection() {
addPrivacySectionWithData(
{ title: "", content: "" },
privacySectionEditors.length,
);
}
function addPrivacySectionWithData(section, index) {
const container = document.getElementById("privacySectionsContainer");
const sectionDiv = document.createElement("div");
sectionDiv.className = "contact-field-group mb-4";
sectionDiv.setAttribute("data-section-index", index);
// Create unique IDs for this section's editor
const editorId = `privacySectionContent_${index}`;
sectionDiv.innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-3">
<h6><i class="bi bi-file-text"></i> Section ${index + 1}</h6>
<button type="button" class="btn btn-sm btn-danger" onclick="removePrivacySection(${index})">
<i class="bi bi-trash"></i> Remove
</button>
</div>
<div class="mb-3">
<label class="form-label">Section Title</label>
<input type="text" class="form-control"
value="${escapeHtml(section.title || "")}"
data-field="title"
placeholder="e.g., Information We Collect">
</div>
<div class="mb-3">
<label class="form-label">Content</label>
<div class="editor-wrapper">
<div id="${editorId}" style="min-height: 200px;"></div>
</div>
</div>
`;
container.appendChild(sectionDiv);
// Initialize Quill editor for this section's content
const editor = new Quill(`#${editorId}`, {
theme: "snow",
modules: {
toolbar: [
[{ header: [2, 3, false] }],
["bold", "italic", "underline"],
[{ list: "ordered" }, { list: "bullet" }],
["link"],
["clean"],
],
},
});
// Set the content if provided
if (section.content) {
try {
const delta = JSON.parse(section.content);
editor.setContents(delta);
} catch {
// If it's plain text/HTML, set it directly
editor.clipboard.dangerouslyPasteHTML(section.content);
}
}
privacySectionEditors[index] = editor;
}
function removePrivacySection(index) {
const container = document.getElementById("privacySectionsContainer");
const sectionDiv = container.querySelector(`[data-section-index="${index}"]`);
if (sectionDiv) {
sectionDiv.remove();
// Remove editor from array (set to null to keep indices)
privacySectionEditors[index] = null;
}
}
// About Page with Team Members Functions
async function showAboutWithTeamFields(page) {
// Hide other editors
@@ -346,7 +559,12 @@ async function showAboutWithTeamFields(page) {
async function loadTeamMembersForAbout() {
try {
const response = await fetch("/api/admin/team-members");
// Reset the deleted IDs when loading fresh data
deletedTeamMemberIds = [];
const response = await fetch("/api/admin/team-members", {
credentials: "include",
});
const data = await response.json();
if (data.success && data.teamMembers) {
aboutTeamMembers = data.teamMembers;
@@ -387,7 +605,7 @@ function displayTeamMembersInEditor() {
${
member.image_url
? `<img src="${member.image_url}" alt="${escapeHtml(
member.name
member.name,
)}" />`
: `<i class="bi bi-person-circle"></i>`
}
@@ -409,8 +627,8 @@ function displayTeamMembersInEditor() {
rows="2"
placeholder="Bio"
onchange="updateTeamMember(${index}, 'bio', this.value)">${escapeHtml(
member.bio || ""
)}</textarea>
member.bio || "",
)}</textarea>
</div>
<div class="mb-2">
<div class="input-group input-group-sm">
@@ -431,7 +649,7 @@ function displayTeamMembersInEditor() {
</div>
</div>
</div>
`
`,
)
.join("");
}
@@ -456,117 +674,71 @@ function updateTeamMember(index, field, value) {
}
function removeTeamMemberFromAbout(index) {
const member = aboutTeamMembers[index];
// If member has an ID, track it for deletion from database
if (member && member.id) {
deletedTeamMemberIds.push(member.id);
}
aboutTeamMembers.splice(index, 1);
displayTeamMembersInEditor();
}
function selectImageForMember(index) {
currentMediaPicker = { purpose: "teamMember", index };
openMediaLibraryModal();
}
function openMediaLibraryModal() {
// 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;
`;
// Initialize if not already
initPagesMediaLibrary();
// 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 = closeMediaLibraryModal;
// 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) {
closeMediaLibraryModal();
}
};
}
function closeMediaLibraryModal() {
const backdrop = document.getElementById("mediaLibraryBackdrop");
if (backdrop) {
backdrop.remove();
if (pagesMediaLibrary) {
pagesMediaLibrary.open();
}
currentMediaPicker = null;
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
// Handle About editor image insertion
if (currentMediaPicker === "aboutEditorImage") {
handleAboutEditorImageSelection(media);
currentMediaPicker = null;
return;
}
// Handle main page editor image insertion
if (currentMediaPicker === "pageEditorImage") {
handlePageEditorImageSelection(media);
currentMediaPicker = null;
return;
}
if (currentMediaPicker.purpose === "teamMember") {
const index = currentMediaPicker.index;
if (aboutTeamMembers[index]) {
// Media is an array, get the first item's URL
const selectedMedia = Array.isArray(media) ? media[0] : media;
aboutTeamMembers[index].image_url = selectedMedia.url;
aboutTeamMembers[index].image_url = media.path;
displayTeamMembersInEditor();
}
}
closeMediaLibraryModal();
currentMediaPicker = null;
}
async function saveTeamMembers() {
try {
// First, delete any removed team members from the database
for (const memberId of deletedTeamMemberIds) {
try {
await fetch(`/api/admin/team-members/${memberId}`, {
method: "DELETE",
credentials: "include",
});
console.log(`Deleted team member ${memberId}`);
} catch (err) {
console.error(`Failed to delete team member ${memberId}:`, err);
}
}
// Clear the deleted IDs array after processing
deletedTeamMemberIds = [];
// Save or update each team member
for (const member of aboutTeamMembers) {
if (!member.name || !member.position) continue; // Skip incomplete members
@@ -584,6 +756,7 @@ async function saveTeamMembers() {
await fetch(`/api/admin/team-members/${member.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
} else {
@@ -591,6 +764,7 @@ async function saveTeamMembers() {
const response = await fetch("/api/admin/team-members", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(payload),
});
const data = await response.json();
@@ -599,6 +773,7 @@ async function saveTeamMembers() {
}
}
}
console.log("Team members saved successfully");
} catch (error) {
console.error("Error saving team members:", error);
}
@@ -663,6 +838,52 @@ async function savePage() {
formData.pagedata = pagedata;
formData.content = generatedHTML; // Store HTML in content field
formData.contenthtml = generatedHTML; // Also in contenthtml
}
// Check if this is privacy/shipping/returns page with structured fields
else if (
(slug.includes("privacy") ||
slug.includes("shipping") ||
slug.includes("return")) &&
document.getElementById("privacyContentSection").style.display !== "none"
) {
// Collect structured privacy data
const pagedata = {
header: {
title: document.getElementById("privacyHeaderTitle").value,
},
lastUpdated: document.getElementById("privacyLastUpdated").value,
sections: [],
};
// Collect sections
const sectionDivs = document.getElementById(
"privacySectionsContainer",
).children;
for (let i = 0; i < sectionDivs.length; i++) {
const sectionDiv = sectionDivs[i];
const title = sectionDiv.querySelector('[data-field="title"]').value;
const editor = privacySectionEditors[i];
if (editor && title) {
// Get content as Delta JSON
const contentDelta = editor.getContents();
const contentHTML = editor.root.innerHTML;
pagedata.sections.push({
title: title,
content: JSON.stringify(contentDelta), // Store as Delta
contentHTML: contentHTML, // Also store rendered HTML
});
}
}
// Store the structured data
formData.pagedata = pagedata;
// Also generate and store as content for backwards compatibility
const generatedHTML = generatePrivacyHTML(pagedata);
formData.content = generatedHTML;
formData.contenthtml = generatedHTML;
} else {
// Use Quill editor content for other pages
const contentDelta = quillEditor.getContents();
@@ -695,7 +916,7 @@ async function savePage() {
const data = await response.json();
if (data.success) {
showSuccess(
id ? "Page updated successfully" : "Page created successfully"
id ? "Page updated successfully" : "Page created successfully",
);
pageModal.hide();
loadPages();
@@ -717,11 +938,11 @@ function generateContactHTML(pagedata) {
(hour) => `
<div>
<p style="font-weight: 600; margin-bottom: 8px;">${escapeHtml(
hour.days
hour.days,
)}</p>
<p style="opacity: 0.95; margin: 0;">${escapeHtml(hour.hours)}</p>
</div>
`
`,
)
.join("");
@@ -743,7 +964,7 @@ function generateContactHTML(pagedata) {
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Phone</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.phone
contactInfo.phone,
)}</p>
</div>
@@ -754,7 +975,7 @@ function generateContactHTML(pagedata) {
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Email</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.email
contactInfo.email,
)}</p>
</div>
@@ -765,7 +986,7 @@ function generateContactHTML(pagedata) {
</div>
<h3 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;">Location</h3>
<p style="font-size: 1rem; opacity: 0.9; margin: 0;">${escapeHtml(
contactInfo.address
contactInfo.address,
)}</p>
</div>
</div>
@@ -780,11 +1001,43 @@ function generateContactHTML(pagedata) {
`;
}
function generatePrivacyHTML(pagedata) {
const { header, lastUpdated, sections } = pagedata;
// Generate sections HTML
const sectionsHTML = sections
.map((section) => {
// Parse the content Delta if it's stored as JSON
let contentHTML = section.contentHTML;
if (!contentHTML && section.content) {
try {
// If we don't have contentHTML, try to get it from the content field
contentHTML = section.content;
} catch {
contentHTML = section.content;
}
}
return `
<h2>${escapeHtml(section.title)}</h2>
<div class="section-content">${contentHTML}</div>
`;
})
.join("");
return `
<div style="max-width: 900px; margin: 0 auto;">
${lastUpdated ? `<p class="policy-meta" style="color: #636e72; font-style: italic; margin-bottom: 24px;">Last updated: ${escapeHtml(lastUpdated)}</p>` : ""}
${sectionsHTML}
</div>
`;
}
async function deletePage(id, title) {
// Show custom confirmation modal instead of browser confirm
showConfirmation(
`Are you sure you want to delete "<strong>${escapeHtml(
title
title,
)}</strong>"?<br><br>` +
`<small class="text-muted">This action cannot be undone.</small>`,
async () => {
@@ -804,7 +1057,7 @@ async function deletePage(id, title) {
console.error("Failed to delete page:", error);
showError("Failed to delete page");
}
}
},
);
}
@@ -845,7 +1098,7 @@ function showError(message) {
function showNotification(message, type) {
const modal = new bootstrap.Modal(
document.getElementById("notificationModal")
document.getElementById("notificationModal"),
);
const modalContent = document.getElementById("notificationModalContent");
const modalHeader = document.getElementById("notificationModalHeader");
@@ -863,7 +1116,7 @@ function showNotification(message, type) {
modalIcon.className = "bi bi-check-circle-fill me-2";
modalTitleText.textContent = "Success";
modalBody.innerHTML = `<p class="mb-0"><i class="bi bi-check-circle text-success me-2"></i>${escapeHtml(
message
message,
)}</p>`;
} else {
modalContent.classList.remove("border-success");
@@ -874,7 +1127,7 @@ function showNotification(message, type) {
modalIcon.className = "bi bi-exclamation-triangle-fill me-2";
modalTitleText.textContent = "Error";
modalBody.innerHTML = `<p class="mb-0"><i class="bi bi-x-circle text-danger me-2"></i>${escapeHtml(
message
message,
)}</p>`;
}
@@ -1061,7 +1314,7 @@ function toggleContentExpand(editorType) {
"Resize handle clicked! Target:",
targetId,
"Element found:",
!!targetElement
!!targetElement,
);
if (!targetElement) {
@@ -1092,7 +1345,7 @@ function toggleContentExpand(editorType) {
const deltaY = e.clientY - resizeState.startY;
const newHeight = Math.max(
200,
Math.min(1200, resizeState.startHeight + deltaY)
Math.min(1200, resizeState.startHeight + deltaY),
);
// Update target element height
@@ -1110,7 +1363,7 @@ function toggleContentExpand(editorType) {
"pageContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar
!!toolbar,
);
if (editor && toolbar) {
@@ -1123,7 +1376,7 @@ function toggleContentExpand(editorType) {
"editor:",
editorHeight,
"total:",
newHeight
newHeight,
);
resizeState.target.style.height = editorHeight + "px";
@@ -1147,7 +1400,7 @@ function toggleContentExpand(editorType) {
"aboutContentEditor resize - editor:",
!!editor,
"toolbar:",
!!toolbar
!!toolbar,
);
if (editor && toolbar) {
@@ -1160,7 +1413,7 @@ function toggleContentExpand(editorType) {
"editor:",
editorHeight,
"total:",
newHeight
newHeight,
);
resizeState.target.style.height = editorHeight + "px";

View File

@@ -6,6 +6,20 @@ let quillEditor;
let portfolioImages = [];
let currentMediaPicker = null;
let isModalExpanded = false;
let portfolioMediaLibrary = null;
// Initialize portfolio media library
function initPortfolioMediaLibrary() {
if (typeof MediaLibrary !== "undefined" && !portfolioMediaLibrary) {
portfolioMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: true, // Allow multiple image selection for portfolio gallery
onSelect: function (media) {
handleMediaSelection(media);
},
});
}
}
document.addEventListener("DOMContentLoaded", function () {
projectModal = new bootstrap.Modal(document.getElementById("projectModal"));
@@ -19,6 +33,9 @@ document.addEventListener("DOMContentLoaded", function () {
// Initialize Quill editor
initializeQuillEditor();
// Initialize media library
initPortfolioMediaLibrary();
checkAuth().then((authenticated) => {
if (authenticated) {
loadProjects();
@@ -123,7 +140,7 @@ async function loadProjects() {
title: p.title,
isactive: p.isactive,
isactiveType: typeof p.isactive,
}))
})),
);
renderProjects(projectsData);
}
@@ -152,7 +169,7 @@ function renderProjects(projects) {
console.log(
`Project ${p.id}: isactive =`,
p.isactive,
`(type: ${typeof p.isactive})`
`(type: ${typeof p.isactive})`,
);
const isActive =
p.isactive === true || p.isactive === "true" || p.isactive === 1;
@@ -174,12 +191,12 @@ function renderProjects(projects) {
<td>${formatDate(p.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editProject('${escapeHtml(
String(p.id)
String(p.id),
)}')">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteProject('${escapeHtml(
String(p.id)
String(p.id),
)}', '${escapeHtml(p.title).replace(/'/g, "&#39;")}')">
<i class="bi bi-trash"></i>
</button>
@@ -192,7 +209,7 @@ function renderProjects(projects) {
function filterProjects() {
const searchTerm = document.getElementById("searchInput").value.toLowerCase();
const filtered = projectsData.filter((p) =>
p.title.toLowerCase().includes(searchTerm)
p.title.toLowerCase().includes(searchTerm),
);
renderProjects(filtered);
}
@@ -237,10 +254,31 @@ async function editProject(id) {
document.getElementById("projectCategory").value = project.category || "";
document.getElementById("projectActive").checked = project.isactive;
// Load images if available (imageurl field or parse from description)
// Load images - check images array first, then fall back to imageurl
portfolioImages = [];
if (project.imageurl) {
// If single image URL exists
// Try to parse images array
if (project.images) {
try {
const imagesArr =
typeof project.images === "string"
? JSON.parse(project.images)
: project.images;
if (Array.isArray(imagesArr) && imagesArr.length > 0) {
imagesArr.forEach((url) => {
portfolioImages.push({
url: url,
filename: url.split("/").pop(),
});
});
}
} catch (e) {
console.warn("Failed to parse images:", e);
}
}
// Fall back to imageurl if no images array
if (portfolioImages.length === 0 && project.imageurl) {
portfolioImages.push({
url: project.imageurl,
filename: project.imageurl.split("/").pop(),
@@ -286,6 +324,7 @@ async function saveProject() {
method: method,
headers: { "Content-Type": "application/json" },
credentials: "include",
cache: "no-cache",
body: JSON.stringify(formData),
});
@@ -294,9 +333,15 @@ async function saveProject() {
showSuccess(
id
? "Project updated successfully! 🎉"
: "Project created successfully! 🎉"
: "Project created successfully! 🎉",
);
projectModal.hide();
// Immediately add to local data and re-render for instant feedback
if (!id && data.project) {
projectsData.unshift(data.project);
renderProjects(projectsData);
}
// Also reload from server to ensure full sync
loadProjects();
} else {
showError(data.message || "Failed to save project");
@@ -308,23 +353,33 @@ async function saveProject() {
}
async function deleteProject(id, name) {
if (!confirm(`Are you sure you want to delete "${name}"?`)) return;
try {
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
showSuccess("Project deleted successfully");
loadProjects();
} else {
showError(data.message || "Failed to delete project");
}
} catch (error) {
console.error("Failed to delete project:", error);
showError("Failed to delete project");
}
showDeleteConfirm(
`Are you sure you want to delete "${name}"? This action cannot be undone.`,
async () => {
try {
const response = await fetch(`/api/admin/portfolio/projects/${id}`, {
method: "DELETE",
credentials: "include",
cache: "no-cache",
});
const data = await response.json();
if (data.success) {
showSuccess("Project deleted successfully");
// Remove immediately from local data and re-render
// Compare as strings to handle type mismatches
const deletedId = String(id);
projectsData = projectsData.filter((p) => String(p.id) !== deletedId);
renderProjects(projectsData);
} else {
showError(data.message || "Failed to delete project");
}
} catch (error) {
console.error("Failed to delete project:", error);
showError("Failed to delete project");
}
},
{ title: "Delete Project", confirmText: "Delete Project" },
);
}
function escapeHtml(text) {
@@ -380,7 +435,7 @@ function renderPortfolioImages() {
<i class="bi bi-x"></i>
</button>
</div>
`
`,
)
.join("");
}
@@ -395,100 +450,30 @@ function removePortfolioImage(index) {
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;
`;
// Initialize if not already
initPortfolioMediaLibrary();
// 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();
if (portfolioMediaLibrary) {
portfolioMediaLibrary.open();
}
currentMediaPicker = null;
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
if (currentMediaPicker.purpose === "portfolioImages") {
// Handle multiple images
// Handle multiple images - media can be array or single object
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)) {
const itemUrl = item.path || item.url;
if (!portfolioImages.find((img) => img.url === itemUrl)) {
portfolioImages.push({
url: item.url,
filename: item.filename || item.url.split("/").pop(),
url: itemUrl,
filename:
item.filename || item.originalName || itemUrl.split("/").pop(),
});
}
});
@@ -497,7 +482,7 @@ function handleMediaSelection(media) {
showSuccess(`${mediaArray.length} image(s) added to portfolio gallery`);
}
closeMediaLibrary();
currentMediaPicker = null;
}
// Toast Notification System

View File

@@ -87,8 +87,10 @@ function initializeQuillEditor() {
// Load all products
async function loadProducts() {
try {
const response = await fetch("/api/admin/products", {
// Add cache-busting to ensure fresh data
const response = await fetch(`/api/admin/products?_t=${Date.now()}`, {
credentials: "include",
cache: "no-store",
});
const data = await response.json();
@@ -459,7 +461,7 @@ function renderImageVariants() {
container.querySelectorAll('[data-action="remove"]').forEach((btn) => {
btn.addEventListener("click", (e) => {
const id = e.currentTarget.dataset.variantId;
imageVariants = imageVariants.filter((v) => v.id !== id);
imageVariants = imageVariants.filter((v) => String(v.id) !== String(id));
renderImageVariants();
});
});
@@ -469,7 +471,11 @@ function renderImageVariants() {
item.addEventListener("click", (e) => {
const variantId = e.currentTarget.dataset.variantId;
const imageUrl = e.currentTarget.dataset.imageUrl;
const variant = imageVariants.find((v) => v.id === variantId);
const variant = imageVariants.find(
(v) => String(v.id) === String(variantId)
);
console.log("Image picker clicked:", { variantId, imageUrl, variant });
if (variant) {
variant.image_url = imageUrl;
@@ -480,16 +486,28 @@ function renderImageVariants() {
.querySelectorAll(".image-picker-item")
.forEach((i) => i.classList.remove("selected"));
e.currentTarget.classList.add("selected");
console.log("Updated variant with new image:", variant);
}
});
});
// Add event listeners for input changes
container.querySelectorAll("[data-variant-id]").forEach((input) => {
input.addEventListener("input", (e) => {
// Use 'input' for text/number fields, 'change' for radio/checkbox
const eventType =
input.type === "radio" || input.type === "checkbox" ? "change" : "input";
input.addEventListener(eventType, (e) => {
const id = e.target.dataset.variantId;
const field = e.target.dataset.field;
const variant = imageVariants.find((v) => v.id === id);
const variant = imageVariants.find((v) => String(v.id) === String(id));
console.log("Input change:", {
id,
field,
value: e.target.value,
variant,
});
if (variant) {
if (field === "color_code_text") {
@@ -517,6 +535,7 @@ function renderImageVariants() {
} else {
variant[field] = e.target.value;
}
console.log("Updated variant:", variant);
}
});
});
@@ -564,7 +583,11 @@ async function editProduct(id) {
product.isbestseller || false;
// Load image variants and extract unique product images
imageVariants = product.images || [];
// Convert numeric IDs to strings for consistency with newly created variants
imageVariants = (product.images || []).map((img) => ({
...img,
id: String(img.id),
}));
console.log("Loaded image variants:", imageVariants);
// Build productImages array from unique image URLs in variants
@@ -749,6 +772,9 @@ async function saveProduct() {
: "✅ Product Created Successfully! Now visible on your shop page."
);
// Notify frontend of product changes
notifyFrontendChange("products");
// Wait a moment then close modal
setTimeout(async () => {
productModal.hide();
@@ -783,141 +809,91 @@ async function saveProduct() {
// Delete product
async function deleteProduct(id, name) {
if (!confirm(`Are you sure you want to delete "${name}"?`)) {
return;
}
showDeleteConfirm(
`Are you sure you want to delete "${name}"? This action cannot be undone.`,
async () => {
try {
// Immediately remove from UI for instant feedback
const row = document
.querySelector(`tr button[data-id="${id}"]`)
?.closest("tr");
if (row) {
row.style.opacity = "0.5";
row.style.pointerEvents = "none";
}
try {
const response = await fetch(`/api/admin/products/${id}`, {
method: "DELETE",
credentials: "include",
});
const response = await fetch(`/api/admin/products/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
if (data.success) {
showSuccess("Product deleted successfully");
loadProducts();
} else {
showError(data.message || "Failed to delete product");
}
} catch (error) {
console.error("Failed to delete product:", error);
showError("Failed to delete product");
}
const data = await response.json();
if (data.success) {
// Remove from local array immediately
productsData = productsData.filter((p) => p.id !== id);
// Re-render without waiting for server
renderProducts(productsData);
showSuccess("Product deleted successfully");
// Also trigger frontend cache invalidation
notifyFrontendChange("products");
} else {
// Restore row if delete failed
if (row) {
row.style.opacity = "1";
row.style.pointerEvents = "auto";
}
showError(data.message || "Failed to delete product");
}
} catch (error) {
console.error("Delete error:", error);
showError("Failed to delete product");
// Reload to restore state
loadProducts();
}
},
{ title: "Delete Product", confirmText: "Delete Product" }
);
}
// ===== 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);
}
});
let productMediaLibrary = null;
function initProductMediaLibrary() {
productMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: true,
onSelect: handleMediaSelection,
});
}
// 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();
if (!productMediaLibrary) {
initProductMediaLibrary();
}
currentMediaPicker = null;
productMediaLibrary.open();
}
function handleMediaSelection(media) {
if (!currentMediaPicker) return;
if (currentMediaPicker.purpose === "productImage") {
// Handle multiple images
// Handle multiple images - media is array in multi-select mode
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)) {
// Check if image already exists - use path instead of url
if (!productImages.find((img) => img.url === item.path)) {
productImages.push({
url: item.url,
alt_text: item.filename || "",
filename: item.filename,
url: item.path,
alt_text: item.name || "",
filename: item.name,
});
}
});
@@ -926,7 +902,7 @@ function handleMediaSelection(media) {
showSuccess(`${mediaArray.length} image(s) added to product gallery`);
}
closeMediaLibrary();
currentMediaPicker = null;
}
// ===== UTILITY FUNCTIONS =====

View File

@@ -1,28 +1,41 @@
// Settings Management JavaScript
let currentSettings = {};
let mediaLibraryModal;
let settingsMediaLibrary = null;
let currentMediaTarget = null;
let selectedMediaUrl = null;
let allMedia = [];
// Initialize settings media library
function initSettingsMediaLibrary() {
if (typeof MediaLibrary !== "undefined" && !settingsMediaLibrary) {
settingsMediaLibrary = new MediaLibrary({
selectMode: true,
multiple: false,
onSelect: function (media) {
if (!currentMediaTarget) return;
// Set the selected URL to the target field
document.getElementById(currentMediaTarget).value = media.path;
// Update preview
if (currentMediaTarget === "siteLogo") {
document.getElementById(
"logoPreview"
).innerHTML = `<img src="${media.path}" alt="Logo" />`;
} else if (currentMediaTarget === "siteFavicon") {
document.getElementById(
"faviconPreview"
).innerHTML = `<img src="${media.path}" alt="Favicon" />`;
}
showToast("Image selected successfully", "success");
},
});
}
}
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);
}
// Initialize media library
initSettingsMediaLibrary();
// Load saved theme
loadTheme();
@@ -251,153 +264,25 @@ function populateSettings() {
}
// Media Library Functions - Make global for onclick handlers
window.openMediaLibrary = async function (targetField) {
window.openMediaLibrary = function (targetField) {
console.log("openMediaLibrary called for:", targetField);
currentMediaTarget = targetField;
selectedMediaUrl = null;
// 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");
// Initialize if not already
initSettingsMediaLibrary();
if (settingsMediaLibrary) {
settingsMediaLibrary.open();
} else {
showToast("Media library not available", "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");
// This is now handled by the MediaLibrary component's onSelect callback
showToast("Please click on an image to select it", "info");
};
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;

View File

@@ -12,6 +12,7 @@ const rolePermissions = {
"View Reports",
"View Financial Data",
],
Sales: ["Manage Products", "Manage Orders", "View Reports"],
Admin: [
"Manage Products",
"Manage Portfolio",
@@ -19,14 +20,8 @@ const rolePermissions = {
"Manage Pages",
"Manage Users",
"View Reports",
],
MasterAdmin: [
"Full System Access",
"Manage Settings",
"Manage Users",
"Manage All Content",
"View Logs",
"System Configuration",
],
};
@@ -85,22 +80,22 @@ function renderUsers(users) {
<td>${formatDate(u.createdat)}</td>
<td>
<button class="btn btn-sm btn-info" onclick="editUser('${escapeHtml(
u.id
u.id,
)}')" title="Edit User">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-warning" onclick="showChangePassword('${escapeHtml(
u.id
u.id,
)}', '${escapeHtml(u.name)}')" title="Change Password">
<i class="bi bi-key"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteUser('${escapeHtml(
u.id
u.id,
)}', '${escapeHtml(u.name)}')" title="Delete User">
<i class="bi bi-trash"></i>
</button>
</td>
</tr>`
</tr>`,
)
.join("");
}
@@ -111,7 +106,7 @@ function filterUsers() {
(u) =>
u.name.toLowerCase().includes(searchTerm) ||
u.email.toLowerCase().includes(searchTerm) ||
u.username.toLowerCase().includes(searchTerm)
u.username.toLowerCase().includes(searchTerm),
);
renderUsers(filtered);
}
@@ -174,6 +169,18 @@ async function saveUser() {
showError("Password must be at least 8 characters long");
return;
}
if (!/[A-Z]/.test(password)) {
showError("Password must contain at least one uppercase letter");
return;
}
if (!/[a-z]/.test(password)) {
showError("Password must contain at least one lowercase letter");
return;
}
if (!/[0-9]/.test(password)) {
showError("Password must contain at least one number");
return;
}
}
const formData = {
@@ -212,7 +219,7 @@ async function saveUser() {
if (data.success) {
showSuccess(
id ? "User updated successfully" : "User created successfully"
id ? "User updated successfully" : "User created successfully",
);
userModal.hide();
loadUsers();
@@ -254,6 +261,21 @@ async function changePassword() {
return;
}
if (!/[A-Z]/.test(newPassword)) {
showError("Password must contain at least one uppercase letter");
return;
}
if (!/[a-z]/.test(newPassword)) {
showError("Password must contain at least one lowercase letter");
return;
}
if (!/[0-9]/.test(newPassword)) {
showError("Password must contain at least one number");
return;
}
showLoading("Changing password...");
try {
@@ -281,34 +303,33 @@ async function changePassword() {
}
async function deleteUser(id, name) {
if (
!confirm(
`Are you sure you want to delete user "${name}"? This action cannot be undone.`
)
)
return;
showDeleteConfirm(
`Are you sure you want to delete user "${name}"? This action cannot be undone.`,
async () => {
showLoading("Deleting user...");
showLoading("Deleting user...");
try {
const response = await fetch(`/api/admin/users/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
hideLoading();
try {
const response = await fetch(`/api/admin/users/${id}`, {
method: "DELETE",
credentials: "include",
});
const data = await response.json();
hideLoading();
if (data.success) {
showSuccess("User deleted successfully");
loadUsers();
} else {
showError(data.message || "Failed to delete user");
}
} catch (error) {
console.error("Failed to delete user:", error);
hideLoading();
showError("Failed to delete user");
}
if (data.success) {
showSuccess("User deleted successfully");
loadUsers();
} else {
showError(data.message || "Failed to delete user");
}
} catch (error) {
console.error("Failed to delete user:", error);
hideLoading();
showError("Failed to delete user");
}
},
{ title: "Delete User", confirmText: "Delete User" },
);
}
function updatePermissionsPreview() {
@@ -323,7 +344,7 @@ function updatePermissionsPreview() {
<i class="bi bi-check-circle-fill" style="color: #10b981; margin-right: 8px;"></i>
<span>${perm}</span>
</div>
`
`,
)
.join("");
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

411
website/admin/menu-old.html Normal file
View File

@@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Menu Management - 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"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<style>
.menu-item {
background: white;
border: 2px solid #e9ecef;
border-radius: 12px;
padding: 20px;
margin-bottom: 15px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.3s ease;
cursor: move;
}
.menu-item:hover {
border-color: #667eea;
transform: translateX(5px);
}
.menu-item-content {
flex: 1;
}
.menu-item-actions {
display: flex;
gap: 10px;
}
.drag-handle {
cursor: grab;
color: #6c757d;
margin-right: 15px;
}
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu" class="active"
><i class="bi bi-list"></i> Menu</a
>
</li>
<li>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
<div class="main-content">
<div class="top-bar">
<div>
<h3>Menu Management</h3>
<p class="mb-0 text-muted">Organize your website navigation</p>
</div>
<div>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<div class="actions-bar">
<button class="btn btn-primary" onclick="showAddMenuItem()">
<i class="bi bi-plus-circle"></i> Add Menu Item
</button>
<button class="btn btn-success" onclick="saveMenuOrder()">
<i class="bi bi-save"></i> Save Order
</button>
</div>
<div class="card">
<div class="p-4">
<h5 class="mb-3">Main Navigation Menu</h5>
<small class="text-muted">Drag and drop to reorder menu items</small>
<div id="menuItems" class="mt-3">
<div class="text-center p-4">
<div class="loading-spinner"></div>
Loading menu items...
</div>
</div>
</div>
</div>
</div>
<!-- Add/Edit Menu Item Modal -->
<div class="modal fade" id="menuModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Add Menu Item</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<form id="menuForm">
<input type="hidden" id="menuItemId" />
<div class="mb-3">
<label for="menuLabel" class="form-label">Label *</label>
<input
type="text"
class="form-control"
id="menuLabel"
required
placeholder="Home, Shop, About..."
/>
</div>
<div class="mb-3">
<label for="menuUrl" class="form-label">URL *</label>
<input
type="text"
class="form-control"
id="menuUrl"
required
placeholder="/shop, /about, /contact"
/>
</div>
<div class="mb-3">
<label for="menuIcon" class="form-label">Icon (optional)</label>
<input
type="text"
class="form-control"
id="menuIcon"
placeholder="bi-house, bi-shop, etc."
/>
<small class="text-muted">Bootstrap Icon name</small>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="menuVisible"
checked
/>
<label class="form-check-label" for="menuVisible">
Visible in menu
</label>
</div>
</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="saveMenuItem()"
>
<i class="bi bi-save"></i> Save
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
let menuItemsData = [];
let menuModal;
document.addEventListener("DOMContentLoaded", function () {
menuModal = new bootstrap.Modal(document.getElementById("menuModal"));
checkAuth().then((authenticated) => {
if (authenticated) {
loadMenuItems();
}
});
});
async function loadMenuItems() {
try {
const response = await fetch("/api/admin/menu", {
credentials: "include",
});
const data = await response.json();
if (data.success) {
menuItemsData = data.items || [];
renderMenuItems();
}
} catch (error) {
console.error("Failed to load menu items:", error);
menuItemsData = [];
renderMenuItems();
}
}
function renderMenuItems() {
const container = document.getElementById("menuItems");
if (menuItemsData.length === 0) {
container.innerHTML = `
<div class="text-center p-4">
<i class="bi bi-inbox" style="font-size: 3rem; color: #ccc;"></i>
<p class="mt-3 text-muted">No menu items yet</p>
<button class="btn btn-primary" onclick="showAddMenuItem()">
<i class="bi bi-plus-circle"></i> Add Your First Menu Item
</button>
</div>`;
return;
}
container.innerHTML = menuItemsData
.map(
(item, index) => `
<div class="menu-item" draggable="true" data-index="${index}">
<div class="d-flex align-items-center flex-grow-1">
<i class="bi bi-grip-vertical drag-handle"></i>
<div class="menu-item-content">
<strong>${item.label}</strong>
<div class="text-muted small">${item.url}</div>
</div>
</div>
<div class="menu-item-actions">
<span class="badge ${
item.visible ? "badge-success" : "badge-danger"
}">
${item.visible ? "Visible" : "Hidden"}
</span>
<button class="btn btn-sm btn-info" onclick="editMenuItem(${index})">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" onclick="deleteMenuItem(${index})">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
`
)
.join("");
// Add drag and drop functionality
addDragAndDrop();
}
function addDragAndDrop() {
const items = document.querySelectorAll(".menu-item");
items.forEach((item) => {
item.addEventListener("dragstart", handleDragStart);
item.addEventListener("dragover", handleDragOver);
item.addEventListener("drop", handleDrop);
item.addEventListener("dragend", handleDragEnd);
});
}
let draggedItem = null;
function handleDragStart(e) {
draggedItem = this;
e.dataTransfer.effectAllowed = "move";
}
function handleDragOver(e) {
if (e.preventDefault) e.preventDefault();
e.dataTransfer.dropEffect = "move";
return false;
}
function handleDrop(e) {
if (e.stopPropagation) e.stopPropagation();
if (draggedItem !== this) {
const fromIndex = parseInt(draggedItem.dataset.index);
const toIndex = parseInt(this.dataset.index);
const item = menuItemsData.splice(fromIndex, 1)[0];
menuItemsData.splice(toIndex, 0, item);
renderMenuItems();
}
return false;
}
function handleDragEnd(e) {
draggedItem = null;
}
function showAddMenuItem() {
document.getElementById("modalTitle").textContent = "Add Menu Item";
document.getElementById("menuForm").reset();
document.getElementById("menuItemId").value = "";
document.getElementById("menuVisible").checked = true;
menuModal.show();
}
function editMenuItem(index) {
const item = menuItemsData[index];
document.getElementById("modalTitle").textContent = "Edit Menu Item";
document.getElementById("menuItemId").value = index;
document.getElementById("menuLabel").value = item.label;
document.getElementById("menuUrl").value = item.url;
document.getElementById("menuIcon").value = item.icon || "";
document.getElementById("menuVisible").checked = item.visible !== false;
menuModal.show();
}
function saveMenuItem() {
const index = document.getElementById("menuItemId").value;
const item = {
label: document.getElementById("menuLabel").value,
url: document.getElementById("menuUrl").value,
icon: document.getElementById("menuIcon").value,
visible: document.getElementById("menuVisible").checked,
};
if (!item.label || !item.url) {
alert("Label and URL are required");
return;
}
if (index === "") {
menuItemsData.push(item);
} else {
menuItemsData[parseInt(index)] = item;
}
menuModal.hide();
renderMenuItems();
saveMenuOrder();
}
function deleteMenuItem(index) {
if (confirm("Are you sure you want to delete this menu item?")) {
menuItemsData.splice(index, 1);
renderMenuItems();
saveMenuOrder();
}
}
async function saveMenuOrder() {
try {
const response = await fetch("/api/admin/menu", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ items: menuItemsData }),
});
const data = await response.json();
if (data.success) {
alert("Menu saved successfully!");
} else {
alert("Failed to save menu: " + (data.message || ""));
}
} catch (error) {
console.error("Failed to save menu:", error);
alert("Failed to save menu");
}
}
</script>
<script src="/admin/js/auth.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,692 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Pages - 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"
/>
<!-- 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" />
<link rel="stylesheet" href="/admin/css/media-library.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">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages" class="active"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings"><i class="bi bi-gear"></i> Settings</a>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
<div class="main-content">
<div class="top-bar">
<div>
<h3>Custom Pages Management</h3>
<p class="mb-0 text-muted">Create and manage custom pages</p>
</div>
<div>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<div class="actions-bar">
<button class="btn btn-primary" onclick="showCreatePage()">
<i class="bi bi-plus-circle"></i> Create New Page
</button>
<div class="search-box">
<i class="bi bi-search"></i>
<input
type="text"
placeholder="Search pages..."
id="searchInput"
oninput="filterPages()"
/>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>ID</th>
<th>Page Title</th>
<th>Slug</th>
<th>Status</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="pagesTableBody">
<tr>
<td colspan="6" class="text-center">
<div class="loading-spinner"></div>
Loading pages...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="modal fade" id="pageModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalTitle">Create Custom Page</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
></button>
</div>
<div class="modal-body">
<form id="pageForm">
<input type="hidden" id="pageId" />
<div class="mb-3">
<label for="pageTitle" class="form-label">Page Title *</label>
<input
type="text"
class="form-control"
id="pageTitle"
required
/>
</div>
<div class="mb-3">
<label for="pageSlug" class="form-label">Slug *</label>
<input
type="text"
class="form-control"
id="pageSlug"
required
/>
<small class="text-muted"
>URL path (e.g., about-us, contact)</small
>
</div>
<div class="mb-3">
<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 d-none"
id="pageContent"
rows="15"
required
></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="pageMetaTitle" class="form-label"
>Meta Title (SEO)</label
>
<input type="text" class="form-control" id="pageMetaTitle" />
</div>
<div class="col-md-6 mb-3">
<label for="pageMetaDescription" class="form-label"
>Meta Description (SEO)</label
>
<input
type="text"
class="form-control"
id="pageMetaDescription"
/>
</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
id="pagePublished"
checked
/>
<label class="form-check-label" for="pagePublished">
Published (visible on website)
</label>
</div>
</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="savePage()">
<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/media-library.js"></script>
<script src="/admin/js/pages.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -18,10 +18,11 @@
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<link rel="stylesheet" href="/admin/css/media-library.css" />
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
@@ -63,6 +64,11 @@
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
@@ -262,6 +268,8 @@
<!-- 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/admin-utils.js"></script>
<script src="/admin/js/media-library.js"></script>
<script src="/admin/js/portfolio.js?v=5.0"></script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -18,11 +18,12 @@
rel="stylesheet"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<link rel="stylesheet" href="/admin/css/media-library.css" />
</head>
<body>
<!-- Sidebar -->
<div class="sidebar" id="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
@@ -40,9 +41,7 @@
>
</li>
<li>
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
@@ -66,6 +65,11 @@
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
@@ -385,6 +389,8 @@
<!-- 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>
<script src="/admin/js/admin-utils.js?v=20260115c"></script>
<script src="/admin/js/media-library.js"></script>
<script src="/admin/js/products.js?v=20260115c"></script>
</body>
</html>

View File

@@ -0,0 +1,640 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Settings - 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"
/>
<link rel="stylesheet" href="/admin/css/admin-style.css" />
<link rel="stylesheet" href="/admin/css/media-library.css" />
<style>
.settings-section {
background: white;
border-radius: 12px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.settings-section h4 {
color: #2c3e50;
font-weight: 700;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #e9ecef;
display: flex;
align-items: center;
gap: 10px;
}
.logo-preview {
width: 200px;
height: 80px;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
overflow: hidden;
background: #f8f9fa;
}
.logo-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.favicon-preview {
width: 64px;
height: 64px;
border: 2px dashed #ccc;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-top: 10px;
overflow: hidden;
background: #f8f9fa;
}
.favicon-preview img {
width: 100%;
height: 100%;
object-fit: contain;
}
.theme-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-top: 15px;
}
.theme-option {
padding: 20px;
border: 3px solid #e9ecef;
border-radius: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.theme-option:hover {
border-color: #667eea;
transform: translateY(-2px);
}
.theme-option.active {
border-color: #667eea;
background: linear-gradient(135deg, #667eea11 0%, #764ba222 100%);
}
.theme-option i {
font-size: 2rem;
margin-bottom: 10px;
}
.color-picker-wrapper {
display: flex;
align-items: center;
gap: 15px;
}
.color-preview {
width: 50px;
height: 50px;
border-radius: 8px;
border: 2px solid #e9ecef;
}
</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">
<li>
<a href="/admin/dashboard"
><i class="bi bi-speedometer2"></i> Dashboard</a
>
</li>
<li>
<a href="/admin/homepage"
><i class="bi bi-house"></i> Homepage Editor</a
>
</li>
<li>
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
</li>
<li>
<a href="/admin/pages"
><i class="bi bi-file-text"></i> Custom Pages</a
>
</li>
<li>
<a href="/admin/media-library"
><i class="bi bi-images"></i> Media Library</a
>
</li>
<li>
<a href="/admin/menu"><i class="bi bi-list"></i> Menu</a>
</li>
<li>
<a href="/admin/settings" class="active"
><i class="bi bi-gear"></i> Settings</a
>
</li>
<li>
<a href="/admin/users"><i class="bi bi-people"></i> Users</a>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
<div class="main-content">
<div class="top-bar">
<div>
<h3>Settings</h3>
<p class="mb-0 text-muted">Configure your website</p>
</div>
<div>
<button class="btn-logout" onclick="logout()">
<i class="bi bi-box-arrow-right"></i> Logout
</button>
</div>
</div>
<!-- General Settings -->
<div class="settings-section">
<h4><i class="bi bi-gear-fill"></i> General Settings</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label for="siteName" class="form-label">Website Name *</label>
<input
type="text"
class="form-control"
id="siteName"
placeholder="Sky Art Shop"
/>
</div>
<div class="col-md-6 mb-3">
<label for="siteTagline" class="form-label">Tagline</label>
<input
type="text"
class="form-control"
id="siteTagline"
placeholder="Your Creative Destination"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="siteEmail" class="form-label">Contact Email</label>
<input
type="email"
class="form-control"
id="siteEmail"
placeholder="info@skyartshop.com"
/>
</div>
<div class="col-md-6 mb-3">
<label for="sitePhone" class="form-label">Phone Number</label>
<input
type="tel"
class="form-control"
id="sitePhone"
placeholder="+1 234 567 8900"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="siteLogo" class="form-label">Logo</label>
<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 selected</span>
</div>
</div>
<div class="col-md-6 mb-3">
<label for="siteFavicon" class="form-label">Favicon</label>
<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>
</div>
</div>
<div class="mb-3">
<label for="timezone" class="form-label">Timezone</label>
<select class="form-control" id="timezone">
<option value="UTC">UTC</option>
<option value="America/New_York">Eastern Time (US & Canada)</option>
<option value="America/Chicago">Central Time (US & Canada)</option>
<option value="America/Denver">Mountain Time (US & Canada)</option>
<option value="America/Los_Angeles">
Pacific Time (US & Canada)
</option>
<option value="Europe/London">London</option>
<option value="Europe/Paris">Paris</option>
<option value="Asia/Tokyo">Tokyo</option>
</select>
</div>
</div>
<!-- Homepage Settings -->
<div class="settings-section">
<h4><i class="bi bi-house-fill"></i> Homepage Settings</h4>
<div class="mb-3">
<label class="form-label">Homepage Layout</label>
<div class="theme-selector">
<div class="theme-option active" onclick="selectLayout('modern')">
<i class="bi bi-grid-3x3-gap"></i>
<div><strong>Modern</strong></div>
<small>Grid-based layout</small>
</div>
<div class="theme-option" onclick="selectLayout('classic')">
<i class="bi bi-list-ul"></i>
<div><strong>Classic</strong></div>
<small>Traditional list</small>
</div>
<div class="theme-option" onclick="selectLayout('minimal')">
<i class="bi bi-square"></i>
<div><strong>Minimal</strong></div>
<small>Clean & simple</small>
</div>
</div>
</div>
<div class="mb-3">
<label class="form-label">Featured Content</label>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="showHero"
checked
/>
<label class="form-check-label" for="showHero"
>Show Hero Section</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="showPromotions"
checked
/>
<label class="form-check-label" for="showPromotions"
>Show Promotions</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="showPortfolio"
checked
/>
<label class="form-check-label" for="showPortfolio"
>Show Portfolio Showcase</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="showBlog"
checked
/>
<label class="form-check-label" for="showBlog"
>Show Recent Blog Posts</label
>
</div>
</div>
</div>
<!-- Product Settings -->
<div class="settings-section">
<h4><i class="bi bi-box-fill"></i> Product Settings</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Default Product Status</label>
<select class="form-control" id="defaultProductStatus">
<option value="active">Active (Published)</option>
<option value="draft">Draft (Hidden)</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="productsPerPage" class="form-label"
>Products Per Page</label
>
<input
type="number"
class="form-control"
id="productsPerPage"
value="12"
min="6"
max="48"
/>
</div>
</div>
<div class="mb-3">
<label class="form-label">Best Seller Logic</label>
<select class="form-control" id="bestSellerLogic">
<option value="manual">Manual Selection</option>
<option value="auto-sales">Automatic (Most Sales)</option>
<option value="auto-views">Automatic (Most Views)</option>
</select>
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enableInventory"
checked
/>
<label class="form-check-label" for="enableInventory"
>Enable Inventory Management</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="showOutOfStock"
checked
/>
<label class="form-check-label" for="showOutOfStock"
>Show Out of Stock Products</label
>
</div>
</div>
</div>
<!-- Security Settings -->
<div class="settings-section">
<h4><i class="bi bi-shield-fill"></i> Security Settings</h4>
<div class="row">
<div class="col-md-6 mb-3">
<label for="passwordExpiration" class="form-label"
>Password Expiration (days)</label
>
<input
type="number"
class="form-control"
id="passwordExpiration"
value="90"
min="0"
/>
<small class="text-muted">Set to 0 for never expires</small>
</div>
<div class="col-md-6 mb-3">
<label for="sessionTimeout" class="form-label"
>Session Timeout (minutes)</label
>
<input
type="number"
class="form-control"
id="sessionTimeout"
value="60"
min="5"
/>
</div>
</div>
<div class="mb-3">
<label for="loginAttempts" class="form-label"
>Max Login Attempts</label
>
<input
type="number"
class="form-control"
id="loginAttempts"
value="5"
min="3"
max="10"
/>
<small class="text-muted"
>Number of failed attempts before account lockout</small
>
</div>
<div class="mb-3">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="requireStrongPassword"
checked
/>
<label class="form-check-label" for="requireStrongPassword">
Require Strong Passwords (8+ chars, uppercase, lowercase, number)
</label>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="enableTwoFactor"
/>
<label class="form-check-label" for="enableTwoFactor"
>Enable Two-Factor Authentication</label
>
</div>
</div>
</div>
<!-- Appearance Settings -->
<div class="settings-section">
<h4><i class="bi bi-palette-fill"></i> Appearance Settings</h4>
<div class="mb-3">
<label class="form-label">Admin Panel Theme</label>
<div class="theme-selector">
<div class="theme-option active" onclick="selectTheme('light')">
<i class="bi bi-sun-fill text-warning"></i>
<div><strong>Light</strong></div>
</div>
<div class="theme-option" onclick="selectTheme('dark')">
<i class="bi bi-moon-fill text-primary"></i>
<div><strong>Dark</strong></div>
</div>
<div class="theme-option" onclick="selectTheme('auto')">
<i class="bi bi-circle-half text-info"></i>
<div><strong>Auto</strong></div>
</div>
</div>
</div>
<div class="mb-3">
<label for="accentColor" class="form-label">Accent Color</label>
<div class="color-picker-wrapper">
<input
type="color"
class="form-control"
id="accentColor"
value="#667eea"
style="width: 80px"
onchange="updateColorPreview()"
/>
<div
class="color-preview"
id="colorPreview"
style="background-color: #667eea"
></div>
<span id="colorValue">#667eea</span>
</div>
</div>
</div>
<!-- Save Button -->
<div class="text-end">
<button class="btn btn-lg btn-primary" onclick="saveSettings()">
<i class="bi bi-save"></i> Save All Settings
</button>
</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/media-library.js"></script>
<script src="/admin/js/settings.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
@@ -78,7 +78,9 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
background: white;
border-left: 4px solid;
animation: slideIn 0.3s ease-out, fadeOut 0.3s ease-in 2.7s;
animation:
slideIn 0.3s ease-out,
fadeOut 0.3s ease-in 2.7s;
opacity: 1;
transform: translateX(0);
min-width: 320px;
@@ -232,7 +234,7 @@
</head>
<body>
<div class="sidebar">
<div class="sidebar-brand">🛍️ Sky Art Shop</div>
<div class="sidebar-brand">Sky Art Shop</div>
<ul class="sidebar-menu">
<li>
<a href="/admin/dashboard"
@@ -248,9 +250,7 @@
<a href="/admin/products"><i class="bi bi-box"></i> Products</a>
</li>
<li>
<a href="/admin/portfolio"
><i class="bi bi-easel"></i> Portfolio</a
>
<a href="/admin/portfolio"><i class="bi bi-easel"></i> Portfolio</a>
</li>
<li>
<a href="/admin/blog"><i class="bi bi-newspaper"></i> Blog</a>
@@ -276,6 +276,11 @@
><i class="bi bi-people"></i> Users</a
>
</li>
<li>
<a href="/admin/customers"
><i class="bi bi-person-hearts"></i> Customers</a
>
</li>
</ul>
</div>
@@ -397,7 +402,8 @@
id="userPassword"
/>
<small class="text-muted"
>Leave blank to keep current password (when editing)</small
>Min 8 chars, uppercase, lowercase, number. Leave blank when
editing to keep current.</small
>
</div>
<div class="col-md-6 mb-3">
@@ -421,8 +427,8 @@
>
<option value="Cashier">Cashier</option>
<option value="Accountant">Accountant</option>
<option value="Sales">Sales</option>
<option value="Admin">Admin</option>
<option value="MasterAdmin">Master Admin</option>
</select>
<small class="text-muted"
>Role determines access permissions</small
@@ -516,7 +522,10 @@
id="newPassword"
required
/>
<small class="text-muted">Minimum 8 characters</small>
<small class="text-muted"
>Min 8 chars, must include uppercase, lowercase, and
number</small
>
</div>
<div class="mb-3">
@@ -557,6 +566,7 @@
<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/admin-utils.js"></script>
<script src="/admin/js/users.js"></script>
</body>
</html>