Real Estate
Real Estate Example for appartment rentals
In this example, there is a list of buildings, which is the items array. Each item has an array of units.
The Buildings
The Units withing a building
The Carousel Controls accepts structured data to display a series of interactive items, each potentially with multiple panels, forms, images, and detailed content. Each item can include titles, images, descriptions, forms, and buttons with various functions. Below is a comprehensive guide on how to pass data to the Carousel Control and define the behavior of the interactive elements.
Tabs
The first tab represents the items, it is the Handlebars template for creating the cards. One tab is required, other tabs are optional. Additional tabs are panel views of that item information. The name of the tab is what you use to add buttons to navigage between the different panel.
The carousel-card class is the main container for each product card in the carousel.
data-index is used to track the index of each card in the carousel.
The card-button class must be used for all the buttons in the card to listen the click events.
The data-carousel-id attribute is used to identify the carousel instance for navigation and image selection.
If there are multiple images, the each image should be wrapped in an img tag with the attribute data-carousel-item along with the id attribute in the format carousel-{index}-item-{imgIdx}
where index is the index of the item and imgIdx is the index of the image. Then data-carousel-prev and data-carousel-next attributes are used for the previous and next buttons to navigate through the images.
Handlebars Tab "List"
{{#each items as |item index|}}
<div class="modern-carousel-card carousel-card" data-index="{{index}}" data-item='{{json item}}'>
<div class="flex flex-col">
<div class="carousel-img-wrap carousel-container card-image" data-carousel-id="carousel-{{index}}" id="carousel-{{index}}" data-carousel="slide">
{{#if (gt item.images.length 0)}}
{{#each item.images as |img imgIdx|}}
<img src="{{img}}" alt="Image for {{../name}}" class="carousel-img card-image" data-carousel-item id="carousel-{{index}}-item-{{imgIdx}}" />
{{/each}}
{{#if (gt item.images.length 1)}}
<button class="carousel-prev" aria-label="Previous image" data-carousel-prev id="carousel-{{index}}-prev">←</button>
<button class="carousel-next" aria-label="Next image" data-carousel-next id="carousel-{{index}}-next">→</button>
{{/if}}
{{else}}
<img src="https://placehold.co/400x240?text=No+Image" alt="No image available" class="carousel-img" style="display:block;" />
{{/if}}
</div>
<div class="address-wrap">
<div class="card-title-modern">{{item.name}}</div>
<div class="card-distance">Distance: {{formatDistance item.distance}} Km</div>
{{#with (splitAddress (concat item.location.Address ', ' item.location.City ', ' item.location.ProvinceCode ' ' item.location.PostalCode)) as |addr|}}
<div class="card-address">
{{addr.[0]}}<br/>
{{addr.[1]}}
</div>
{{/with}}
</div>
</div>
<div class="card-details">
<div class="card-desc-modern">{{{truncateHtml item.buildingDescription 320}}}</div>
<div class="card-actions-modern">
<button class="card-button-modern card-button" data-type="panel" onclick='carousel.selectByName("Units")'>Units</button>
<button class="card-button-modern card-button" data-type="panel" onclick='carousel.selectByName("Info")'>Info</button>
<button class="card-button-modern" onclick='ai12zBot.sendMessage("Request a Tour form","Request a Tour")'>Book Tour</button>
</div>
</div>
</div>
{{/each}}
<div class="pagination-controls-modern">
<button class="list-btn prev-btn" {{#unless hasPrev}}disabled{{/unless}}>Prev</button>
<span>Page {{getcurrentPage}} of {{getPageCount}}</span>
<button class="list-btn next-btn" {{#unless hasNext}}disabled{{/unless}}>Next</button>
</div>
Note:
1st div tag has to have the class carousel-card
The buttons for image carousel, the class is important
The carousel.selectByName the button needs the class card-button
The panel-button class must be used for the buttons in the details panel to listen the click events.
Handlebars Tab "Units"
<div class="suite-row-scroll">
{{#each item.suites as |suite index|}}
<div class="suite-card">
<div class="card-title">
Suite {{inc index}}: {{suite.bedrooms}} Bed / {{suite.bathrooms}} Bath
</div>
<div class="card-subtitle">
{{suite.squareFeet}} sq ft - ${{suite.rate}} CAD/Month
</div>
<div class="card-subtitle">Available: {{suite.availabilityDate}}</div>
<img src="{{suite.floorplanImage}}" alt="Floor Image of {{../item.name}}" />
</div>
{{/each}}
</div>
<button
class="units-back-btn panel-button list-btn text-white"
data-type="panel"
onclick='carousel.selectByName("List"); setTimeout(window.setupAllCarousels, 100);'
>
Back
</button>
Note: the button to the list needs the class list-btn The panel-button class must be used for the buttons in the panel section to listen the click events.
Handlebars Tab "Info"
<div class="amenities-section p-4 bg-gray-50 rounded-md shadow-md w-full">
<h4 class="text-lg font-semibold text-gray-800 mb-2">Amenities</h4>
<ul class="list-disc list-inside text-gray-700 space-y-1">
{{#each item.amenities}}
<li>{{this}}</li>
{{/each}}
</ul>
<h4 class="text-lg font-semibold text-gray-800 mb-2">Utilities</h4>
<ul class="list-disc list-inside text-gray-700 space-y-1">
{{#each item.utilities}}
<li>{{this}}</li>
{{/each}}
</ul>
<p>
<b>Contact Info: </b> {{item.location.Address}}, {{item.location.City}},
{{item.location.Country}}
</p>
<button
class="panel-button list-btn text-white"
data-type="panel"
onclick='carousel.selectByName("List"); setTimeout(window.setupAllCarousels, 100);'
>
Back
</button>
</div>
Note: The button with carousel.selectByName("List"), going to the list tab, needs the class list-btn
Script
// --- Custom Handlebars Helpers ---
window.Handlebars.registerHelper("inc", function (value) {
return parseInt(value, 10) + 1
})
window.Handlebars.registerHelper("formatDistance", function (distance) {
if (typeof distance === "number") return distance.toFixed(1)
var n = parseFloat(distance)
return isNaN(n) ? distance : n.toFixed(1)
})
window.Handlebars.registerHelper("splitAddress", function (addressLine) {
if (!addressLine) return ["", ""]
var idx = addressLine.indexOf(",")
if (idx === -1) return [addressLine, ""]
return [addressLine.slice(0, idx), addressLine.slice(idx + 1).trim()]
})
window.Handlebars.registerHelper("gt", function (a, b, options) {
return a > b
})
window.Handlebars.registerHelper("concat", function () {
// Remove the last argument (Handlebars options object)
var args = Array.prototype.slice.call(arguments, 0, -1)
return args.join("")
})
window.Handlebars.registerHelper("truncateHtml", function (html, maxLen) {
if (!html) return ""
var div = document.createElement("div")
div.innerHTML = html
var text = div.textContent || div.innerText || ""
if (text.length <= maxLen) return html
var truncated = text.slice(0, maxLen)
if (truncated.lastIndexOf(" ") > 0)
truncated = truncated.slice(0, truncated.lastIndexOf(" "))
return truncated + "…"
})
window.Handlebars.registerHelper("gt", function (a, b, options) {
return a > b
})
Style
/* --- Modern Carousel Card Styles --- */
.modern-carousel-card {
display: flex;
flex-direction: row;
align-items: stretch;
background: #fff;
border-radius: 1.5rem;
box-shadow: 0 8px 32px 0 rgba(52, 78, 134, 0.10), 0 1.5px 8px 0 rgba(40, 30, 70, 0.04);
margin-bottom: 2.2rem;
padding: 0;
border: 1.5px solid #e5e7eb;
min-width: 0;
width: 100%;
overflow: visible;
}
.carousel-img-wrap {
flex: 1 1 16%;
min-width: 170px;
max-width: 260px;
background: #f6f8fa;
border-top-left-radius: 1.5rem;
/*border-bottom-left-radius: 1.5rem;*/
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
position: relative;
overflow: hidden;
aspect-ratio: 4/3;
padding-top: 1.2rem;
padding-bottom: 1.2rem;
}
.carousel-img {
width: 100%;
height: 180px;
object-fit: cover;
border-radius: 1.5rem 0 0 0rem;
background: #f3f4f6;
display: block;
transition: opacity 0.2s ease-in-out;
position: absolute;
}
.carousel-img.active {
opacity: 1;
visibility: visible;
position: relative;
z-index: 1;
}
.carousel-img-wrap .card-title-modern {
margin-top: 0.7rem;
margin-bottom: 0.3rem;
font-size: 1.25rem;
font-weight: 800;
color: #1e293b;
text-align: center;
}
.carousel-img-wrap .card-distance {
color: #64748b;
font-size: 1.05rem;
margin-bottom: 0.1rem;
text-align: center;
}
.carousel-img-wrap .card-address {
color: #334155;
font-size: 1.01rem;
margin-bottom: 0.7rem;
text-align: center;
}
.card-details {
flex: 1 1 64%;
padding: 2.2rem 2rem 2.2rem 1.2rem;
display: flex;
flex-direction: column;
justify-content: flex-start;
min-width: 0;
min-height: 0;
}
.card-title-modern {
font-size: 1.2rem;
font-weight: 200;
color: #1e293b;
margin-bottom: 0.7rem;
font-family: 'Inter', Arial, sans-serif;
}
.card-distance {
color: #64748b;
font-size: 1.1rem;
margin-bottom: 0.3rem;
}
.card-address {
color: #334155;
font-size: 1.08rem;
margin-bottom: 0.7rem;
}
.card-desc-modern {
color: #334155;
font-size: 1.13rem;
margin-bottom: 1.2rem;
line-height: 1.55;
font-family: 'Inter', Arial, sans-serif;
word-break: break-word;
}
.card-actions-modern {
display: flex;
flex-wrap: wrap;
gap: 0.7rem;
margin-top: 1.2rem;
}
.card-button-modern {
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
letter-spacing: 0.07em;
text-transform: uppercase;
font-size: 1.01rem;
padding: 0.7em 1.5em;
border-radius: 1.2em;
border: none;
box-shadow: 0 2px 8px rgba(52, 123, 229, 0.10);
cursor: pointer;
transition: background 0.18s, box-shadow 0.14s, transform 0.10s;
min-width: 98px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Inter', Arial, sans-serif;
line-height: 1.15;
}
.card-button-modern:hover, .card-button-modern:focus {
background: linear-gradient(135deg, #2563eb 0%, #60a5fa 100%);
box-shadow: 0 4px 16px rgba(52, 123, 229, 0.13);
transform: scale(1.04);
}
/* Carousel Dots and Arrows */
.carousel-dots {
position: absolute;
left: 50%;
bottom: 12px;
transform: translateX(-50%);
display: flex;
gap: 0.4rem;
z-index: 2;
}
.carousel-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #e5e7eb;
border: none;
cursor: pointer;
transition: background 0.18s;
outline: none;
}
.carousel-dot.active, .carousel-dot:focus {
background: #2d7be5;
}
.carousel-prev, .carousel-next {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: #fff;
border: none;
border-radius: 50%;
width: 34px;
height: 34px;
box-shadow: 0 2px 8px rgba(52, 123, 229, 0.10);
color: #2d7be5;
font-size: 1.3rem;
cursor: pointer;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.15s, color 0.15s;
}
.carousel-prev { left: 10px; }
.carousel-next { right: 10px; }
.carousel-prev:hover, .carousel-next:hover {
background: #e0e7ff;
color: #1e40af;
}
/* Responsive Styles */
@media (max-width: 700px) {
.modern-carousel-card {
flex-direction: column;
border-radius: 1.1rem;
max-width: 99vw;
min-width: 0;
}
.carousel-img-wrap {
border-radius: 1.1rem 1.1rem 0 0;
min-width: 0;
max-width: 100%;
aspect-ratio: 4/3;
}
.carousel-img {
height: 170px;
border-radius: 1.1rem 1.1rem 0 0;
}
.card-details {
padding: 1.5rem 1.1rem 1.4rem 1.1rem;
}
.card-actions-modern {
flex-direction: column;
gap: 0.7rem;
margin-top: 1.1rem;
}
.card-button-modern {
width: 100%;
min-width: 0;
min-height: 54px;
font-size: 1.1rem;
padding: 1.2em 0;
border-radius: 1.2em;
margin-left: 0;
}
}
/* Pagination Controls */
.pagination-controls-modern {
display: flex;
align-items: center;
justify-content: center;
gap: 1.2rem;
margin: 1.2rem 0 0 0;
}
.list-btn.prev-btn, .list-btn.next-btn {
background: #f3f4f6;
color: #2d7be5;
border: none;
border-radius: 1.2em;
padding: 0.6em 1.2em;
font-weight: 600;
font-size: 1.01rem;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.list-btn[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
/* --- Units Panel --- */
.suite-row-scroll {
display: flex;
flex-wrap: wrap;
gap: 1.2rem;
overflow-x: auto;
padding-bottom: 1.2rem;
}
.suite-card {
background: #fff;
border-radius: 1.1rem;
box-shadow: 0 2px 8px rgba(52, 78, 134, 0.10);
border: 1.2px solid #e5e7eb;
min-width: 260px;
max-width: 340px;
flex: 1 1 260px;
display: flex;
flex-direction: column;
align-items: stretch;
padding: 1.2rem 1.2rem 0.7rem 1.2rem;
}
.suite-card img {
width: 100%;
height: 160px;
object-fit: cover;
border-radius: 0.7rem;
margin-top: 0.7rem;
background: #f3f4f6;
}
.card-title {
font-size: 1.15rem;
font-weight: 700;
color: #1e293b;
margin-bottom: 0.3rem;
}
.card-subtitle {
font-size: 1.01rem;
color: #334155;
margin-bottom: 0.2rem;
}
.units-back-btn {
margin: 1.2rem 0 0 0;
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
border-radius: 1.2em;
padding: 0.7em 1.5em;
border: none;
font-size: 1.01rem;
cursor: pointer;
}
/* --- Info Panel --- */
.amenities-section {
background: #f8fafc;
border-radius: 1.1rem;
box-shadow: 0 2px 8px rgba(52, 78, 134, 0.10);
border: 1.2px solid #e5e7eb;
margin: 0.7rem 0 0 0;
}
.amenities-section h4 {
color: #1e293b;
font-size: 1.15rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.amenities-section ul {
margin-bottom: 0.7rem;
}
.amenities-section li {
font-size: 1.01rem;
color: #334155;
}
.amenities-section p {
margin-top: 0.7rem;
color: #334155;
font-size: 1.01rem;
}
.amenities-section .panel-button {
margin-top: 1.2rem;
background: linear-gradient(135deg, #2d7be5 0%, #3b82f6 100%);
color: #fff;
font-weight: 700;
border-radius: 1.2em;
padding: 0.7em 1.5em;
border: none;
font-size: 1.01rem;
cursor: pointer;
}
.address-wrap {
padding:10px;
}
}
the JSON
{
"indicator": true,
"items": [
{
"amenities": ["Vinyl Plank Floors", "Laundry facilities"],
"buildingDescription": "<p>30 Edith offers comfortable living in Toronto's pleasant Yonge and Eglinton Corridor. Offering newly renovated, pet-friendly 1 & 2 bedroom suites, indoor and outdoor parking, and spacious balconies offering scenic views of Eglinton Park, residents will enjoy a comfortable lifestyle. A short walk to Yonge Street provides access to various dining and shopping options, and the nearby Yonge/Eglinton subway station offers easy transit throughout Toronto. Discover why 30 Edith is the perfect place to call home.</p>",
"distance": 6.103,
"id": "278818",
"images": [
"https://assets.rentsync.com/interrent_reit/images/gallery/1152/1714166313643_04_26_2024_16_53_46.jpg",
"https://assets.rentsync.com/interrent_reit/images/gallery/1152/1707235842689_30-Edith-2-Bed-Plan-C-Kitchen.jpg"
],
"location": {
"Address": "30 Edith Drive",
"City": "Toronto",
"Country": "Canada",
"CountryCode": "CAN",
"Intersection": null,
"Latitude": "43.7065652",
"Longitude": "-79.4033893",
"Neighbourhood": null,
"PostalCode": "M4R 1Y8",
"Province": "Ontario",
"ProvinceCode": "ON"
},
"name": "30 Edith",
"panels": [],
"suites": [
{
"availabilityDate": "Available Now",
"bathrooms": "1",
"bedrooms": "1",
"floorplanImage": "https://assets.rentsync.com/interrent_reit/images/floorplans/1718660925_30_Edith_-_Plan_A_1096-1A_Second_Floor_copy.jpg",
"id": "1044818",
"rate": "2199",
"squareFeet": "508"
},
{
"availabilityDate": "2025-05-31",
"bathrooms": "1",
"bedrooms": "1",
"floorplanImage": "https://assets.rentsync.com/interrent_reit/images/floorplans/1742025039_30_Edith_-_Plan_D_1096-1D_Second_Floor_copy.jpg",
"id": "1044819",
"rate": "2499",
"squareFeet": "485"
},
{
"availabilityDate": "Available Now",
"bathrooms": "1",
"bedrooms": "1",
"floorplanImage": "https://assets.rentsync.com/interrent_reit/images/floorplans/1738918135_30_Edith_-_Plan_B_1096-1B_Second_Floor_copy.jpg",
"id": "1048468",
"rate": "2549",
"squareFeet": "658"
}
],
"utilities": ["Heat", "Water"]
}
]
}