Step 2.7: Build single property template with gallery, details, features, and agent card
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
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();
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user