diff --git a/Media.JoshHeaps.Net/Pages/Index.cshtml b/Media.JoshHeaps.Net/Pages/Index.cshtml index 72128d8..c74d66e 100644 --- a/Media.JoshHeaps.Net/Pages/Index.cshtml +++ b/Media.JoshHeaps.Net/Pages/Index.cshtml @@ -29,8 +29,11 @@ -
-

Upload Image

+ + + + @section Scripts { } \ No newline at end of file diff --git a/Media.JoshHeaps.Net/wwwroot/css/gallery.css b/Media.JoshHeaps.Net/wwwroot/css/gallery.css index 0a0c53d..60467ac 100644 --- a/Media.JoshHeaps.Net/wwwroot/css/gallery.css +++ b/Media.JoshHeaps.Net/wwwroot/css/gallery.css @@ -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 { display: grid; diff --git a/Media.JoshHeaps.Net/wwwroot/js/gallery.js b/Media.JoshHeaps.Net/wwwroot/js/gallery.js index cd07c27..b878842 100644 --- a/Media.JoshHeaps.Net/wwwroot/js/gallery.js +++ b/Media.JoshHeaps.Net/wwwroot/js/gallery.js @@ -1,154 +1,194 @@ -// Lazy loading for gallery images using Intersection Observer -let currentOffset = 20; // Initial load was 20 items -let isLoading = false; -let hasMore = true; +// Wait for DOM to be fully loaded +document.addEventListener('DOMContentLoaded', function() { + // Upload form toggle functionality + 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 -const loadingElement = document.getElementById('loading'); -const galleryElement = document.getElementById('gallery'); + if (addFilesBtn && uploadFormCard) { + addFilesBtn.addEventListener('click', () => { + uploadFormCard.style.display = 'block'; + addFilesBtn.classList.add('active'); + }); -if (loadingElement && galleryElement) { - const observer = new IntersectionObserver((entries) => { - entries.forEach(entry => { - if (entry.isIntersecting && !isLoading && hasMore) { - loadMoreImages(); + if (closeUploadBtn) { + closeUploadBtn.addEventListener('click', () => { + uploadFormCard.style.display = 'none'; + addFilesBtn.classList.remove('active'); + }); + } + + // 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() { - if (isLoading || !hasMore) return; + // Lazy loading for gallery images using Intersection Observer + let currentOffset = 20; // Initial load was 20 items + let isLoading = false; + let hasMore = true; - isLoading = true; - loadingElement.style.display = 'block'; + // Create intersection observer for lazy loading + const loadingElement = document.getElementById('loading'); + const galleryElement = document.getElementById('gallery'); - 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); + if (loadingElement && galleryElement) { + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting && !isLoading && hasMore) { + loadMoreImages(); + } + }); + }, { + rootMargin: '200px' // Start loading 200px before the element comes into view }); - 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'; + observer.observe(loadingElement); + } + + async function loadMoreImages() { + if (isLoading || !hasMore) return; + + isLoading = true; + 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) { - const item = document.createElement('div'); - item.className = 'gallery-item'; - item.setAttribute('data-media-id', media.id); + function createGalleryItem(media) { + const item = document.createElement('div'); + item.className = 'gallery-item'; + item.setAttribute('data-media-id', media.id); - const img = document.createElement('img'); - img.src = `/api/media/image/${media.id}`; - img.alt = media.fileName; - img.loading = 'lazy'; - item.appendChild(img); + const img = document.createElement('img'); + img.src = `/api/media/image/${media.id}`; + img.alt = media.fileName; + img.loading = 'lazy'; + item.appendChild(img); - if (media.description) { - const desc = document.createElement('div'); - desc.className = 'gallery-item-description'; - desc.textContent = media.description; - 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; + if (media.description) { + const desc = document.createElement('div'); + desc.className = 'gallery-item-description'; + desc.textContent = media.description; + item.appendChild(desc); } - }; - form.appendChild(deleteBtn); - info.appendChild(form); - item.appendChild(info); + const info = document.createElement('div'); + 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 -galleryElement?.addEventListener('click', (e) => { - if (e.target.tagName === 'IMG') { - const modal = createImageModal(e.target.src); - document.body.appendChild(modal); + 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); + item.appendChild(info); + + return item; } -}); -function createImageModal(imageSrc) { - const modal = document.createElement('div'); - modal.className = 'image-modal'; - modal.innerHTML = ` - - - `; - - 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(); + // Optional: Add image preview on click + galleryElement?.addEventListener('click', (e) => { + if (e.target.tagName === 'IMG') { + const modal = createImageModal(e.target.src); + document.body.appendChild(modal); } }); - return modal; -} + function createImageModal(imageSrc) { + const modal = document.createElement('div'); + modal.className = 'image-modal'; + modal.innerHTML = ` + + + `; + + 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