Overview
The gallery component provides a full-featured image viewing experience with:
- Main image display with navigation controls
- Thumbnail grid with pagination
- Responsive design (4 thumbnails on mobile, 8 on desktop)
- Lightbox integration for full-screen viewing
- 360° tour button overlay (when available)
HTML Structure
The gallery is defined in index.html at lines 99-137:
<section class="bg-white dark:bg-slate-800 p-4 rounded-2xl shadow-xl">
<!-- Main Image Display -->
<div class="relative aspect-[16/9] w-full rounded-xl overflow-hidden mb-4 group">
<img id="gallery-main-image" alt="Imagen principal" class="w-full h-full object-cover" src=""/>
<!-- 360 Tour Overlay -->
<div id="btn-360-container" class="absolute inset-0 flex items-center justify-center pointer-events-none" style="display: none;">
<a id="property-360" href="#" target="_blank" rel="noopener" class="btn-360 pointer-events-auto flex items-center gap-3 px-8 py-4 rounded-full text-slate-900 font-display font-extrabold text-lg uppercase tracking-widest transition-all duration-300 border-2 border-white/20">
<span class="material-symbols-outlined text-3xl">360</span>
Vista 360°
</a>
</div>
<!-- Navigation Arrows -->
<button id="gallery-main-prev" class="absolute left-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-black/40 hover:bg-black/60 text-white transition-colors z-10">
<span class="material-icons-outlined">chevron_left</span>
</button>
<button id="gallery-main-next" class="absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-full bg-black/40 hover:bg-black/60 text-white transition-colors z-10">
<span class="material-icons-outlined">chevron_right</span>
</button>
<!-- Photo Count Badge -->
<div id="gallery-count" class="absolute bottom-4 right-4 flex gap-2">
<span class="bg-black/50 backdrop-blur-md text-white px-3 py-1 rounded-lg text-xs font-medium flex items-center gap-1">
<span class="material-icons-outlined text-sm">photo_library</span>
<span id="photo-count">0</span> Fotos
</span>
</div>
</div>
<!-- Thumbnail Navigation -->
<div class="relative">
<div class="flex items-center gap-2">
<button id="thumb-prev" class="flex-shrink-0 p-2 rounded-full bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-600 dark:text-slate-300 transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<span class="material-icons-outlined text-xl">chevron_left</span>
</button>
<div id="gallery-thumbnails" class="flex-1 grid grid-cols-4 md:grid-cols-8 gap-2"></div>
<button id="thumb-next" class="flex-shrink-0 p-2 rounded-full bg-slate-100 dark:bg-slate-700 hover:bg-slate-200 dark:hover:bg-slate-600 text-slate-600 dark:text-slate-300 transition-colors disabled:opacity-30 disabled:cursor-not-allowed">
<span class="material-icons-outlined text-xl">chevron_right</span>
</button>
</div>
<!-- Pagination dots -->
<div id="thumb-pagination" class="flex justify-center gap-2 mt-3"></div>
</div>
</section>
renderGallery() Function
The main gallery rendering function from app.js:237-392:
const renderGallery = (images) => {
const mainImage = $("#gallery-main-image");
const thumbContainer = $("#gallery-thumbnails");
const photoCount = $("#photo-count");
const paginationContainer = $("#thumb-pagination");
const prevBtn = $("#thumb-prev");
const nextBtn = $("#thumb-next");
const mainPrevBtn = $("#gallery-main-prev");
const mainNextBtn = $("#gallery-main-next");
// Resolve image paths (handle relative and absolute URLs)
const resolvedImages = images.length
? images.map((img) => (img.startsWith("http") ? img : `images/${img}`))
: ["https://via.placeholder.com/1200x750?text=Sin+imagen"];
// Store images for lightbox and gallery
lightboxImages = resolvedImages;
galleryImages = resolvedImages;
galleryThumbsPerPage = getThumbsPerPage();
// Update photo count
if (photoCount) {
photoCount.textContent = resolvedImages.length;
}
// Set active image
const setActive = (index) => {
galleryCurrentIndex = index;
mainImage.src = resolvedImages[index];
mainImage.alt = `Imagen ${index + 1}`;
updateGalleryThumbHighlights();
// Show 360 button only on first image
const btn360Container = $("#btn-360-container");
if (btn360Container && btn360Container.dataset.has360 === "true") {
btn360Container.style.display = index === 0 ? "flex" : "none";
}
};
// Event listeners for navigation
if (mainPrevBtn) mainPrevBtn.onclick = galleryPrevImage;
if (mainNextBtn) mainNextBtn.onclick = galleryNextImage;
// Make main image clickable to open lightbox
mainImage.classList.add("cursor-pointer");
mainImage.onclick = () => openLightbox(galleryCurrentIndex);
// Initial render
renderThumbnails();
setActive(0);
};
Responsive Thumbnails Per Page
From app.js:31-33:
const getThumbsPerPage = () => {
return window.innerWidth < 768 ? 4 : 8;
};
From app.js:288-322:
const renderThumbnails = () => {
thumbContainer.innerHTML = "";
galleryThumbsPerPage = getThumbsPerPage();
const totalPages = getTotalPages(resolvedImages.length, galleryThumbsPerPage);
const startIndex = galleryThumbPage * galleryThumbsPerPage;
const endIndex = Math.min(startIndex + galleryThumbsPerPage, resolvedImages.length);
// Render thumbnails for current page
for (let i = startIndex; i < endIndex; i++) {
const src = resolvedImages[i];
const thumb = document.createElement("div");
const isActive = i === galleryCurrentIndex;
thumb.className = `thumbnail-item aspect-square rounded-md overflow-hidden bg-slate-100 dark:bg-slate-700 cursor-pointer transition ${isActive ? "ring-2 ring-primary opacity-100" : "opacity-80 hover:opacity-100"}`;
const img = document.createElement("img");
img.src = src;
img.alt = `Miniatura ${i + 1}`;
img.className = "w-full h-full object-cover";
thumb.appendChild(img);
// Click to select, double-click to open lightbox
thumb.addEventListener("click", () => setActive(i));
thumb.addEventListener("dblclick", () => openLightbox(i));
thumbContainer.appendChild(thumb);
}
// Update navigation buttons
if (prevBtn) prevBtn.disabled = galleryThumbPage === 0;
if (nextBtn) nextBtn.disabled = galleryThumbPage >= totalPages - 1;
// Render pagination dots
if (paginationContainer) {
renderPaginationDots(paginationContainer, galleryThumbPage, totalPages, (page) => {
galleryThumbPage = page;
renderThumbnails();
}, false);
}
};
Navigation Controls
Image Navigation
From app.js:324-344:
const galleryNextImage = () => {
const newIndex = (galleryCurrentIndex + 1) % resolvedImages.length;
setActive(newIndex);
// Auto-advance page if needed
const newPage = Math.floor(newIndex / galleryThumbsPerPage);
if (newPage !== galleryThumbPage) {
galleryThumbPage = newPage;
renderThumbnails();
}
};
const galleryPrevImage = () => {
const newIndex = (galleryCurrentIndex - 1 + resolvedImages.length) % resolvedImages.length;
setActive(newIndex);
// Auto-advance page if needed
const newPage = Math.floor(newIndex / galleryThumbsPerPage);
if (newPage !== galleryThumbPage) {
galleryThumbPage = newPage;
renderThumbnails();
}
};
Thumbnail Page Navigation
From app.js:346-359:
const thumbNextPage = () => {
const totalPages = getTotalPages(resolvedImages.length, galleryThumbsPerPage);
if (galleryThumbPage < totalPages - 1) {
galleryThumbPage++;
renderThumbnails();
}
};
const thumbPrevPage = () => {
if (galleryThumbPage > 0) {
galleryThumbPage--;
renderThumbnails();
}
};
From app.js:39-52:
const renderPaginationDots = (container, currentPage, totalPages, onPageClick, isLight = false) => {
container.innerHTML = "";
if (totalPages <= 1) return;
for (let i = 0; i < totalPages; i++) {
const dot = document.createElement("button");
const activeClass = isLight
? (i === currentPage ? "bg-white" : "bg-white/40 hover:bg-white/60")
: (i === currentPage ? "bg-primary" : "bg-slate-300 dark:bg-slate-600 hover:bg-slate-400");
dot.className = `w-2.5 h-2.5 rounded-full transition-colors ${activeClass}`;
dot.addEventListener("click", () => onPageClick(i));
container.appendChild(dot);
}
};
Gallery State Management
From app.js:9-13:
// Gallery state
let galleryImages = [];
let galleryCurrentIndex = 0;
let galleryThumbPage = 0;
let galleryThumbsPerPage = 8; // Will be adjusted for mobile
Responsive Behavior
The gallery automatically adjusts to screen size changes from app.js:380-387:
// Handle window resize
window.addEventListener("resize", () => {
const newPerPage = getThumbsPerPage();
if (newPerPage !== galleryThumbsPerPage) {
galleryThumbsPerPage = newPerPage;
galleryThumbPage = Math.floor(galleryCurrentIndex / galleryThumbsPerPage);
renderThumbnails();
}
});
The gallery component automatically switches between 4 thumbnails per page on mobile devices (< 768px) and 8 thumbnails on desktop.
Key Features
- Automatic pagination: Thumbnails are automatically paginated based on screen size
- Active highlighting: Current image is highlighted with a primary-colored ring
- Lightbox integration: Click main image or double-click thumbnail to open lightbox
- 360° tour support: Shows overlay button on first image when virtual tour is available
- Keyboard navigation: Arrow keys work in lightbox mode
- Auto-advance pages: Automatically switches thumbnail pages when navigating past page boundaries