This commit reflects an intentional reorganization of the project. - Deletes obsolete root-level files. - Restructures the admin and gallery components. - Tracks previously untracked application modules.
257 lines
11 KiB
JavaScript
257 lines
11 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
const gallery = document.getElementById('photo-gallery');
|
|
const searchInput = document.getElementById('searchInput');
|
|
const filterBtns = document.querySelectorAll('.filter-btn');
|
|
const modal = document.getElementById('image-modal');
|
|
const modalImg = document.getElementById('modal-image-src');
|
|
const modalCaption = document.getElementById('modal-caption');
|
|
const modalCaptionTags = document.getElementById('modal-caption-tags');
|
|
const modalCloseBtn = modal.querySelector('.modal-close-btn') || modal.querySelector('.delete');
|
|
const modalBackground = modal.querySelector('.modal-background');
|
|
const resultCountEl = document.getElementById('result-count');
|
|
const noResults = document.getElementById('no-results');
|
|
const isTouchDevice = 'ontouchstart' in window || (navigator.maxTouchPoints || 0) > 0;
|
|
const topButton = document.getElementById('top');
|
|
|
|
const fallbackPhotos = [
|
|
{
|
|
path: 'assets/pics/gallery/classic/20230617_131551.webp',
|
|
caption: "20' Classic Arch",
|
|
tags: ['arch', 'classic', 'outdoor', 'wedding']
|
|
},
|
|
{
|
|
path: 'assets/pics/gallery/classic/_Photos_20241207_083534.webp',
|
|
caption: 'Classic Columns',
|
|
tags: ['columns', 'classic', 'indoor', 'corporate', 'black-tie']
|
|
},
|
|
{
|
|
path: 'assets/pics/gallery/centerpiece/20230108_112718.jpg}.webp',
|
|
caption: 'Cocktail Arrangements',
|
|
tags: ['centerpiece', 'cocktail', 'tablescape', 'baby-shower']
|
|
},
|
|
{
|
|
path: 'assets/pics/gallery/organic/20241121_200047~2.jpg',
|
|
caption: 'Organic Garland',
|
|
tags: ['organic', 'garland', 'outdoor', 'birthday']
|
|
},
|
|
{
|
|
path: 'assets/pics/gallery/organic/20250202_133930~2.jpg',
|
|
caption: 'Organic Garland (Pastel)',
|
|
tags: ['organic', 'garland', 'pastel']
|
|
}
|
|
];
|
|
let photos = [];
|
|
|
|
const normalizeTags = (tags) => {
|
|
if (Array.isArray(tags)) return tags;
|
|
if (typeof tags === 'string') {
|
|
return tags.split(',').map(tag => tag.trim()).filter(Boolean);
|
|
}
|
|
return [];
|
|
};
|
|
|
|
async function fetchPhotos() {
|
|
try {
|
|
const response = await fetch(`${window.location.protocol}//${window.location.hostname}:5000/photos`);
|
|
if (!response.ok) {
|
|
throw new Error(`Fetch failed with status ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
photos = Array.isArray(data) && data.length ? data : fallbackPhotos;
|
|
} catch (error) {
|
|
console.error('Error fetching photos:', error);
|
|
photos = fallbackPhotos;
|
|
}
|
|
renderFlatGallery(photos);
|
|
}
|
|
|
|
function updateResultCount(count) {
|
|
if (!resultCountEl) return;
|
|
const total = photos.length;
|
|
const totalText = total ? `${total}` : '0';
|
|
const countText = count !== undefined ? `${count}` : totalText;
|
|
if (count === 0) {
|
|
resultCountEl.innerHTML = `<span>${countText}</span> photos shown • ${totalText} total`;
|
|
return;
|
|
}
|
|
resultCountEl.innerHTML = count === total
|
|
? `<span>${countText}</span> photos on display`
|
|
: `<span>${countText}</span> shown • ${totalText} total`;
|
|
}
|
|
|
|
function renderFlatGallery(photoArray) {
|
|
gallery.innerHTML = ''; // Clear skeleton or old photos
|
|
updateResultCount(photoArray.length);
|
|
|
|
if (photoArray.length === 0) {
|
|
if (noResults) {
|
|
gallery.appendChild(noResults);
|
|
noResults.style.display = 'block';
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (noResults) {
|
|
noResults.style.display = 'none';
|
|
}
|
|
|
|
photoArray.forEach((photo, idx) => {
|
|
const resolveUrl = (p) => {
|
|
if (typeof p !== 'string') return '';
|
|
if (p.startsWith('http') || p.startsWith('assets') || p.startsWith('/assets')) return p;
|
|
return `http://localhost:5000/${p}`;
|
|
};
|
|
const src = resolveUrl(photo.path);
|
|
const srcset = photo.variants
|
|
? [
|
|
photo.variants.thumb ? `${resolveUrl(photo.variants.thumb)} 640w` : null,
|
|
photo.variants.medium ? `${resolveUrl(photo.variants.medium)} 1200w` : null,
|
|
src ? `${src} 2000w` : null
|
|
].filter(Boolean).join(', ')
|
|
: '';
|
|
const photoTags = normalizeTags(photo.tags);
|
|
const photoCard = document.createElement('div');
|
|
photoCard.className = 'gallery-item';
|
|
const tagBadges = photoTags.map(tag => `<span class="tag-chip"><i class="fa-solid fa-wand-magic-sparkles"></i>${tag}</span>`).join('');
|
|
photoCard.innerHTML = `
|
|
<div class="gallery-photo">
|
|
<img loading="lazy" ${srcset ? `srcset="${srcset}" sizes="(min-width: 1024px) 33vw, (min-width: 768px) 45vw, 90vw"` : ''} src="${src}" alt="${photo.caption}" data-caption="${photo.caption}" data-tags="${photoTags.join(',')}" data-full-src="${src}" decoding="async">
|
|
</div>
|
|
<div class="gallery-overlay">
|
|
<div class="overlay-bottom">
|
|
<p class="overlay-title">${photo.caption}</p>
|
|
<p class="overlay-subtitle">${photoTags.join(', ')}</p>
|
|
<div class="overlay-tags" aria-label="Tags for ${photo.caption}">${tagBadges}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
gallery.appendChild(photoCard);
|
|
setTimeout(() => requestAnimationFrame(() => photoCard.classList.add('is-visible')), idx * 70);
|
|
|
|
const imgEl = photoCard.querySelector('img');
|
|
imgEl.addEventListener('click', (e) => {
|
|
const card = e.currentTarget.closest('.gallery-item');
|
|
if (isTouchDevice && card) {
|
|
if (!card.classList.contains('touch-active')) {
|
|
card.classList.add('touch-active');
|
|
setTimeout(() => card.classList.remove('touch-active'), 2200);
|
|
return;
|
|
}
|
|
}
|
|
openModal(e.target);
|
|
if (card) card.classList.remove('touch-active');
|
|
});
|
|
});
|
|
}
|
|
|
|
function filterPhotos() {
|
|
const searchTerm = searchInput.value.toLowerCase();
|
|
// Deactivate tag buttons when searching
|
|
filterBtns.forEach(btn => btn.classList.remove('is-active'));
|
|
if (searchTerm) {
|
|
const filteredPhotos = photos.filter(photo => {
|
|
const photoTags = normalizeTags(photo.tags);
|
|
const captionMatch = photo.caption.toLowerCase().includes(searchTerm);
|
|
const tagMatch = photoTags.some(tag => tag.toLowerCase().includes(searchTerm));
|
|
return captionMatch || tagMatch;
|
|
});
|
|
renderFlatGallery(filteredPhotos);
|
|
} else {
|
|
renderFlatGallery(photos);
|
|
// Reactivate 'All' button if search is cleared
|
|
document.querySelector('.filter-btn[data-tag="all"]').classList.add('is-active');
|
|
}
|
|
}
|
|
|
|
function filterByTag(tag) {
|
|
searchInput.value = '';
|
|
if (tag === 'all') {
|
|
renderFlatGallery(photos);
|
|
} else {
|
|
const filteredPhotos = photos.filter(photo => {
|
|
const photoTags = normalizeTags(photo.tags);
|
|
return photoTags.some(t => t.toLowerCase() === tag);
|
|
});
|
|
renderFlatGallery(filteredPhotos);
|
|
}
|
|
}
|
|
|
|
function openModal(imageElement) {
|
|
const rect = imageElement.getBoundingClientRect();
|
|
const centerX = window.innerWidth / 2;
|
|
const centerY = window.innerHeight / 2;
|
|
const imgCenterX = rect.left + rect.width / 2;
|
|
const imgCenterY = rect.top + rect.height / 2;
|
|
const translateX = imgCenterX - centerX;
|
|
const translateY = imgCenterY - centerY;
|
|
const scaleStartRaw = Math.max(
|
|
rect.width / (window.innerWidth * 0.8),
|
|
rect.height / (window.innerHeight * 0.8),
|
|
0.55
|
|
);
|
|
const scaleStart = Math.min(Math.max(scaleStartRaw, 0.72), 0.96);
|
|
document.documentElement.style.setProperty('--modal-img-translate-x', `${translateX}px`);
|
|
document.documentElement.style.setProperty('--modal-img-translate-y', `${translateY}px`);
|
|
document.documentElement.style.setProperty('--modal-img-scale', scaleStart.toFixed(3));
|
|
modalImg.src = imageElement.dataset.fullSrc || imageElement.src;
|
|
modalCaption.textContent = imageElement.dataset.caption;
|
|
if (modalCaptionTags) {
|
|
const tags = (imageElement.dataset.tags || '').split(',').filter(Boolean);
|
|
modalCaptionTags.innerHTML = tags.map(t => `<span class="tag-chip"><i class="fa-solid fa-wand-magic-sparkles"></i>${t}</span>`).join('');
|
|
}
|
|
modal.classList.remove('show-bg');
|
|
modal.classList.add('chrome-hidden');
|
|
modal.classList.add('is-active');
|
|
document.documentElement.classList.add('is-clipped');
|
|
document.body.classList.add('modal-open');
|
|
if (topButton) topButton.style.display = 'none';
|
|
// Fade in chrome and background immediately after paint
|
|
requestAnimationFrame(() => {
|
|
modal.classList.add('show-bg');
|
|
modal.classList.remove('chrome-hidden');
|
|
});
|
|
}
|
|
|
|
function closeModal() {
|
|
modal.classList.remove('is-active');
|
|
modal.classList.remove('show-bg');
|
|
modal.classList.remove('chrome-hidden');
|
|
document.documentElement.classList.remove('is-clipped');
|
|
document.body.classList.remove('modal-open');
|
|
document.documentElement.style.removeProperty('--modal-img-translate-x');
|
|
document.documentElement.style.removeProperty('--modal-img-translate-y');
|
|
document.documentElement.style.removeProperty('--modal-img-scale');
|
|
if (topButton) {
|
|
const shouldShow = document.body.scrollTop > 130 || document.documentElement.scrollTop > 130;
|
|
topButton.style.display = shouldShow ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
searchInput.addEventListener('keyup', filterPhotos);
|
|
|
|
filterBtns.forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const tag = btn.dataset.tag;
|
|
filterByTag(tag.toLowerCase());
|
|
filterBtns.forEach(otherBtn => otherBtn.classList.remove('is-active'));
|
|
btn.classList.add('is-active');
|
|
});
|
|
});
|
|
|
|
|
|
if (modalCloseBtn) modalCloseBtn.addEventListener('click', closeModal);
|
|
modalBackground.addEventListener('click', closeModal);
|
|
|
|
function renderSkeletonLoader() {
|
|
gallery.innerHTML = ''; // Clear gallery
|
|
for (let i = 0; i < 8; i++) {
|
|
const skeletonItem = document.createElement('div');
|
|
skeletonItem.className = 'skeleton-item';
|
|
gallery.appendChild(skeletonItem);
|
|
}
|
|
}
|
|
|
|
renderSkeletonLoader();
|
|
fetchPhotos();
|
|
});
|