Step 2.7: Build single property template with gallery, details, features, and agent card

This commit is contained in:
Hanson.xyz Dev
2025-11-28 16:39:56 -06:00
parent d71d8c85ba
commit 60f460bee3
11 changed files with 1136 additions and 3 deletions
+1 -1
View File
@@ -407,4 +407,4 @@ UNLOCK TABLES;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
-- Dump completed on 2025-11-28 16:37:18
-- Dump completed on 2025-11-28 16:39:55
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,205 @@
<?php
/**
* Single Property Template
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
get_header();
while (have_posts()) :
the_post();
$property_id = get_the_ID();
// Get property data
$price = get_field('property_price', $property_id);
$street_address = get_field('street_address', $property_id);
$city = get_field('city', $property_id);
$state = get_field('state', $property_id);
$zip_code = get_field('zip_code', $property_id);
$mls_number = get_field('mls_number', $property_id);
$bedrooms = get_field('bedrooms', $property_id);
$bathrooms = get_field('bathrooms', $property_id);
$square_feet = get_field('square_feet', $property_id);
$lot_size = get_field('lot_size', $property_id);
$year_built = get_field('year_built', $property_id);
$garage = get_field('garage', $property_id);
$short_description = get_field('short_description', $property_id);
$property_features = get_field('property_features', $property_id);
$gallery = get_field('property_gallery', $property_id);
$listing_agent = get_field('listing_agent', $property_id);
// Get status from taxonomy
$status_terms = get_the_terms($property_id, 'property_status');
$status = $status_terms && !is_wp_error($status_terms) ? $status_terms[0]->name : '';
$status_class = homeproz_get_status_class($status);
// Get type from taxonomy
$type_terms = get_the_terms($property_id, 'property_type');
$type = $type_terms && !is_wp_error($type_terms) ? $type_terms[0]->name : '';
// Format full address
$full_address = $street_address;
if ($city) $full_address .= ', ' . $city;
if ($state) $full_address .= ', ' . $state;
if ($zip_code) $full_address .= ' ' . $zip_code;
?>
<main id="primary" class="site-main single-property-main">
<!-- Breadcrumbs -->
<nav class="breadcrumbs" aria-label="Breadcrumb">
<div class="container">
<ol class="breadcrumb-list">
<li><a href="<?php echo esc_url(home_url('/')); ?>">Home</a></li>
<li><a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>">Properties</a></li>
<li aria-current="page"><?php echo esc_html($street_address ?: get_the_title()); ?></li>
</ol>
</div>
</nav>
<div class="container">
<div class="single-property-layout">
<!-- Main Content -->
<div class="single-property-content">
<!-- Gallery -->
<?php get_template_part('template-parts/property/property-gallery', null, array('gallery' => $gallery, 'property_id' => $property_id)); ?>
<!-- Property Header -->
<header class="property-header">
<div class="property-header-top">
<?php if ($status) : ?>
<span class="badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html($status); ?></span>
<?php endif; ?>
<?php if ($type) : ?>
<span class="property-type"><?php echo esc_html($type); ?></span>
<?php endif; ?>
</div>
<h1 class="property-title"><?php echo esc_html(homeproz_format_price($price)); ?></h1>
<p class="property-address">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
<?php echo esc_html($full_address); ?>
</p>
<?php if ($mls_number) : ?>
<p class="property-mls">MLS# <?php echo esc_html($mls_number); ?></p>
<?php endif; ?>
</header>
<!-- Property Specs -->
<?php if ($bedrooms || $bathrooms || $square_feet || $lot_size || $year_built || $garage) : ?>
<section class="property-specs-section">
<h2 class="section-title">Property Details</h2>
<ul class="property-specs-grid">
<?php if ($bedrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bedrooms</span>
<span class="spec-value"><?php echo esc_html($bedrooms); ?></span>
</li>
<?php endif; ?>
<?php if ($bathrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bathrooms</span>
<span class="spec-value"><?php echo esc_html($bathrooms); ?></span>
</li>
<?php endif; ?>
<?php if ($square_feet) : ?>
<li class="spec-item">
<span class="spec-label">Square Feet</span>
<span class="spec-value"><?php echo esc_html(number_format($square_feet)); ?></span>
</li>
<?php endif; ?>
<?php if ($lot_size) : ?>
<li class="spec-item">
<span class="spec-label">Lot Size</span>
<span class="spec-value"><?php echo esc_html($lot_size); ?></span>
</li>
<?php endif; ?>
<?php if ($year_built) : ?>
<li class="spec-item">
<span class="spec-label">Year Built</span>
<span class="spec-value"><?php echo esc_html($year_built); ?></span>
</li>
<?php endif; ?>
<?php if ($garage) : ?>
<li class="spec-item">
<span class="spec-label">Garage</span>
<span class="spec-value"><?php echo esc_html($garage); ?> Car</span>
</li>
<?php endif; ?>
</ul>
</section>
<?php endif; ?>
<!-- Description -->
<section class="property-description">
<h2 class="section-title">Description</h2>
<?php if ($short_description) : ?>
<p class="property-short-desc"><?php echo esc_html($short_description); ?></p>
<?php endif; ?>
<div class="property-full-desc">
<?php the_content(); ?>
</div>
</section>
<!-- Features -->
<?php if ($property_features && is_array($property_features)) : ?>
<section class="property-features">
<h2 class="section-title">Features & Amenities</h2>
<ul class="features-list">
<?php
$feature_labels = array(
'central_air' => 'Central Air',
'central_heat' => 'Central Heat',
'fireplace' => 'Fireplace',
'hardwood_floors' => 'Hardwood Floors',
'updated_kitchen' => 'Updated Kitchen',
'updated_bathrooms' => 'Updated Bathrooms',
'basement' => 'Basement',
'finished_basement' => 'Finished Basement',
'deck_patio' => 'Deck/Patio',
'pool' => 'Pool',
'fenced_yard' => 'Fenced Yard',
'sprinkler_system' => 'Sprinkler System',
'smart_home' => 'Smart Home Features',
'solar_panels' => 'Solar Panels',
'new_roof' => 'New Roof',
'new_windows' => 'New Windows',
'waterfront' => 'Waterfront',
'lake_access' => 'Lake Access',
);
foreach ($property_features as $feature) :
$label = isset($feature_labels[$feature]) ? $feature_labels[$feature] : ucwords(str_replace('_', ' ', $feature));
?>
<li class="feature-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
<?php echo esc_html($label); ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
</div>
<!-- Sidebar -->
<aside class="single-property-sidebar">
<?php get_template_part('template-parts/property/property-agent', null, array('agent' => $listing_agent, 'property_id' => $property_id)); ?>
</aside>
</div>
</div>
</main>
<?php
endwhile;
get_footer();
+1
View File
@@ -9,6 +9,7 @@ import './main.scss';
// Import component JS
import '../template-parts/header/navigation.js';
import '../template-parts/property/property-filters.js';
import '../template-parts/property/property-gallery.js';
(function($) {
'use strict';
+2
View File
@@ -16,6 +16,8 @@
@import '../template-parts/content/content-404.scss';
@import '../template-parts/property/property-card.scss';
@import '../template-parts/property/property-filters.scss';
@import '../template-parts/property/property-gallery.scss';
@import '../template-parts/property/single-property.scss';
// ============================================
// CSS Custom Properties (Design Tokens)
@@ -0,0 +1,84 @@
<?php
/**
* Property Agent Card Template Part
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$agent = isset($args['agent']) ? $args['agent'] : null;
$property_id = isset($args['property_id']) ? $args['property_id'] : get_the_ID();
// Get company contact as fallback
$phone = homeproz_get_option('phone');
$email = homeproz_get_option('email');
// If we have an agent user
if ($agent && is_array($agent)) {
$agent_name = $agent['display_name'];
$agent_email = $agent['user_email'];
// Try to get agent phone from user meta
$agent_phone = get_user_meta($agent['ID'], 'phone', true) ?: $phone;
} else {
$agent_name = 'HomeProz Real Estate';
$agent_email = $email;
$agent_phone = $phone;
}
?>
<div class="agent-card card">
<div class="agent-card-header">
<h3 class="agent-card-title">Contact Agent</h3>
</div>
<div class="agent-card-content">
<div class="agent-info">
<div class="agent-avatar">
<?php if ($agent && is_array($agent)) : ?>
<?php echo get_avatar($agent['ID'], 80); ?>
<?php else : ?>
<div class="agent-avatar-placeholder">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="agent-details">
<p class="agent-name"><?php echo esc_html($agent_name); ?></p>
<p class="agent-role">Listing Agent</p>
</div>
</div>
<div class="agent-contact">
<?php if ($agent_phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $agent_phone)); ?>" class="btn btn-primary agent-phone-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<?php echo esc_html($agent_phone); ?>
</a>
<?php endif; ?>
<?php if ($agent_email) : ?>
<a href="mailto:<?php echo esc_attr($agent_email); ?>?subject=<?php echo esc_attr('Inquiry about ' . get_the_title($property_id)); ?>" class="btn btn-secondary agent-email-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
Send Email
</a>
<?php endif; ?>
</div>
</div>
<!-- Quick Inquiry Form (optional - can be expanded) -->
<div class="agent-card-footer">
<p class="agent-card-note">Interested in this property? Contact us today for a showing.</p>
</div>
</div>
@@ -0,0 +1,184 @@
/**
* Property Gallery JavaScript
*
* Lightbox and thumbnail navigation
*
* @package HomeProz
*/
(function($) {
'use strict';
var PropertyGallery = {
// Elements
$gallery: null,
$lightbox: null,
$mainImage: null,
$thumbnails: null,
$lightboxImage: null,
$lightboxCounter: null,
// State
images: [],
currentIndex: 0,
/**
* Initialize
*/
init: function() {
this.$gallery = $('.property-gallery');
this.$lightbox = $('#property-lightbox');
if (!this.$gallery.length) {
return;
}
this.$mainImage = this.$gallery.find('.gallery-main-image img');
this.$thumbnails = this.$gallery.find('.gallery-thumbnail');
this.$lightboxImage = this.$lightbox.find('.lightbox-image');
this.$lightboxCounter = this.$lightbox.find('.lightbox-current');
// Load images data
var $dataScript = $('#gallery-images-data');
if ($dataScript.length) {
this.images = JSON.parse($dataScript.text());
}
if (this.images.length === 0) {
return;
}
this.bindEvents();
},
/**
* Bind events
*/
bindEvents: function() {
var self = this;
// Thumbnail clicks
this.$thumbnails.on('click', function() {
var index = parseInt($(this).data('index'));
self.setMainImage(index);
});
// Open lightbox
this.$gallery.find('[data-lightbox-trigger]').on('click', function() {
self.openLightbox(self.currentIndex);
});
// Close lightbox
this.$lightbox.find('.lightbox-close, .lightbox-overlay').on('click', function() {
self.closeLightbox();
});
// Navigation
this.$lightbox.find('.lightbox-prev').on('click', function() {
self.prevImage();
});
this.$lightbox.find('.lightbox-next').on('click', function() {
self.nextImage();
});
// Keyboard navigation
$(document).on('keydown', function(e) {
if (!self.$lightbox.is('[aria-hidden="false"]')) {
return;
}
switch (e.key) {
case 'Escape':
self.closeLightbox();
break;
case 'ArrowLeft':
self.prevImage();
break;
case 'ArrowRight':
self.nextImage();
break;
}
});
},
/**
* Set main gallery image
*/
setMainImage: function(index) {
if (index < 0 || index >= this.images.length) {
return;
}
this.currentIndex = index;
// Update main image
var image = this.images[index];
this.$mainImage.attr('src', image.url);
this.$mainImage.attr('alt', image.alt || 'Property photo');
// Update active thumbnail
this.$thumbnails.removeClass('is-active');
this.$thumbnails.filter('[data-index="' + index + '"]').addClass('is-active');
},
/**
* Open lightbox
*/
openLightbox: function(index) {
this.currentIndex = index;
this.updateLightboxImage();
this.$lightbox.attr('aria-hidden', 'false');
$('body').addClass('lightbox-open');
},
/**
* Close lightbox
*/
closeLightbox: function() {
this.$lightbox.attr('aria-hidden', 'true');
$('body').removeClass('lightbox-open');
},
/**
* Previous image
*/
prevImage: function() {
var newIndex = this.currentIndex - 1;
if (newIndex < 0) {
newIndex = this.images.length - 1;
}
this.currentIndex = newIndex;
this.updateLightboxImage();
},
/**
* Next image
*/
nextImage: function() {
var newIndex = this.currentIndex + 1;
if (newIndex >= this.images.length) {
newIndex = 0;
}
this.currentIndex = newIndex;
this.updateLightboxImage();
},
/**
* Update lightbox image
*/
updateLightboxImage: function() {
var image = this.images[this.currentIndex];
this.$lightboxImage.attr('src', image.url);
this.$lightboxImage.attr('alt', image.alt || 'Property photo');
this.$lightboxCounter.text(this.currentIndex + 1);
}
};
// Initialize on document ready
$(function() {
PropertyGallery.init();
});
})(jQuery);
@@ -0,0 +1,140 @@
<?php
/**
* Property Gallery Template Part
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$gallery = isset($args['gallery']) ? $args['gallery'] : array();
$property_id = isset($args['property_id']) ? $args['property_id'] : get_the_ID();
// Build images array - combine featured image with gallery
$images = array();
// Add featured image first if available
if (has_post_thumbnail($property_id)) {
$featured_id = get_post_thumbnail_id($property_id);
$images[] = array(
'id' => $featured_id,
'url' => wp_get_attachment_image_url($featured_id, 'large'),
'full' => wp_get_attachment_image_url($featured_id, 'full'),
'alt' => get_post_meta($featured_id, '_wp_attachment_image_alt', true),
);
}
// Add gallery images
if ($gallery && is_array($gallery)) {
foreach ($gallery as $attachment_id) {
// Skip if same as featured image
if (isset($featured_id) && $attachment_id == $featured_id) {
continue;
}
$images[] = array(
'id' => $attachment_id,
'url' => wp_get_attachment_image_url($attachment_id, 'large'),
'full' => wp_get_attachment_image_url($attachment_id, 'full'),
'alt' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
);
}
}
$image_count = count($images);
?>
<div class="property-gallery" data-gallery-count="<?php echo esc_attr($image_count); ?>">
<?php if ($image_count > 0) : ?>
<!-- Main Image -->
<div class="gallery-main">
<button class="gallery-main-image" type="button" data-lightbox-trigger aria-label="Open gallery">
<img src="<?php echo esc_url($images[0]['url']); ?>" alt="<?php echo esc_attr($images[0]['alt'] ?: 'Property photo'); ?>" />
<?php if ($image_count > 1) : ?>
<span class="gallery-count">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
<?php echo esc_html($image_count); ?> Photos
</span>
<?php endif; ?>
</button>
</div>
<?php if ($image_count > 1) : ?>
<!-- Thumbnails -->
<div class="gallery-thumbnails">
<?php foreach (array_slice($images, 0, 5) as $index => $image) : ?>
<button
class="gallery-thumbnail <?php echo $index === 0 ? 'is-active' : ''; ?>"
type="button"
data-index="<?php echo esc_attr($index); ?>"
aria-label="View image <?php echo esc_attr($index + 1); ?>"
>
<img src="<?php echo esc_url(wp_get_attachment_image_url($image['id'], 'thumbnail')); ?>" alt="" loading="lazy" />
<?php if ($index === 4 && $image_count > 5) : ?>
<span class="thumbnail-more">+<?php echo esc_html($image_count - 5); ?></span>
<?php endif; ?>
</button>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Lightbox -->
<div class="gallery-lightbox" id="property-lightbox" aria-hidden="true">
<div class="lightbox-overlay"></div>
<div class="lightbox-container">
<button class="lightbox-close" type="button" aria-label="Close gallery">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous image">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<div class="lightbox-image-container">
<img class="lightbox-image" src="" alt="" />
</div>
<button class="lightbox-nav lightbox-next" type="button" aria-label="Next image">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
<div class="lightbox-counter">
<span class="lightbox-current">1</span> / <span class="lightbox-total"><?php echo esc_html($image_count); ?></span>
</div>
</div>
<!-- Store image data for JS -->
<script type="application/json" id="gallery-images-data">
<?php echo wp_json_encode(array_map(function($img) {
return array(
'url' => $img['full'],
'alt' => $img['alt'],
);
}, $images)); ?>
</script>
</div>
<?php else : ?>
<!-- No Images Placeholder -->
<div class="gallery-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<p>No photos available</p>
</div>
<?php endif; ?>
</div>
@@ -0,0 +1,210 @@
/**
* Property Gallery Styles
*
* @package HomeProz
*/
.property-gallery {
margin-bottom: 2rem;
}
// Main Image
.gallery-main {
position: relative;
margin-bottom: 0.75rem;
}
.gallery-main-image {
display: block;
width: 100%;
padding: 0;
border: none;
background: none;
cursor: pointer;
border-radius: 0.5rem;
overflow: hidden;
img {
width: 100%;
height: auto;
aspect-ratio: 16 / 10;
object-fit: cover;
display: block;
}
}
.gallery-count {
position: absolute;
bottom: 1rem;
right: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: rgba(0, 0, 0, 0.75);
border-radius: 0.25rem;
color: white;
font-size: 0.875rem;
font-weight: 500;
}
// Thumbnails
.gallery-thumbnails {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
@media (max-width: 640px) {
grid-template-columns: repeat(4, 1fr);
}
}
.gallery-thumbnail {
position: relative;
padding: 0;
border: 2px solid transparent;
background: none;
cursor: pointer;
border-radius: 0.25rem;
overflow: hidden;
&.is-active {
border-color: var(--color-accent);
}
img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
display: block;
}
}
.thumbnail-more {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
font-size: 1rem;
font-weight: 600;
}
// Gallery Placeholder
.gallery-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400px;
background-color: var(--color-bg-card);
border-radius: 0.5rem;
color: var(--color-text-muted);
svg {
margin-bottom: 1rem;
}
p {
margin: 0;
}
}
// Lightbox
.gallery-lightbox {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
&[aria-hidden="false"] {
display: block;
}
}
.lightbox-overlay {
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.95);
}
.lightbox-container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
padding: 4rem 1rem;
}
.lightbox-close {
position: absolute;
top: 1rem;
right: 1rem;
z-index: 10;
padding: 0.5rem;
background: none;
border: none;
color: white;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
}
}
.lightbox-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
padding: 1rem;
background-color: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 50%;
color: white;
cursor: pointer;
opacity: 0.8;
&:hover {
opacity: 1;
background-color: rgba(255, 255, 255, 0.2);
}
&.lightbox-prev {
left: 1rem;
}
&.lightbox-next {
right: 1rem;
}
}
.lightbox-image-container {
max-width: 90vw;
max-height: calc(100vh - 8rem);
display: flex;
align-items: center;
justify-content: center;
}
.lightbox-image {
max-width: 100%;
max-height: calc(100vh - 8rem);
object-fit: contain;
}
.lightbox-counter {
position: absolute;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem 1rem;
background-color: rgba(0, 0, 0, 0.75);
border-radius: 0.25rem;
color: white;
font-size: 0.875rem;
}
@@ -0,0 +1,307 @@
/**
* Single Property Styles
*
* @package HomeProz
*/
// Breadcrumbs
.breadcrumbs {
background-color: var(--color-bg-card);
padding: 1rem 0;
border-bottom: 1px solid var(--color-border);
}
.breadcrumb-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
li {
display: flex;
align-items: center;
&:not(:last-child)::after {
content: '/';
margin-left: 0.5rem;
color: var(--color-text-muted);
}
a {
color: var(--color-text-muted);
text-decoration: none;
&:hover {
color: var(--color-accent-light);
}
}
&:last-child {
color: var(--color-text);
}
}
}
// Main Layout
.single-property-main {
padding-bottom: 4rem;
}
.single-property-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
padding-top: 2rem;
@media (min-width: 1024px) {
grid-template-columns: 1fr 350px;
gap: 3rem;
}
}
// Property Header
.property-header {
margin-bottom: 2rem;
}
.property-header-top {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.property-type {
font-size: 0.8125rem;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.property-title {
font-size: 2.5rem;
margin-bottom: 0.75rem;
@media (max-width: 768px) {
font-size: 2rem;
}
}
.property-address {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 1.125rem;
color: var(--color-text-muted);
margin-bottom: 0.5rem;
svg {
flex-shrink: 0;
margin-top: 0.125rem;
color: var(--color-accent);
}
}
.property-mls {
font-size: 0.875rem;
color: var(--color-sold);
margin-bottom: 0;
}
// Section Title
.section-title {
font-size: 1.25rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
// Property Specs Grid
.property-specs-section {
margin-bottom: 2rem;
}
.property-specs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
@media (min-width: 640px) {
grid-template-columns: repeat(3, 1fr);
}
}
.property-specs-grid .spec-item {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem;
background-color: var(--color-bg-card);
border-radius: 0.25rem;
}
.spec-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.spec-value {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
}
// Description
.property-description {
margin-bottom: 2rem;
}
.property-short-desc {
font-size: 1.125rem;
color: var(--color-text);
margin-bottom: 1rem;
}
.property-full-desc {
color: var(--color-text-muted);
line-height: 1.7;
}
// Features List
.property-features {
margin-bottom: 2rem;
}
.features-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 0;
@media (min-width: 640px) {
grid-template-columns: repeat(3, 1fr);
}
}
.feature-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9375rem;
color: var(--color-text-muted);
svg {
flex-shrink: 0;
color: var(--color-success);
}
}
// Sidebar
.single-property-sidebar {
@media (min-width: 1024px) {
position: sticky;
top: 100px;
align-self: start;
}
}
// Agent Card
.agent-card {
padding: 1.5rem;
}
.agent-card-header {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.agent-card-title {
font-size: 1.125rem;
margin-bottom: 0;
}
.agent-info {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.agent-avatar {
flex-shrink: 0;
img {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
}
.agent-avatar-placeholder {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-dark);
border-radius: 50%;
color: var(--color-text-muted);
}
.agent-name {
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.agent-role {
font-size: 0.8125rem;
color: var(--color-text-muted);
margin-bottom: 0;
}
.agent-contact {
display: flex;
flex-direction: column;
gap: 0.75rem;
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
}
}
.agent-card-footer {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
}
.agent-card-note {
font-size: 0.8125rem;
color: var(--color-text-muted);
text-align: center;
margin-bottom: 0;
}
// Body class for lightbox
body.lightbox-open {
overflow: hidden;
}