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 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 / banos → banos
garajes → parqueaderos
num_habitaciones → habitaciones
conjunto / conjunto_edificio → conjunto
barriocomun → barrio
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
}
// 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
}
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);
};
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)
: "--";
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);
};
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);
};
Total number of items to paginate
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;
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);
}
};
DOM element to render dots into (will be cleared first)
Zero-based index of current active page
Callback function called with page index when dot is clicked: (pageIndex: number) => void
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);
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();
}
});