Skip to main content
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);
};
data
object
required
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)));
};
property
object
required
Normalized property object containing badge fields: tipo_inmueble, area_privada, estrato, estado, ciudad

Behavior

  • Clears existing badges
  • Collects non-empty badge values in order:
    1. Property type (e.g., “Apartamento”)
    2. Private area with m² suffix
    3. Stratum with “Estrato” prefix
    4. Status (e.g., “En venta”)
    5. 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
    });
};
property
object
required
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);
    });
};
property
object
required
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);
  });
};
features
string[]
required
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);
  });
};
services
object[]
required
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

renderContact

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)
    : "--";
};
property
object
required
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
// 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";
  }
};
property
object
required
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()
};

Build docs developers (and LLMs) love