The rendering functions transform normalized property data into DOM elements, populating all sections of the property listing page.
populatePage
Main rendering function that populates all page sections with property data.
const populatePage = (data) => {
const property = normalizeProperty(data);
$("#property-title").textContent = property.titulo || "Inmueble en venta";
$("#property-location").textContent = [property.barrio, property.ciudad, property.departamento]
.filter(Boolean)
.join(" · ");
$("#property-price").textContent = formatCurrency(property.precio);
const pricePrev = $("#property-price-prev");
if (pricePrev) {
pricePrev.textContent = property.precio_anterior
? formatCurrency(property.precio_anterior)
: "";
}
$("#property-description").textContent = property.descripcion || "Sin descripción";
// 360° tour button handling...
renderBadges(property);
renderGallery(property.images || []);
renderDetails(property);
renderDistribution(property);
renderFeatures(property.caracteristicas_propiedad || []);
renderServices(property.servicios || []);
renderContact(property);
renderMap(property);
};
Raw property data object from API or JSON file. Can be nested under property, data.property, or at root level. Will be normalized before rendering.
Behavior
- Normalizes property data using
normalizeProperty()
- Updates title, location, price, and description text content
- Handles previous price display (strikethrough pricing)
- Configures 360° tour button visibility and links
- Calls specialized render functions for each page section
Location: ~/workspace/source/app.js:613-662
360° Tour Handling
const btn360Container = $("#btn-360-container");
const tour360 = $("#property-360");
const tour360Btn = $("#property-360-btn");
if (property.url_360) {
if (btn360Container) {
btn360Container.style.display = "flex";
btn360Container.dataset.has360 = "true";
}
if (tour360) tour360.href = property.url_360;
if (tour360Btn) {
tour360Btn.href = property.url_360;
tour360Btn.style.display = "inline-flex";
}
} else {
// Hide 360° tour buttons
}
Example Usage
// From window global
populatePage(window.PROPERTY_DATA);
// From fetch
const data = await fetch("./property-5157395.json").then(r => r.json());
populatePage(data);
renderBadges
Renders badge pills in the hero section header.
const renderBadges = (property) => {
const container = $("#property-badges");
container.innerHTML = "";
const badges = [];
if (property.tipo_inmueble) badges.push(property.tipo_inmueble);
if (property.area_privada) badges.push(`${formatNumber(property.area_privada)} m²`);
if (property.estrato) badges.push(`Estrato ${property.estrato}`);
if (property.estado) badges.push(property.estado);
if (property.ciudad) badges.push(property.ciudad);
badges.forEach((text) => container.appendChild(buildBadge(text)));
};
Normalized property object containing badge fields: tipo_inmueble, area_privada, estrato, estado, ciudad
Behavior
- Clears existing badges
- Collects non-empty badge values in order:
- Property type (e.g., “Apartamento”)
- Private area with m² suffix
- Stratum with “Estrato” prefix
- Status (e.g., “En venta”)
- City name
- Creates badge elements using
buildBadge()
- Appends badges to
#property-badges container
Location: ~/workspace/source/app.js:223-235
Badge Styling
const buildBadge = (text) => {
const badge = document.createElement("span");
badge.className = "px-4 py-1.5 rounded-full bg-white/10 text-white text-sm font-medium backdrop-blur-md";
badge.textContent = text;
return badge;
};
renderDetails
Renders the detailed property specifications list.
const renderDetails = (property) => {
const detailsList = $("#property-details");
detailsList.innerHTML = "";
const areaPrivada = property.area_privada ? `${formatNumber(property.area_privada)} m²` : null;
const areaConstruida = property.area_construida
? `${formatNumber(property.area_construida)} m²`
: null;
const details = [
["Área privada", areaPrivada],
["Área construida", areaConstruida],
["Parqueaderos", property.parqueaderos],
["Baños", property.banos],
["Habitaciones", property.habitaciones],
["Antigüedad", property.antiguedad],
["Tipo", property.tipo_inmueble],
["Estrato", property.estrato],
];
details
.filter(([, value]) => value && value !== "--")
.forEach(([label, value]) => {
// Creates row with label and value
});
};
Normalized property object with detail fields
Behavior
- Clears existing details
- Formats area values with m² suffix
- Creates array of
[label, value] pairs
- Filters out empty/missing values
- Renders each detail as a row with:
- Label: Gray text, left-aligned
- Value: Bold text, right-aligned
- Border separator between rows
Location: ~/workspace/source/app.js:394-429
DOM Structure
<div class="flex justify-between items-center py-2 border-b border-slate-100 dark:border-slate-700/50">
<span class="text-slate-500 text-sm">Área privada</span>
<span class="font-bold">120 m²</span>
</div>
renderDistribution
Renders a simplified distribution summary (rooms, bathrooms, parking, stratum).
const renderDistribution = (property) => {
const list = $("#property-distribution");
list.innerHTML = "";
const items = [
["Habitaciones", property.habitaciones],
["Baños", property.banos],
["Parqueaderos", property.parqueaderos],
["Estrato", property.estrato],
];
items
.filter(([, value]) => value && value !== "--")
.forEach(([label, value]) => {
const li = document.createElement("li");
const span = document.createElement("span");
span.textContent = label;
const strong = document.createElement("strong");
strong.textContent = value;
li.appendChild(span);
li.appendChild(strong);
list.appendChild(li);
});
};
Normalized property object with distribution fields
Behavior
- Clears existing list
- Filters out empty values
- Creates
<li> elements with <span> (label) and <strong> (value)
- Appends to
#property-distribution list
Location: ~/workspace/source/app.js:431-454
renderFeatures
Renders property features as styled badge elements.
const renderFeatures = (features) => {
const container = $("#property-features");
container.innerHTML = "";
if (!features || !features.length) {
container.innerHTML = "<span class=\"text-slate-500 text-sm\">No hay características disponibles.</span>";
return;
}
features.forEach((feature) => {
const span = document.createElement("span");
span.className = "bg-slate-50 dark:bg-slate-700/50 px-4 py-2 rounded-lg text-sm text-slate-700 dark:text-slate-300 border border-slate-100 dark:border-slate-600";
span.textContent = feature;
container.appendChild(span);
});
};
Array of feature strings (e.g., [“Piscina”, “Gimnasio”, “Zona BBQ”])
Behavior
- Clears container
- Shows “No hay características disponibles” if array is empty
- Creates badge-style
<span> elements for each feature
- Appends to
#property-features container
Location: ~/workspace/source/app.js:456-471
Styling
- Light mode: Light gray background with border
- Dark mode: Semi-transparent dark background
- Rounded corners with padding
renderServices
Renders property services with names and costs.
const renderServices = (services) => {
const container = $("#property-services");
container.innerHTML = "";
if (!services || !services.length) {
container.innerHTML = "<p class=\"text-slate-500 text-sm\">No hay servicios registrados.</p>";
return;
}
services.forEach((service) => {
const item = document.createElement("div");
item.className = "flex justify-between items-center p-3 rounded-xl bg-slate-50 dark:bg-slate-700/30";
const label = document.createElement("span");
label.className = "text-sm font-medium";
label.textContent = service.nombre || service.servicio || "Servicio";
const value = document.createElement("span");
value.className = "text-sm font-bold";
value.textContent = service.valor ? formatCurrency(service.valor) : "--";
item.appendChild(label);
item.appendChild(value);
container.appendChild(item);
});
};
Array of service objects with nombre/servicio and valor properties
Service Object Structure
{
nombre: "Agua", // or servicio
valor: 50000 // Cost in COP
}
Behavior
- Clears container
- Shows “No hay servicios registrados” if array is empty
- Creates row for each service with:
- Service name (left, medium weight)
- Formatted cost (right, bold)
- Uses
formatCurrency() for cost display
Location: ~/workspace/source/app.js:473-495
Populates contact links and building information.
const renderContact = (property) => {
const phone = property.telefono || "";
const email = property.correo || "";
const phoneLink = $("#contact-phone");
const phoneText = $("#contact-phone-text");
if (phoneText) {
phoneText.textContent = phone ? `Llamar ${phone}` : "Llamar";
}
if (phoneLink) {
phoneLink.href = phone ? `tel:${phone}` : "#";
}
const emailLink = $("#contact-email");
if (emailLink) {
emailLink.href = email ? `mailto:${email}` : "#";
}
const whatsappLink = $("#contact-whatsapp");
if (phone) {
const cleaned = phone.replace(/\D/g, "");
if (whatsappLink) whatsappLink.href = `https://wa.me/57${cleaned}`;
} else {
if (whatsappLink) whatsappLink.href = "#";
}
const contactBuilding = $("#contact-building");
if (contactBuilding) contactBuilding.textContent = property.conjunto || "--";
const contactAdmin = $("#contact-admin");
if (contactAdmin) contactAdmin.textContent = property.administracion
? formatCurrency(property.administracion)
: "--";
};
Normalized property object with contact fields: telefono, correo, conjunto, administracion
Behavior
- Updates phone link (
tel:) and display text
- Updates email link (
mailto:)
- Updates WhatsApp link with Colombian country code (+57)
- Strips non-digit characters from phone number
- Formats as
https://wa.me/57{cleaned}
- Updates building name
- Updates administration cost with currency formatting
Location: ~/workspace/source/app.js:497-529
WhatsApp Link Example
// Input: telefono = "(300) 123-4567"
// Output: https://wa.me/573001234567
renderMap
Renders the property location map using OpenStreetMap embed.
const renderMap = (property) => {
const address = [property.direccion, property.barrio, property.ciudad]
.filter(Boolean)
.join(", ");
const mapLink = $("#property-map");
const mapPreview = $("#property-map-preview");
const coords = property.latitud && property.longitud
? `${property.latitud},${property.longitud}`
: null;
$("#property-address").textContent = address || "Dirección no disponible";
const mapUrl = coords
? `https://www.google.com/maps/search/?api=1&query=${coords}`
: `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`;
mapLink.href = mapUrl;
if (coords) {
const [lat, lon] = coords.split(",");
const delta = 0.01;
const left = parseFloat(lon) - delta;
const right = parseFloat(lon) + delta;
const top = parseFloat(lat) + delta;
const bottom = parseFloat(lat) - delta;
const iframe = document.createElement("iframe");
iframe.src = `https://www.openstreetmap.org/export/embed.html?bbox=${left}%2C${bottom}%2C${right}%2C${top}&layer=mapnik&marker=${lat}%2C${lon}`;
iframe.loading = "lazy";
iframe.referrerPolicy = "no-referrer-when-downgrade";
iframe.title = "Mapa de ubicación";
iframe.style.border = "0";
iframe.style.width = "100%";
iframe.style.height = "100%";
mapPreview.innerHTML = "";
mapPreview.appendChild(iframe);
} else {
mapPreview.textContent = "Ubicación aproximada";
}
};
Normalized property object with location fields: direccion, barrio, ciudad, latitud, longitud
Behavior
- Constructs address string from available fields
- Updates
#property-address text
- Creates Google Maps search link:
- With coords: Uses lat/long query
- Without coords: Uses address search query
- If coordinates available:
- Calculates bounding box (±0.01 degrees)
- Embeds OpenStreetMap iframe with marker
- Sets iframe attributes for lazy loading
- If no coordinates:
- Shows “Ubicación aproximada” text
Location: ~/workspace/source/app.js:531-569
Map Bounding Box Calculation
const delta = 0.01; // ~1.1 km at equator
left = longitude - delta
right = longitude + delta
top = latitude + delta
bottom = latitude - delta
Integration
Typical rendering flow:
const loadData = async () => {
initLightbox();
const data = await fetch("./property-5157395.json").then(r => r.json());
// populatePage() calls all render functions:
populatePage(data);
// → renderBadges()
// → renderGallery()
// → renderDetails()
// → renderDistribution()
// → renderFeatures()
// → renderServices()
// → renderContact()
// → renderMap()
};