Skip to main content

Overview

Ficha Dubai features a comprehensive image gallery system with thumbnail pagination, responsive layouts, and smooth navigation. The gallery adapts to different screen sizes and provides an intuitive browsing experience.

Main Image Display

The gallery displays a large main image with navigation arrows and click-to-expand functionality:
const mainImage = $("#gallery-main-image");

// Make main image clickable to open lightbox
mainImage.classList.add("cursor-pointer");
mainImage.onclick = () => openLightbox(galleryCurrentIndex);

Thumbnail Pagination

The gallery uses a sophisticated pagination system that adapts to screen size:
const getThumbsPerPage = () => {
  return window.innerWidth < 768 ? 4 : 8;
};

const getTotalPages = (totalItems, perPage) => {
  return Math.ceil(totalItems / perPage);
};
On mobile devices (< 768px), the gallery shows 4 thumbnails per page. On desktop, it displays 8 thumbnails per page.

Rendering Thumbnails

The renderThumbnails() function handles the display of paginated thumbnails:
const renderThumbnails = () => {
  thumbContainer.innerHTML = "";
  galleryThumbsPerPage = getThumbsPerPage();
  
  const totalPages = getTotalPages(resolvedImages.length, galleryThumbsPerPage);
  const startIndex = galleryThumbPage * galleryThumbsPerPage;
  const endIndex = Math.min(startIndex + galleryThumbsPerPage, resolvedImages.length);
  
  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);
    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);
  }
};
Code location: app.js:288-322 The gallery maintains its state through several global variables:
// Gallery state
let galleryImages = [];
let galleryCurrentIndex = 0;
let galleryThumbPage = 0;
let galleryThumbsPerPage = 8; // Will be adjusted for mobile
Code location: app.js:10-13

Setting Active Image

The setActive() function updates the main image and highlights the corresponding thumbnail:
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";
  }
};
Code location: app.js:261-272

Image Navigation

Users can navigate through images using arrow buttons or keyboard shortcuts:
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();
  }
};
Code location: app.js:324-344

Thumbnail Page Navigation

const thumbNextPage = () => {
  const totalPages = getTotalPages(resolvedImages.length, galleryThumbsPerPage);
  if (galleryThumbPage < totalPages - 1) {
    galleryThumbPage++;
    renderThumbnails();
  }
};

const thumbPrevPage = () => {
  if (galleryThumbPage > 0) {
    galleryThumbPage--;
    renderThumbnails();
  }
};
Code location: app.js:346-359

Pagination Dots

Visual pagination indicators help users track their position:
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);
  }
};
Code location: app.js:39-52

Responsive Behavior

The gallery automatically adjusts when the window is resized:
// Handle window resize
window.addEventListener("resize", () => {
  const newPerPage = getThumbsPerPage();
  if (newPerPage !== galleryThumbsPerPage) {
    galleryThumbsPerPage = newPerPage;
    galleryThumbPage = Math.floor(galleryCurrentIndex / galleryThumbsPerPage);
    renderThumbnails();
  }
});
Code location: app.js:380-387

HTML Structure

The gallery HTML uses Tailwind CSS for styling:
<section class="bg-white dark:bg-slate-800 p-4 rounded-2xl shadow-xl">
  <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=""/>
    <!-- Navigation arrows on main image -->
    <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>
  </div>
  <!-- Thumbnails with pagination -->
  <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>
Code location: index.html:100-137
Double-click any thumbnail to open the full-screen lightbox viewer.

Key Features

Responsive Thumbnails

Automatically adjusts thumbnail count based on screen size (4 on mobile, 8 on desktop)

Pagination

Visual pagination dots and arrow navigation for browsing large image sets

Highlight Active

Active thumbnail is highlighted with a colored ring for easy identification

Lightbox Integration

Click main image or double-click thumbnails to open full-screen lightbox

Build docs developers (and LLMs) love