Files
SkyArtShop/Views/Home/Index.cshtml
Local Server 703ab57984 Fix admin route access and backend configuration
- 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
2025-12-13 22:34:11 -06:00

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>