Move file addition to modal

This commit is contained in:
jheaps
2025-10-09 11:21:47 -06:00
parent f4c88f83df
commit 9269e806b2
3 changed files with 255 additions and 132 deletions
+10 -2
View File
@@ -29,8 +29,11 @@
</div> </div>
</div> </div>
<div class="card"> <div class="card" id="upload-form-card" style="display:none;">
<h2>Upload Image</h2> <div class="upload-form-header">
<h2>Upload Image</h2>
<button type="button" class="close-upload-form" id="close-upload-btn">&times;</button>
</div>
@if (!string.IsNullOrEmpty(Model.UploadMessage)) @if (!string.IsNullOrEmpty(Model.UploadMessage))
{ {
<div class="alert @(Model.UploadSuccess ? "alert-success" : "alert-error")"> <div class="alert @(Model.UploadSuccess ? "alert-success" : "alert-error")">
@@ -79,6 +82,11 @@
</div> </div>
</div> </div>
<!-- Floating Add Files Button -->
<button type="button" class="floating-add-btn" id="add-files-btn" title="Add Files">
<span class="btn-icon">+</span>
</button>
@section Scripts { @section Scripts {
<script src="~/js/gallery.js" asp-append-version="true"></script> <script src="~/js/gallery.js" asp-append-version="true"></script>
} }
@@ -1,3 +1,78 @@
/* Floating Add Files Button */
.floating-add-btn {
position: fixed;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
cursor: pointer;
z-index: 1000;
transition: transform 0.2s, box-shadow 0.2s;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
line-height: 1;
}
.floating-add-btn:hover {
transform: scale(1.1);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.6);
}
.floating-add-btn:active {
transform: scale(0.95);
}
.floating-add-btn .btn-icon {
transition: transform 0.3s;
}
.floating-add-btn.active .btn-icon {
transform: rotate(45deg);
}
/* Upload Form Styles */
#upload-form-card {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
max-width: 500px;
width: 90%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.upload-form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.close-upload-form {
background: transparent;
border: none;
font-size: 32px;
color: #999;
cursor: pointer;
padding: 0;
width: 32px;
height: 32px;
line-height: 1;
transition: color 0.2s;
}
.close-upload-form:hover {
color: #333;
}
/* Gallery Grid Layout */ /* Gallery Grid Layout */
.gallery-grid { .gallery-grid {
display: grid; display: grid;
+170 -130
View File
@@ -1,154 +1,194 @@
// Lazy loading for gallery images using Intersection Observer // Wait for DOM to be fully loaded
let currentOffset = 20; // Initial load was 20 items document.addEventListener('DOMContentLoaded', function() {
let isLoading = false; // Upload form toggle functionality
let hasMore = true; const addFilesBtn = document.getElementById('add-files-btn');
const uploadFormCard = document.getElementById('upload-form-card');
const closeUploadBtn = document.getElementById('close-upload-btn');
// Create intersection observer for lazy loading if (addFilesBtn && uploadFormCard) {
const loadingElement = document.getElementById('loading'); addFilesBtn.addEventListener('click', () => {
const galleryElement = document.getElementById('gallery'); uploadFormCard.style.display = 'block';
addFilesBtn.classList.add('active');
});
if (loadingElement && galleryElement) { if (closeUploadBtn) {
const observer = new IntersectionObserver((entries) => { closeUploadBtn.addEventListener('click', () => {
entries.forEach(entry => { uploadFormCard.style.display = 'none';
if (entry.isIntersecting && !isLoading && hasMore) { addFilesBtn.classList.remove('active');
loadMoreImages(); });
}
// Close on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && uploadFormCard.style.display === 'block') {
uploadFormCard.style.display = 'none';
addFilesBtn.classList.remove('active');
} }
}); });
}, {
rootMargin: '200px' // Start loading 200px before the element comes into view
});
observer.observe(loadingElement); // Close when clicking outside the form
} document.addEventListener('click', (e) => {
if (uploadFormCard.style.display === 'block' &&
!uploadFormCard.contains(e.target) &&
!addFilesBtn.contains(e.target)) {
uploadFormCard.style.display = 'none';
addFilesBtn.classList.remove('active');
}
});
}
async function loadMoreImages() { // Lazy loading for gallery images using Intersection Observer
if (isLoading || !hasMore) return; let currentOffset = 20; // Initial load was 20 items
let isLoading = false;
let hasMore = true;
isLoading = true; // Create intersection observer for lazy loading
loadingElement.style.display = 'block'; const loadingElement = document.getElementById('loading');
const galleryElement = document.getElementById('gallery');
try { if (loadingElement && galleryElement) {
const response = await fetch(`/api/media/load?offset=${currentOffset}&limit=20`); const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (!response.ok) { if (entry.isIntersecting && !isLoading && hasMore) {
throw new Error('Failed to load images'); loadMoreImages();
} }
});
const mediaItems = await response.json(); }, {
rootMargin: '200px' // Start loading 200px before the element comes into view
if (mediaItems.length === 0) {
hasMore = false;
loadingElement.style.display = 'none';
return;
}
// Add new images to gallery
mediaItems.forEach(media => {
const galleryItem = createGalleryItem(media);
galleryElement.appendChild(galleryItem);
}); });
currentOffset += mediaItems.length; observer.observe(loadingElement);
} catch (error) { }
console.error('Error loading images:', error);
loadingElement.innerHTML = 'Failed to load more images.'; async function loadMoreImages() {
} finally { if (isLoading || !hasMore) return;
isLoading = false;
if (hasMore) { isLoading = true;
loadingElement.style.display = 'none'; loadingElement.style.display = 'block';
try {
const response = await fetch(`/api/media/load?offset=${currentOffset}&limit=20`);
if (!response.ok) {
throw new Error('Failed to load images');
}
const mediaItems = await response.json();
if (mediaItems.length === 0) {
hasMore = false;
loadingElement.style.display = 'none';
return;
}
// Add new images to gallery
mediaItems.forEach(media => {
const galleryItem = createGalleryItem(media);
galleryElement.appendChild(galleryItem);
});
currentOffset += mediaItems.length;
} catch (error) {
console.error('Error loading images:', error);
loadingElement.innerHTML = 'Failed to load more images.';
} finally {
isLoading = false;
if (hasMore) {
loadingElement.style.display = 'none';
}
} }
} }
}
function createGalleryItem(media) { function createGalleryItem(media) {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'gallery-item'; item.className = 'gallery-item';
item.setAttribute('data-media-id', media.id); item.setAttribute('data-media-id', media.id);
const img = document.createElement('img'); const img = document.createElement('img');
img.src = `/api/media/image/${media.id}`; img.src = `/api/media/image/${media.id}`;
img.alt = media.fileName; img.alt = media.fileName;
img.loading = 'lazy'; img.loading = 'lazy';
item.appendChild(img); item.appendChild(img);
if (media.description) { if (media.description) {
const desc = document.createElement('div'); const desc = document.createElement('div');
desc.className = 'gallery-item-description'; desc.className = 'gallery-item-description';
desc.textContent = media.description; desc.textContent = media.description;
item.appendChild(desc); item.appendChild(desc);
}
const info = document.createElement('div');
info.className = 'gallery-item-info';
const date = document.createElement('span');
date.className = 'gallery-item-date';
const createdDate = new Date(media.createdAt);
date.textContent = createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
info.appendChild(date);
const form = document.createElement('form');
form.method = 'post';
form.action = '?handler=Delete'; // Relative to current page
form.style.display = 'inline';
// Add anti-forgery token
const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
if (tokenInput) {
const tokenClone = tokenInput.cloneNode(true);
form.appendChild(tokenClone);
}
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'MediaId'; // Match C# property name exactly
hiddenInput.value = media.id;
form.appendChild(hiddenInput);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'submit';
deleteBtn.className = 'btn-delete';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = function(e) {
if (!confirm('Are you sure you want to delete this image?')) {
e.preventDefault();
return false;
} }
};
form.appendChild(deleteBtn);
info.appendChild(form); const info = document.createElement('div');
item.appendChild(info); info.className = 'gallery-item-info';
return item; const date = document.createElement('span');
} date.className = 'gallery-item-date';
const createdDate = new Date(media.createdAt);
date.textContent = createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
info.appendChild(date);
// Optional: Add image preview on click const form = document.createElement('form');
galleryElement?.addEventListener('click', (e) => { form.method = 'post';
if (e.target.tagName === 'IMG') { form.action = '?handler=Delete'; // Relative to current page
const modal = createImageModal(e.target.src); form.style.display = 'inline';
document.body.appendChild(modal);
// Add anti-forgery token
const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
if (tokenInput) {
const tokenClone = tokenInput.cloneNode(true);
form.appendChild(tokenClone);
}
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'MediaId'; // Match C# property name exactly
hiddenInput.value = media.id;
form.appendChild(hiddenInput);
const deleteBtn = document.createElement('button');
deleteBtn.type = 'submit';
deleteBtn.className = 'btn-delete';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = function(e) {
if (!confirm('Are you sure you want to delete this image?')) {
e.preventDefault();
return false;
}
};
form.appendChild(deleteBtn);
info.appendChild(form);
item.appendChild(info);
return item;
} }
});
function createImageModal(imageSrc) { // Optional: Add image preview on click
const modal = document.createElement('div'); galleryElement?.addEventListener('click', (e) => {
modal.className = 'image-modal'; if (e.target.tagName === 'IMG') {
modal.innerHTML = ` const modal = createImageModal(e.target.src);
<div class="modal-backdrop"></div> document.body.appendChild(modal);
<div class="modal-content">
<img src="${imageSrc}" alt="Full size image" />
<button class="modal-close">&times;</button>
</div>
`;
modal.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-backdrop') ||
e.target.classList.contains('modal-close') ||
e.target.classList.contains('image-modal')) {
modal.remove();
} }
}); });
return modal; function createImageModal(imageSrc) {
} const modal = document.createElement('div');
modal.className = 'image-modal';
modal.innerHTML = `
<div class="modal-backdrop"></div>
<div class="modal-content">
<img src="${imageSrc}" alt="Full size image" />
<button class="modal-close">&times;</button>
</div>
`;
modal.addEventListener('click', (e) => {
if (e.target.classList.contains('modal-backdrop') ||
e.target.classList.contains('modal-close') ||
e.target.classList.contains('image-modal')) {
modal.remove();
}
});
return modal;
}
}); // End of DOMContentLoaded