Move file addition to modal
This commit is contained in:
@@ -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">×</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;
|
||||||
|
|||||||
@@ -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">×</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">×</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
|
||||||
|
|||||||
Reference in New Issue
Block a user