- Added /admin redirect to login page in nginx config - Fixed backend server.js route ordering for proper admin handling - Updated authentication middleware and routes - Added user management routes - Configured PostgreSQL integration - Updated environment configuration
447 lines
19 KiB
Plaintext
Executable File
447 lines
19 KiB
Plaintext
Executable File
<!-- Image Picker Modal -->
|
|
<div class="modal fade" id="imagePickerModal" tabindex="-1" aria-labelledby="imagePickerModalLabel" aria-hidden="true" data-bs-backdrop="true" style="z-index: 1060;">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="imagePickerModalLabel">
|
|
<i class="bi bi-images"></i> Select or Upload Images
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Tab Navigation -->
|
|
<ul class="nav nav-tabs mb-3" id="imagePickerTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="library-tab" data-bs-toggle="tab" data-bs-target="#library" type="button" role="tab">
|
|
<i class="bi bi-folder2-open"></i> Image Library
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload" type="button" role="tab">
|
|
<i class="bi bi-cloud-upload"></i> Upload New
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content" id="imagePickerTabContent">
|
|
<!-- Image Library Tab -->
|
|
<div class="tab-pane fade show active" id="library" role="tabpanel">
|
|
<div class="mb-3">
|
|
<div class="btn-group w-100 mb-2" role="group">
|
|
<button type="button" class="btn btn-outline-primary" id="showProductImagesBtn" onclick="showProductImages()">Product Images (<span id="productImageCount">0</span>)</button>
|
|
<button type="button" class="btn btn-outline-secondary active" id="showAllImagesBtn" onclick="showAllImages()">All Images</button>
|
|
</div>
|
|
<input type="text" class="form-control" id="imageSearchInput" placeholder="Search images..." onkeyup="filterImages()">
|
|
</div>
|
|
<div id="imageLibraryLoading" class="text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-2 text-muted">Loading images...</p>
|
|
</div>
|
|
<div id="imageLibraryContent" style="display: none;">
|
|
<div class="row g-2" id="imageLibraryGrid" style="max-height: 500px; overflow-y: auto;">
|
|
<!-- Images will be loaded here dynamically -->
|
|
</div>
|
|
<div id="noImagesMessage" class="text-center py-5" style="display: none;">
|
|
<i class="bi bi-images" style="font-size: 48px; color: #ccc;"></i>
|
|
<p class="text-muted mt-2">No images found in library</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Upload Tab -->
|
|
<div class="tab-pane fade" id="upload" role="tabpanel">
|
|
<div class="border rounded p-4 text-center" style="border-style: dashed !important;">
|
|
<i class="bi bi-cloud-arrow-up" style="font-size: 48px; color: #0d6efd;"></i>
|
|
<h5 class="mt-3">Upload Images</h5>
|
|
<p class="text-muted">Drag and drop files here or click to browse</p>
|
|
<input type="file" class="form-control" id="newImageUpload" accept="image/*" multiple style="display: none;">
|
|
<button type="button" class="btn btn-primary" onclick="document.getElementById('newImageUpload').click()">
|
|
<i class="bi bi-folder2-open"></i> Choose Files
|
|
</button>
|
|
<small class="d-block mt-2 text-muted">Supported formats: JPG, PNG, GIF, WEBP</small>
|
|
</div>
|
|
<div id="uploadProgress" class="mt-3" style="display: none;">
|
|
<div class="progress">
|
|
<div id="uploadProgressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
<small class="text-muted" id="uploadProgressText">Uploading...</small>
|
|
</div>
|
|
<div id="uploadedImagesPreview" class="row g-2 mt-3">
|
|
<!-- Uploaded images preview -->
|
|
</div>
|
|
</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" id="selectImagesBtn" onclick="confirmImageSelection()">
|
|
<i class="bi bi-check-lg"></i> Select Images (<span id="selectedCount">0</span>)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Ensure nested modals work correctly */
|
|
.modal-backdrop.show:nth-of-type(2) {
|
|
z-index: 1055;
|
|
}
|
|
|
|
#imagePickerModal.show {
|
|
z-index: 1060 !important;
|
|
}
|
|
|
|
.modal-backdrop.show {
|
|
z-index: 1050;
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
let selectedImages = [];
|
|
let imagePickerCallback = null;
|
|
let imagePickerMode = 'multiple'; // 'single' or 'multiple'
|
|
let allLibraryImages = [];
|
|
let currentViewMode = 'all'; // 'product' or 'all'
|
|
|
|
// Open Image Picker Modal
|
|
function openImagePicker(callback, mode = 'multiple') {
|
|
imagePickerCallback = callback;
|
|
imagePickerMode = mode;
|
|
selectedImages = [];
|
|
updateSelectedCount();
|
|
|
|
// Update product image count
|
|
const productImages = window.currentProductImages || [];
|
|
document.getElementById('productImageCount').textContent = productImages.length;
|
|
|
|
// Show modal - use getOrCreateInstance to handle existing instances
|
|
const modalElement = document.getElementById('imagePickerModal');
|
|
if (!modalElement) {
|
|
console.error('Image Picker Modal element not found!');
|
|
alert('Error: Image picker not available. Please refresh the page.');
|
|
return;
|
|
}
|
|
|
|
const modal = bootstrap.Modal.getOrCreateInstance(modalElement);
|
|
modal.show();
|
|
|
|
// Start with product images if available, otherwise all images
|
|
if (productImages.length > 0) {
|
|
currentViewMode = 'product';
|
|
showProductImages();
|
|
} else {
|
|
currentViewMode = 'all';
|
|
loadImageLibrary();
|
|
}
|
|
}
|
|
|
|
// Load Image Library
|
|
async function loadImageLibrary() {
|
|
const loading = document.getElementById('imageLibraryLoading');
|
|
const content = document.getElementById('imageLibraryContent');
|
|
const grid = document.getElementById('imageLibraryGrid');
|
|
const noImagesMsg = document.getElementById('noImagesMessage');
|
|
|
|
loading.style.display = 'block';
|
|
content.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch('/admin/upload/list');
|
|
const images = await response.json();
|
|
allLibraryImages = images;
|
|
|
|
loading.style.display = 'none';
|
|
content.style.display = 'block';
|
|
|
|
if (images.length === 0) {
|
|
grid.style.display = 'none';
|
|
noImagesMsg.style.display = 'block';
|
|
} else {
|
|
grid.style.display = 'flex';
|
|
noImagesMsg.style.display = 'none';
|
|
renderImageLibrary(images);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading images:', error);
|
|
loading.innerHTML = '<div class="alert alert-danger">Error loading images</div>';
|
|
}
|
|
}
|
|
|
|
// Render Image Library
|
|
function renderImageLibrary(images) {
|
|
const grid = document.getElementById('imageLibraryGrid');
|
|
grid.innerHTML = '';
|
|
|
|
images.forEach(imageUrl => {
|
|
const col = document.createElement('div');
|
|
col.className = 'col-6 col-md-3 col-lg-2';
|
|
|
|
const itemDiv = document.createElement('div');
|
|
itemDiv.className = 'image-library-item position-relative';
|
|
itemDiv.setAttribute('data-image-url', imageUrl);
|
|
itemDiv.style.cssText = 'cursor: pointer; border: 3px solid transparent; border-radius: 8px; overflow: hidden; transition: all 0.2s;';
|
|
|
|
itemDiv.innerHTML = `
|
|
<img src="${imageUrl}" class="img-fluid" style="width: 100%; height: 120px; object-fit: cover; display: block;">
|
|
<div class="image-overlay position-absolute top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
|
|
style="background: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s;">
|
|
<i class="bi bi-check-circle-fill text-white" style="font-size: 2rem;"></i>
|
|
</div>
|
|
`;
|
|
|
|
// Use event listener instead of inline onclick to avoid URL escaping issues
|
|
itemDiv.addEventListener('click', function() {
|
|
toggleImageSelection(imageUrl, this);
|
|
});
|
|
|
|
col.appendChild(itemDiv);
|
|
grid.appendChild(col);
|
|
});
|
|
}
|
|
|
|
// Show Product Images
|
|
function showProductImages() {
|
|
currentViewMode = 'product';
|
|
const productImages = window.currentProductImages || [];
|
|
|
|
// Update button states
|
|
document.getElementById('showProductImagesBtn').classList.add('active');
|
|
document.getElementById('showProductImagesBtn').classList.remove('btn-outline-primary');
|
|
document.getElementById('showProductImagesBtn').classList.add('btn-primary');
|
|
document.getElementById('showAllImagesBtn').classList.remove('active', 'btn-primary');
|
|
document.getElementById('showAllImagesBtn').classList.add('btn-outline-secondary');
|
|
|
|
const loading = document.getElementById('imageLibraryLoading');
|
|
const content = document.getElementById('imageLibraryContent');
|
|
const grid = document.getElementById('imageLibraryGrid');
|
|
const noImagesMsg = document.getElementById('noImagesMessage');
|
|
|
|
loading.style.display = 'none';
|
|
content.style.display = 'block';
|
|
|
|
if (productImages.length === 0) {
|
|
grid.style.display = 'none';
|
|
noImagesMsg.style.display = 'block';
|
|
noImagesMsg.innerHTML = '<i class=\"bi bi-images\" style=\"font-size: 48px; color: #ccc;\"></i><p class=\"text-muted mt-2\">No images added to this product yet. Switch to \"All Images\" to browse the full library.</p>';
|
|
} else {
|
|
grid.style.display = 'flex';
|
|
noImagesMsg.style.display = 'none';
|
|
renderImageLibrary(productImages);
|
|
}
|
|
}
|
|
|
|
// Show All Images
|
|
function showAllImages() {
|
|
currentViewMode = 'all';
|
|
|
|
// Update button states
|
|
document.getElementById('showAllImagesBtn').classList.add('active');
|
|
document.getElementById('showAllImagesBtn').classList.remove('btn-outline-secondary');
|
|
document.getElementById('showAllImagesBtn').classList.add('btn-primary');
|
|
document.getElementById('showProductImagesBtn').classList.remove('active', 'btn-primary');
|
|
document.getElementById('showProductImagesBtn').classList.add('btn-outline-primary');
|
|
|
|
loadImageLibrary();
|
|
}
|
|
|
|
// Filter Images
|
|
function filterImages() {
|
|
const searchTerm = document.getElementById('imageSearchInput').value.toLowerCase();
|
|
|
|
if (currentViewMode === 'product') {
|
|
const productImages = window.currentProductImages || [];
|
|
const filteredImages = productImages.filter(url => url.toLowerCase().includes(searchTerm));
|
|
renderImageLibrary(filteredImages);
|
|
} else {
|
|
const filteredImages = allLibraryImages.filter(url => url.toLowerCase().includes(searchTerm));
|
|
// Toggle Image Selection
|
|
function toggleImageSelection(imageUrl, element) {
|
|
if (imagePickerMode === 'single') {
|
|
// Single selection mode
|
|
selectedImages = [imageUrl];
|
|
// Remove selection from all
|
|
document.querySelectorAll('.image-library-item').forEach(item => {
|
|
item.style.borderColor = 'transparent';
|
|
item.querySelector('.image-overlay').style.opacity = '0';
|
|
});
|
|
// Add selection to clicked
|
|
element.style.borderColor = '#0d6efd';
|
|
element.querySelector('.image-overlay').style.opacity = '1';
|
|
} else {
|
|
// Multiple selection mode
|
|
const index = selectedImages.indexOf(imageUrl);
|
|
if (index > -1) {
|
|
selectedImages.splice(index, 1);
|
|
element.style.borderColor = 'transparent';
|
|
element.querySelector('.image-overlay').style.opacity = '0';
|
|
} else {
|
|
selectedImages.push(imageUrl);
|
|
element.style.borderColor = '#0d6efd';
|
|
element.querySelector('.image-overlay').style.opacity = '1';
|
|
}
|
|
}
|
|
updateSelectedCount();
|
|
}
|
|
|
|
// Update Selected Count
|
|
function updateSelectedCount() {
|
|
document.getElementById('selectedCount').textContent = selectedImages.length;
|
|
document.getElementById('selectImagesBtn').disabled = selectedImages.length === 0;
|
|
}
|
|
|
|
// Confirm Image Selection
|
|
function confirmImageSelection() {
|
|
if (selectedImages.length === 0) {
|
|
alert('Please select at least one image.');
|
|
return;
|
|
}
|
|
|
|
if (!imagePickerCallback) {
|
|
console.error('No callback function defined!');
|
|
alert('Error: Callback function not found. Please close and try again.');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
imagePickerCallback(selectedImages);
|
|
const modalElement = document.getElementById('imagePickerModal');
|
|
const modalInstance = bootstrap.Modal.getInstance(modalElement);
|
|
if (modalInstance) {
|
|
modalInstance.hide();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in callback:', error);
|
|
alert('Error processing selection: ' + error.message);
|
|
}
|
|
}
|
|
|
|
// Handle New Image Upload
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const uploadInput = document.getElementById('newImageUpload');
|
|
if (uploadInput) {
|
|
uploadInput.addEventListener('change', handleNewImageUpload);
|
|
}
|
|
|
|
// Drag and drop support
|
|
const uploadArea = document.querySelector('#upload .border');
|
|
if (uploadArea) {
|
|
uploadArea.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.style.backgroundColor = '#e7f3ff';
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', () => {
|
|
uploadArea.style.backgroundColor = '';
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
uploadArea.style.backgroundColor = '';
|
|
const files = e.dataTransfer.files;
|
|
handleNewImageUpload({ target: { files } });
|
|
});
|
|
}
|
|
});
|
|
|
|
// Handle New Image Upload
|
|
async function handleNewImageUpload(event) {
|
|
const files = event.target.files;
|
|
if (files.length === 0) return;
|
|
|
|
const progressDiv = document.getElementById('uploadProgress');
|
|
const progressBar = document.getElementById('uploadProgressBar');
|
|
const progressText = document.getElementById('uploadProgressText');
|
|
const previewDiv = document.getElementById('uploadedImagesPreview');
|
|
|
|
progressDiv.style.display = 'block';
|
|
previewDiv.innerHTML = '';
|
|
|
|
const uploadedUrls = [];
|
|
|
|
for (let i = 0; i < files.length; i++) {
|
|
const file = files[i];
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
progressText.textContent = `Uploading ${i + 1} of ${files.length}...`;
|
|
progressBar.style.width = ((i / files.length) * 100) + '%';
|
|
|
|
try {
|
|
const response = await fetch('/admin/upload/image', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
uploadedUrls.push(result.url);
|
|
|
|
// Add to preview
|
|
const col = document.createElement('div');
|
|
col.className = 'col-4';
|
|
col.innerHTML = `
|
|
<div class="position-relative">
|
|
<img src="${result.url}" class="img-fluid rounded" style="width: 100%; height: 100px; object-fit: cover;">
|
|
<span class="badge bg-success position-absolute top-0 end-0 m-1">
|
|
<i class="bi bi-check-lg"></i>
|
|
</span>
|
|
</div>
|
|
`;
|
|
previewDiv.appendChild(col);
|
|
}
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
}
|
|
}
|
|
|
|
progressBar.style.width = '100%';
|
|
progressText.textContent = `Successfully uploaded ${uploadedUrls.length} images`;
|
|
|
|
// Auto-select uploaded images
|
|
selectedImages = uploadedUrls;
|
|
updateSelectedCount();
|
|
|
|
// Reload library
|
|
setTimeout(() => {
|
|
loadImageLibrary();
|
|
// Switch to library tab
|
|
document.getElementById('library-tab').click();
|
|
}, 1000);
|
|
|
|
// Reset input
|
|
event.target.value = '';
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.image-library-item:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.image-library-item:hover .image-overlay {
|
|
opacity: 0.3 !important;
|
|
}
|
|
|
|
#imageLibraryGrid {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: #0d6efd #f8f9fa;
|
|
}
|
|
|
|
#imageLibraryGrid::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
#imageLibraryGrid::-webkit-scrollbar-track {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
#imageLibraryGrid::-webkit-scrollbar-thumb {
|
|
background: #0d6efd;
|
|
border-radius: 4px;
|
|
}
|
|
</style>
|