Skip to main content
The utility functions provide data transformation, currency/number formatting, and pagination helpers used throughout the application.

normalizeProperty

Normalizes raw property data from various API response structures into a consistent format.
const normalizeProperty = (raw) => {
  const property = raw?.property || raw?.data?.property || raw;
  const details = property?.detalles_propiedad || {};
  const images = Array.isArray(property?.images)
    ? property.images
        .slice()
        .sort((a, b) => (a.order || 0) - (b.order || 0))
        .map((img) => img.url || img)
    : [];

  return {
    titulo: property?.titulo,
    descripcion: property?.descripcion,
    url_360: property?.url_360,
    images,
    servicios: property?.servicios || [],
    caracteristicas_propiedad: property?.caracteristicas_propiedad || [],
    tipo_inmueble: details?.tipo_inmueble || property?.tipo_inmueble,
    estrato: details?.estrato || property?.estrato,
    conjunto: details?.conjunto || details?.conjunto_edificio || property?.conjunto,
    direccion: details?.direccion || property?.direccion,
    barrio: details?.barriocomun || property?.barrio,
    ciudad: details?.ciudad || property?.ciudad,
    departamento: details?.departamento || property?.departamento,
    precio: details?.precio_venta ?? property?.precio,
    precio_anterior: details?.precio_anterior ?? property?.precio_anterior,
    area_privada: details?.area ?? property?.area_privada,
    area_construida: details?.area_construida ?? property?.area_construida,
    habitaciones: details?.num_habitaciones ?? property?.habitaciones,
    banos: details?.baños ?? details?.banos ?? property?.banos,
    parqueaderos: details?.garajes ?? property?.parqueaderos,
    antiguedad: details?.anos_antiguedad ?? property?.antiguedad,
    telefono: details?.telefono || details?.contacto_zona || property?.telefono,
    correo: details?.correo || property?.correo,
    nombre_contacto: property?.nombre_contacto,
    administracion: details?.last_admin_price ?? property?.administracion,
    latitud: details?.latitud ?? property?.latitud,
    longitud: details?.longitud ?? property?.longitud,
    estado: property?.estado,
  };
};
raw
object
required
Raw property data from API. Can be nested under raw.property, raw.data.property, or at root level. May have details nested under detalles_propiedad.

Behavior

  • Flexible nesting: Handles property data at root, under property, or under data.property
  • Details extraction: Merges fields from detalles_propiedad object
  • Image sorting: Orders images by order property, extracts URLs
  • Fallback handling: Uses nullish coalescing (??) to handle 0 values correctly
  • Field mapping: Maps various field name variations to consistent names
    • baños / banosbanos
    • garajesparqueaderos
    • num_habitacioneshabitaciones
    • conjunto / conjunto_edificioconjunto
    • barriocomunbarrio
Location: ~/workspace/source/app.js:571-611

Return Value

Returns normalized property object with consistent field names:
{
  titulo: string,
  descripcion: string,
  url_360: string,
  images: string[],         // Sorted by order
  servicios: object[],
  caracteristicas_propiedad: string[],
  tipo_inmueble: string,
  estrato: number,
  conjunto: string,
  direccion: string,
  barrio: string,
  ciudad: string,
  departamento: string,
  precio: number,
  precio_anterior: number,
  area_privada: number,
  area_construida: number,
  habitaciones: number,
  banos: number,
  parqueaderos: number,
  antiguedad: string,
  telefono: string,
  correo: string,
  nombre_contacto: string,
  administracion: number,
  latitud: number,
  longitud: number,
  estado: string
}

Example Input Structures

// Structure 1: Nested under property
{
  property: {
    titulo: "Apartamento",
    detalles_propiedad: {
      precio_venta: 300000000
    }
  }
}

// Structure 2: Nested under data.property
{
  data: {
    property: {
      titulo: "Apartamento"
    }
  }
}

// Structure 3: Flat at root
{
  titulo: "Apartamento",
  precio: 300000000
}

formatCurrency

Formats numeric values as Colombian pesos (COP) with thousand separators.
const formatCurrency = (value) => {
  if (!value && value !== 0) return "--";
  return new Intl.NumberFormat("es-CO", {
    style: "currency",
    currency: "COP",
    maximumFractionDigits: 0,
  }).format(value);
};
value
number
required
Numeric value to format. Returns ”—” for null/undefined/empty values. Correctly handles 0.

Behavior

  • Uses Intl.NumberFormat with Colombian locale (es-CO)
  • Currency: Colombian Peso (COP)
  • No decimal places (maximumFractionDigits: 0)
  • Fallback: Returns "--" for null/undefined values
  • Zero handling: Correctly formats 0 as “COP 0” (not ”—”)
Location: ~/workspace/source/app.js:15-22

Return Value

String formatted as Colombian currency:
formatCurrency(300000000)  // "COP 300.000.000"
formatCurrency(50000)      // "COP 50.000"
formatCurrency(0)          // "COP 0"
formatCurrency(null)       // "--"
formatCurrency(undefined)  // "--"

Usage Examples

// Price display
$("#property-price").textContent = formatCurrency(property.precio);

// Service costs
value.textContent = service.valor ? formatCurrency(service.valor) : "--";

// Administration fees
contactAdmin.textContent = property.administracion
  ? formatCurrency(property.administracion)
  : "--";

formatNumber

Formats numeric values with thousand separators (no currency symbol).
const formatNumber = (value) => {
  if (!value && value !== 0) return "--";
  return new Intl.NumberFormat("es-CO").format(value);
};
value
number
required
Numeric value to format. Returns ”—” for null/undefined/empty values. Correctly handles 0.

Behavior

  • Uses Intl.NumberFormat with Colombian locale (es-CO)
  • No currency symbol or unit
  • Adds thousand separators (periods in Colombian locale)
  • Fallback: Returns "--" for null/undefined values
Location: ~/workspace/source/app.js:24-27

Return Value

String formatted with thousand separators:
formatNumber(120)        // "120"
formatNumber(1500)       // "1.500"
formatNumber(300000)     // "300.000"
formatNumber(0)          // "0"
formatNumber(null)       // "--"

Usage Examples

// Area display
badges.push(`${formatNumber(property.area_privada)} m²`);

// Details list
const areaPrivada = property.area_privada 
  ? `${formatNumber(property.area_privada)} m²` 
  : null;

getThumbsPerPage

Calculates the number of thumbnails to display per page based on viewport width.
const getThumbsPerPage = () => {
  return window.innerWidth < 768 ? 4 : 8;
};

Behavior

  • Mobile (< 768px): Returns 4 thumbnails per page
  • Desktop (≥ 768px): Returns 8 thumbnails per page
  • Breakpoint: 768px (matches Tailwind’s md: breakpoint)
Location: ~/workspace/source/app.js:31-33

Return Value

  • 4 for mobile viewports
  • 8 for desktop viewports

Usage Examples

// Gallery initialization
galleryThumbsPerPage = getThumbsPerPage();

// Responsive recalculation
window.addEventListener("resize", () => {
  const newPerPage = getThumbsPerPage();
  if (newPerPage !== galleryThumbsPerPage) {
    galleryThumbsPerPage = newPerPage;
    galleryThumbPage = Math.floor(galleryCurrentIndex / galleryThumbsPerPage);
    renderThumbnails();
  }
});

getTotalPages

Calculates total number of pagination pages needed for a set of items.
const getTotalPages = (totalItems, perPage) => {
  return Math.ceil(totalItems / perPage);
};
totalItems
number
required
Total number of items to paginate
perPage
number
required
Number of items per page

Behavior

  • Uses Math.ceil() to round up
  • Ensures partial last page is counted
Location: ~/workspace/source/app.js:35-37

Return Value

Integer representing total pages needed:
getTotalPages(10, 8)   // 2 pages (8 + 2)
getTotalPages(16, 8)   // 2 pages (8 + 8)
getTotalPages(17, 8)   // 3 pages (8 + 8 + 1)
getTotalPages(5, 8)    // 1 page
getTotalPages(0, 8)    // 0 pages

Usage Examples

// Gallery thumbnails
const totalPages = getTotalPages(resolvedImages.length, galleryThumbsPerPage);

// Lightbox thumbnails
const totalPages = getTotalPages(lightboxImages.length, LIGHTBOX_THUMBS_PER_PAGE);

// Navigation button state
if (prevBtn) prevBtn.disabled = galleryThumbPage === 0;
if (nextBtn) nextBtn.disabled = galleryThumbPage >= totalPages - 1;

renderPaginationDots

Renders clickable pagination dots for thumbnail navigation.
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);
  }
};
container
HTMLElement
required
DOM element to render dots into (will be cleared first)
currentPage
number
required
Zero-based index of current active page
totalPages
number
required
Total number of pages
onPageClick
function
required
Callback function called with page index when dot is clicked: (pageIndex: number) => void
isLight
boolean
default:"false"
Use light styling for dark backgrounds (lightbox). Default is dark styling for light backgrounds (gallery).

Behavior

  • Clears container before rendering
  • Early return if totalPages <= 1 (no pagination needed)
  • Creates button element for each page
  • Applies styling based on isLight parameter:
    • Light mode (gallery): Active = primary color, Inactive = gray
    • Dark mode (lightbox): Active = white, Inactive = white/40
  • Binds click handler to call onPageClick(i)
Location: ~/workspace/source/app.js:39-52

Styling

// Gallery dots (isLight = false)
Active:   "bg-primary"
Inactive: "bg-slate-300 dark:bg-slate-600 hover:bg-slate-400"

// Lightbox dots (isLight = true)
Active:   "bg-white"
Inactive: "bg-white/40 hover:bg-white/60"

// All dots: "w-2.5 h-2.5 rounded-full transition-colors"

Usage Examples

// Gallery pagination
renderPaginationDots(
  paginationContainer, 
  galleryThumbPage, 
  totalPages, 
  (page) => {
    galleryThumbPage = page;
    renderThumbnails();
  }, 
  false  // Dark styling
);

// Lightbox pagination
renderPaginationDots(
  paginationContainer, 
  lightboxThumbPage, 
  totalPages, 
  (page) => {
    lightboxThumbPage = page;
    renderLightboxThumbnails();
  }, 
  true  // Light styling
);

Helper Function: $ (DOM Selector)

Shorthand for document.querySelector().
const $ = (selector) => document.querySelector(selector);
selector
string
required
CSS selector string

Behavior

  • Returns first matching element or null
  • Used throughout the codebase for concise DOM queries
Location: ~/workspace/source/app.js:1

Usage Examples

const lightbox = $("#lightbox");
const mainImage = $("#gallery-main-image");
const prevBtn = $("#thumb-prev");

// Chaining
$("#property-title").textContent = property.titulo || "Inmueble en venta";

Integration Patterns

Data Flow

// 1. Load raw data
const rawData = await fetch("property.json").then(r => r.json());

// 2. Normalize structure
const property = normalizeProperty(rawData);

// 3. Format values
const priceText = formatCurrency(property.precio);
const areaText = `${formatNumber(property.area_privada)} m²`;

// 4. Calculate pagination
const thumbsPerPage = getThumbsPerPage();
const totalPages = getTotalPages(images.length, thumbsPerPage);

// 5. Render pagination
renderPaginationDots(container, currentPage, totalPages, onPageClick);

Responsive Handling

window.addEventListener("resize", () => {
  const newPerPage = getThumbsPerPage();
  if (newPerPage !== galleryThumbsPerPage) {
    galleryThumbsPerPage = newPerPage;
    // Recalculate current page to keep current image visible
    galleryThumbPage = Math.floor(galleryCurrentIndex / galleryThumbsPerPage);
    renderThumbnails();
  }
});

Build docs developers (and LLMs) love