Skip to main content

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);
};

Thumbnail Pagination System

Responsive Thumbnails Per Page

From app.js:31-33:
const getThumbsPerPage = () => {
  return window.innerWidth < 768 ? 4 : 8;
};

Rendering Thumbnails with Pagination

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);
  }
};

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();
  }
};

Pagination Dots Helper

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);
  }
};
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

Build docs developers (and LLMs) love