Property filters overhaul: status sorting, simplified UI
- Remove status dropdown (always show all properties) - Remove sort dropdown (use status-based sorting) - Sort order: Active > Pending > Sold, then by modified date - Map view: half height, 2-column property grid - Beds field same width as others - Add CLAUDE.md documentation for property system 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
@@ -15,16 +15,16 @@
|
||||
RewriteRule (.+)\.jpg$ /wp-content/uploads-webpc/$1.jpg.avif [NC,T=image/avif,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.jpeg.avif -f
|
||||
RewriteRule (.+)\.jpeg$ /wp-content/uploads-webpc/$1.jpeg.avif [NC,T=image/avif,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.png.avif -f
|
||||
RewriteRule (.+)\.png$ /wp-content/uploads-webpc/$1.png.avif [NC,T=image/avif,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.webp.avif -f
|
||||
RewriteRule (.+)\.webp$ /wp-content/uploads-webpc/$1.webp.avif [NC,T=image/avif,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/avif
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.jpeg.avif -f
|
||||
RewriteRule (.+)\.jpeg$ /wp-content/uploads-webpc/$1.jpeg.avif [NC,T=image/avif,L]
|
||||
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
@@ -32,15 +32,15 @@
|
||||
RewriteRule (.+)\.jpg$ /wp-content/uploads-webpc/$1.jpg.webp [NC,T=image/webp,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.jpeg.webp -f
|
||||
RewriteRule (.+)\.jpeg$ /wp-content/uploads-webpc/$1.jpeg.webp [NC,T=image/webp,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.png.webp -f
|
||||
RewriteRule (.+)\.png$ /wp-content/uploads-webpc/$1.png.webp [NC,T=image/webp,L]
|
||||
RewriteCond %{HTTP_ACCEPT} image/webp
|
||||
RewriteCond %{REQUEST_FILENAME} -f
|
||||
RewriteCond %{DOCUMENT_ROOT}/wp-content/uploads-webpc/$1.jpeg.webp -f
|
||||
RewriteRule (.+)\.jpeg$ /wp-content/uploads-webpc/$1.jpeg.webp [NC,T=image/webp,L]
|
||||
</IfModule>
|
||||
<IfModule mod_headers.c>
|
||||
<FilesMatch "(?i)\.(jpg|jpeg|png|webp)(\.(webp|avif))?$">
|
||||
<FilesMatch "(?i)\.(jpg|png|webp|jpeg)(\.(webp|avif))?$">
|
||||
Header always set Cache-Control "private"
|
||||
Header always set X-LiteSpeed-Cache-Control "no-cache"
|
||||
Header append Vary "Accept"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Property Archive Template
|
||||
*
|
||||
* Displays the properties listing page
|
||||
* Displays the properties listing page with optional map view
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
@@ -13,9 +13,12 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
get_header();
|
||||
|
||||
// Check if map view is enabled
|
||||
$show_map = isset($_GET['view']) && $_GET['view'] === 'map';
|
||||
?>
|
||||
|
||||
<main id="primary" class="site-main">
|
||||
<main id="primary" class="site-main property-archive-main">
|
||||
<!-- Archive Hero -->
|
||||
<section class="archive-hero">
|
||||
<div class="container">
|
||||
@@ -28,12 +31,166 @@ get_header();
|
||||
<!-- Filters -->
|
||||
<?php get_template_part('template-parts/property/property-filters'); ?>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="property-results">
|
||||
<?php get_template_part('template-parts/property/property-results'); ?>
|
||||
<!-- View Toggle -->
|
||||
<div class="view-toggle">
|
||||
<a href="<?php echo esc_url(remove_query_arg('view')); ?>" class="view-toggle-btn <?php echo !$show_map ? 'active' : ''; ?>">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<rect x="3" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/>
|
||||
</svg>
|
||||
<span>Grid</span>
|
||||
</a>
|
||||
<a href="<?php echo esc_url(add_query_arg('view', 'map')); ?>" class="view-toggle-btn <?php echo $show_map ? 'active' : ''; ?>">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="18"/>
|
||||
<line x1="16" y1="6" x2="16" y2="22"/>
|
||||
</svg>
|
||||
<span>Map</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($show_map) : ?>
|
||||
<!-- Map + List View -->
|
||||
<div class="property-map-layout">
|
||||
<div class="property-map-container">
|
||||
<div id="property-map" class="property-map">
|
||||
<!-- Leaflet map will be initialized here -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="property-list-container">
|
||||
<div id="property-results">
|
||||
<?php get_template_part('template-parts/property/property-results'); ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php else : ?>
|
||||
<!-- Grid View -->
|
||||
<div id="property-results">
|
||||
<?php get_template_part('template-parts/property/property-results'); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php if ($show_map) : ?>
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script>
|
||||
(function($) {
|
||||
// Wait for DOM
|
||||
$(document).ready(function() {
|
||||
// Initialize map centered on Albert Lea area
|
||||
var map = L.map('property-map').setView([43.6480, -93.3685], 10);
|
||||
|
||||
// Add OpenStreetMap tiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// Property data from PHP
|
||||
var properties = [
|
||||
<?php
|
||||
// Get all properties for map markers
|
||||
$map_properties = new WP_Query(array(
|
||||
'post_type' => 'property',
|
||||
'posts_per_page' => -1,
|
||||
'tax_query' => array(
|
||||
array(
|
||||
'taxonomy' => 'property_status',
|
||||
'field' => 'slug',
|
||||
'terms' => array('active', 'pending'),
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
$markers = array();
|
||||
if ($map_properties->have_posts()) :
|
||||
while ($map_properties->have_posts()) :
|
||||
$map_properties->the_post();
|
||||
$city = get_field('city');
|
||||
$price = get_field('property_price');
|
||||
$address = get_field('street_address');
|
||||
|
||||
// Approximate coordinates based on city (can be enhanced with geocoding)
|
||||
$city_coords = array(
|
||||
'Albert Lea' => array(43.6480, -93.3685),
|
||||
'Austin' => array(43.6666, -92.9746),
|
||||
'Glenville' => array(43.5733, -93.2779),
|
||||
'Emmons' => array(43.5013, -93.4896),
|
||||
'Clarks Grove' => array(43.7627, -93.3196),
|
||||
'Alden' => array(43.6719, -93.5768),
|
||||
'Hartland' => array(43.8030, -93.4846),
|
||||
'Geneva' => array(43.8255, -93.2682),
|
||||
'Owatonna' => array(44.0838, -93.2260),
|
||||
'Faribault' => array(44.2949, -93.2688),
|
||||
'Rochester' => array(44.0234, -92.4699),
|
||||
'Mankato' => array(44.1636, -93.9994),
|
||||
);
|
||||
|
||||
// Get coords for city, default to Albert Lea
|
||||
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea'];
|
||||
// Add small random offset to prevent overlapping markers
|
||||
$lat = $coords[0] + (rand(-50, 50) / 10000);
|
||||
$lng = $coords[1] + (rand(-50, 50) / 10000);
|
||||
|
||||
$markers[] = array(
|
||||
'id' => get_the_ID(),
|
||||
'lat' => $lat,
|
||||
'lng' => $lng,
|
||||
'title' => get_the_title(),
|
||||
'price' => '$' . number_format($price),
|
||||
'address' => $address . ', ' . $city,
|
||||
'url' => get_permalink(),
|
||||
);
|
||||
endwhile;
|
||||
wp_reset_postdata();
|
||||
endif;
|
||||
|
||||
// Output JSON
|
||||
foreach ($markers as $i => $marker) {
|
||||
echo json_encode($marker);
|
||||
if ($i < count($markers) - 1) echo ',';
|
||||
}
|
||||
?>
|
||||
];
|
||||
|
||||
// Custom marker icon
|
||||
var propertyIcon = L.divIcon({
|
||||
className: 'property-marker',
|
||||
html: '<div class="marker-pin"></div>',
|
||||
iconSize: [30, 40],
|
||||
iconAnchor: [15, 40],
|
||||
popupAnchor: [0, -40]
|
||||
});
|
||||
|
||||
// Add markers for each property
|
||||
properties.forEach(function(prop) {
|
||||
if (prop.lat && prop.lng) {
|
||||
var marker = L.marker([prop.lat, prop.lng], {icon: propertyIcon}).addTo(map);
|
||||
marker.bindPopup(
|
||||
'<div class="map-popup">' +
|
||||
'<strong>' + prop.price + '</strong><br>' +
|
||||
'<span>' + prop.address + '</span><br>' +
|
||||
'<a href="' + prop.url + '">View Details</a>' +
|
||||
'</div>'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Fit bounds if we have properties
|
||||
if (properties.length > 0) {
|
||||
var bounds = L.latLngBounds(properties.map(function(p) { return [p.lat, p.lng]; }));
|
||||
map.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
|
||||
|
After Width: | Height: | Size: 39 MiB |
|
After Width: | Height: | Size: 2.9 MiB |
|
After Width: | Height: | Size: 9.0 KiB |
@@ -67,6 +67,7 @@ $featured_commercial = new WP_Query(array(
|
||||
'primary_cta_url' => home_url('/properties/'),
|
||||
'secondary_cta_text' => 'Contact Us',
|
||||
'secondary_cta_url' => home_url('/contact/'),
|
||||
'background_image' => get_template_directory_uri() . '/assets/images/hero.webp',
|
||||
'size' => 'large',
|
||||
));
|
||||
|
||||
@@ -140,122 +141,6 @@ $featured_commercial = new WP_Query(array(
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Why Choose Us / Features Section -->
|
||||
<section class="features-section">
|
||||
<div class="container">
|
||||
<header class="features-section-header">
|
||||
<h2 class="features-section-title">Why HomeProz?</h2>
|
||||
<p class="features-section-subtitle">We're committed to helping you achieve your real estate goals</p>
|
||||
</header>
|
||||
|
||||
<div class="features-grid">
|
||||
<?php
|
||||
get_template_part('template-parts/components/feature-block', null, array(
|
||||
'icon' => 'local-expertise',
|
||||
'title' => 'Local Expertise',
|
||||
'text' => 'Deep knowledge of the Albert Lea area and surrounding Minnesota communities helps us find the perfect property for you.',
|
||||
));
|
||||
|
||||
get_template_part('template-parts/components/feature-block', null, array(
|
||||
'icon' => 'personalized',
|
||||
'title' => 'Personalized Service',
|
||||
'text' => 'We take the time to understand your unique needs and provide tailored guidance throughout your real estate journey.',
|
||||
));
|
||||
|
||||
get_template_part('template-parts/components/feature-block', null, array(
|
||||
'icon' => 'results',
|
||||
'title' => 'Results-Driven',
|
||||
'text' => 'Our track record speaks for itself. We work tirelessly to get you the best possible outcome, whether buying or selling.',
|
||||
));
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Team Preview Section -->
|
||||
<section class="team-section">
|
||||
<div class="container">
|
||||
<header class="team-section-header">
|
||||
<h2 class="team-section-title">Meet Our Agents</h2>
|
||||
<p class="team-section-subtitle">A dedicated team of real estate professionals ready to serve you</p>
|
||||
</header>
|
||||
|
||||
<div class="team-grid">
|
||||
<?php
|
||||
// Team member data - can be replaced with ACF repeater or CPT later
|
||||
$team_members = array(
|
||||
array(
|
||||
'name' => 'Heidi Johnson',
|
||||
'title' => 'REALTOR',
|
||||
'phone' => '507-402-2310',
|
||||
'email' => 'heidi@homeprozrealestate.com',
|
||||
),
|
||||
array(
|
||||
'name' => 'Kyra Johnson',
|
||||
'title' => 'REALTOR',
|
||||
'phone' => '507-516-4870',
|
||||
'email' => 'kyra@homeprozrealestate.com',
|
||||
),
|
||||
array(
|
||||
'name' => 'John Hill',
|
||||
'title' => 'REALTOR',
|
||||
'phone' => '507-383-1738',
|
||||
'email' => 'john@homeprozrealestate.com',
|
||||
),
|
||||
array(
|
||||
'name' => 'Mindy Moreno',
|
||||
'title' => 'REALTOR',
|
||||
'phone' => '507-438-5900',
|
||||
'email' => 'mindy@homeprozrealestate.com',
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($team_members as $member) :
|
||||
get_template_part('template-parts/components/agent-card', null, $member);
|
||||
endforeach;
|
||||
?>
|
||||
</div>
|
||||
|
||||
<div class="section-footer">
|
||||
<a href="<?php echo esc_url(home_url('/about/')); ?>" class="btn btn-secondary">
|
||||
Learn More About Us
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials Section -->
|
||||
<section class="testimonials-section">
|
||||
<div class="container">
|
||||
<header class="testimonials-section-header">
|
||||
<h2 class="testimonials-section-title">What Our Clients Say</h2>
|
||||
<p class="testimonials-section-subtitle">Real stories from satisfied buyers and sellers</p>
|
||||
</header>
|
||||
|
||||
<div class="testimonials-grid">
|
||||
<?php
|
||||
// Testimonial data - can be replaced with ACF repeater or CPT later
|
||||
$testimonials = array(
|
||||
array(
|
||||
'quote' => 'HomeProz made our home buying experience incredibly smooth. They were patient, knowledgeable, and always available to answer our questions. We found our dream home faster than we ever expected!',
|
||||
'name' => 'Sarah & Mike T.',
|
||||
'location' => 'Home Buyers, Albert Lea',
|
||||
),
|
||||
array(
|
||||
'quote' => 'Selling our home was stress-free thanks to the HomeProz team. They handled everything professionally and got us a great price. Highly recommend!',
|
||||
'name' => 'Robert K.',
|
||||
'location' => 'Home Seller, Austin',
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($testimonials as $testimonial) :
|
||||
get_template_part('template-parts/components/testimonial', null, $testimonial);
|
||||
endforeach;
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php
|
||||
// Contact CTA Section
|
||||
get_template_part('template-parts/components/cta-section', null, array(
|
||||
|
||||
@@ -107,30 +107,22 @@ function homeproz_ajax_filter_properties() {
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting
|
||||
switch ($sort) {
|
||||
case 'oldest':
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_low':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_high':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
case 'newest':
|
||||
default:
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
}
|
||||
// Fetch all matching properties for status-based sorting
|
||||
$args['posts_per_page'] = -1;
|
||||
$args['orderby'] = 'modified';
|
||||
$args['order'] = 'DESC';
|
||||
|
||||
$properties = new WP_Query($args);
|
||||
$all_properties = get_posts($args);
|
||||
|
||||
// Sort by status (Active > Pending > Sold) then by modified date
|
||||
$sorted_properties = homeproz_sort_properties_by_status($all_properties);
|
||||
|
||||
// Handle pagination manually
|
||||
$per_page = 12;
|
||||
$total = count($sorted_properties);
|
||||
$max_pages = ceil($total / $per_page);
|
||||
$offset = ($paged - 1) * $per_page;
|
||||
$paged_properties = array_slice($sorted_properties, $offset, $per_page);
|
||||
|
||||
ob_start();
|
||||
|
||||
@@ -138,45 +130,45 @@ function homeproz_ajax_filter_properties() {
|
||||
?>
|
||||
<div class="properties-meta">
|
||||
<p class="properties-count">
|
||||
<?php if ($properties->found_posts > 0) : ?>
|
||||
Showing <strong><?php echo esc_html($properties->found_posts); ?></strong>
|
||||
<?php echo $properties->found_posts === 1 ? 'property' : 'properties'; ?>
|
||||
<?php if ($total > 0) : ?>
|
||||
Showing <strong><?php echo esc_html($total); ?></strong>
|
||||
<?php echo $total === 1 ? 'property' : 'properties'; ?>
|
||||
<?php else : ?>
|
||||
No properties found
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($properties->have_posts()) : ?>
|
||||
<?php if (!empty($paged_properties)) : ?>
|
||||
<div class="properties-grid">
|
||||
<?php
|
||||
while ($properties->have_posts()) :
|
||||
$properties->the_post();
|
||||
global $post;
|
||||
foreach ($paged_properties as $property_post) :
|
||||
$post = $property_post;
|
||||
setup_postdata($post);
|
||||
get_template_part('template-parts/property/property-card');
|
||||
endwhile;
|
||||
endforeach;
|
||||
wp_reset_postdata();
|
||||
?>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Build pagination
|
||||
$big = 999999999;
|
||||
$base_url = get_post_type_archive_link('property');
|
||||
|
||||
// Build URL with current filters
|
||||
$filter_args = array();
|
||||
if ($property_type) $filter_args['property_type'] = $property_type;
|
||||
if ($property_status) $filter_args['property_status'] = $property_status;
|
||||
if ($property_location) $filter_args['property_location'] = $property_location;
|
||||
if ($min_price) $filter_args['min_price'] = $min_price;
|
||||
if ($max_price) $filter_args['max_price'] = $max_price;
|
||||
if ($beds) $filter_args['beds'] = $beds;
|
||||
if ($sort && $sort !== 'newest') $filter_args['sort'] = $sort;
|
||||
|
||||
$pagination = paginate_links(array(
|
||||
'base' => add_query_arg('paged', '%#%', $base_url),
|
||||
'format' => '',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $properties->max_num_pages,
|
||||
'total' => $max_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
@@ -203,14 +195,12 @@ function homeproz_ajax_filter_properties() {
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
wp_reset_postdata();
|
||||
|
||||
$html = ob_get_clean();
|
||||
|
||||
wp_send_json_success(array(
|
||||
'html' => $html,
|
||||
'found_posts' => $properties->found_posts,
|
||||
'max_pages' => $properties->max_num_pages,
|
||||
'found_posts' => $total,
|
||||
'max_pages' => $max_pages,
|
||||
));
|
||||
}
|
||||
add_action('wp_ajax_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
|
||||
|
||||
@@ -158,6 +158,54 @@ function homeproz_footer_fallback_menu() {
|
||||
echo '</ul>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get status sort order value
|
||||
* Active = 1 (first), Pending = 2, Sold = 3 (last)
|
||||
*
|
||||
* @param int $post_id Property post ID
|
||||
* @return int Sort order value
|
||||
*/
|
||||
function homeproz_get_status_sort_order($post_id) {
|
||||
$status_terms = get_the_terms($post_id, 'property_status');
|
||||
if (!$status_terms || is_wp_error($status_terms)) {
|
||||
return 99; // Unknown status goes last
|
||||
}
|
||||
|
||||
$status = strtolower($status_terms[0]->slug);
|
||||
switch ($status) {
|
||||
case 'active':
|
||||
return 1;
|
||||
case 'pending':
|
||||
return 2;
|
||||
case 'sold':
|
||||
return 3;
|
||||
default:
|
||||
return 99;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort properties array by status order then modified date
|
||||
*
|
||||
* @param array $posts Array of WP_Post objects
|
||||
* @return array Sorted array
|
||||
*/
|
||||
function homeproz_sort_properties_by_status($posts) {
|
||||
usort($posts, function($a, $b) {
|
||||
$order_a = homeproz_get_status_sort_order($a->ID);
|
||||
$order_b = homeproz_get_status_sort_order($b->ID);
|
||||
|
||||
if ($order_a !== $order_b) {
|
||||
return $order_a - $order_b;
|
||||
}
|
||||
|
||||
// Same status, sort by modified date (newest first)
|
||||
return strtotime($b->post_modified) - strtotime($a->post_modified);
|
||||
});
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get theme option from ACF or defaults
|
||||
*
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
.Agents_Archive {
|
||||
.agents-section {
|
||||
padding: 3rem 0 4rem;
|
||||
padding: 1.125rem 0 5.875rem;
|
||||
}
|
||||
|
||||
// Agents Grid
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Property Archive System
|
||||
|
||||
Property listing and filtering system for the real estate website.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `property-filters.php` | Filter form UI (Type, Location, Beds, Price Range) |
|
||||
| `property-filters.js` | AJAX filtering without page reload |
|
||||
| `property-filters.scss` | Filter bar and map view styles |
|
||||
| `property-results.php` | Property grid with pagination |
|
||||
| `property-card.php` | Individual property card component |
|
||||
| `property-agent.php` | Agent sidebar widget on single property |
|
||||
|
||||
## Sorting Logic
|
||||
|
||||
Properties are sorted by:
|
||||
1. **Status**: Active first, Pending second, Sold third
|
||||
2. **Modified Date**: Most recently modified first within each status group
|
||||
|
||||
This sorting is handled in PHP via `homeproz_sort_properties_by_status()` in `inc/template-functions.php`.
|
||||
|
||||
## Filter Fields
|
||||
|
||||
- **Type**: Taxonomy `property_type` (House, Land, Commercial, etc.)
|
||||
- **Location**: Taxonomy `property_location` (City names)
|
||||
- **Beds**: ACF field `bedrooms` (minimum bedrooms filter)
|
||||
- **Price Range**: ACF field `property_price` (min/max numeric filter)
|
||||
|
||||
Status filter was removed - all properties (Active, Pending, Sold) are always shown.
|
||||
|
||||
## AJAX Handler
|
||||
|
||||
`homeproz_ajax_filter_properties()` in `inc/ajax-handlers.php`
|
||||
- Receives filter params via POST
|
||||
- Returns HTML for `#property-results` container
|
||||
- Uses same sorting logic as initial page load
|
||||
|
||||
## Map View
|
||||
|
||||
Toggle between Grid and Map views. Map view:
|
||||
- Uses Leaflet.js with OpenStreetMap tiles
|
||||
- City-based coordinates (no geocoding API)
|
||||
- Properties displayed in half-height map + 2-column grid
|
||||
@@ -129,12 +129,10 @@
|
||||
action: 'homeproz_filter_properties',
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_status: formData.property_status,
|
||||
property_location: formData.property_location,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
sort: formData.sort,
|
||||
paged: page
|
||||
},
|
||||
success: function(response) {
|
||||
@@ -171,12 +169,10 @@
|
||||
getFormData: function() {
|
||||
return {
|
||||
property_type: this.$form.find('[name="property_type"]').val() || '',
|
||||
property_status: this.$form.find('[name="property_status"]').val() || '',
|
||||
property_location: this.$form.find('[name="property_location"]').val() || '',
|
||||
min_price: this.$form.find('[name="min_price"]').val() || '',
|
||||
max_price: this.$form.find('[name="max_price"]').val() || '',
|
||||
beds: this.$form.find('[name="beds"]').val() || '',
|
||||
sort: this.$form.find('[name="sort"]').val() || 'newest'
|
||||
beds: this.$form.find('[name="beds"]').val() || ''
|
||||
};
|
||||
},
|
||||
|
||||
@@ -204,7 +200,7 @@
|
||||
|
||||
// Add non-empty filters to URL
|
||||
for (var key in formData) {
|
||||
if (formData[key] && formData[key] !== 'newest') {
|
||||
if (formData[key]) {
|
||||
url.searchParams.set(key, formData[key]);
|
||||
}
|
||||
}
|
||||
@@ -231,7 +227,6 @@
|
||||
*/
|
||||
resetFilters: function() {
|
||||
this.$form.find('select').val('');
|
||||
this.$form.find('[name="sort"]').val('newest');
|
||||
this.filterProperties(1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,19 +41,6 @@ $property_locations = get_terms(array('taxonomy' => 'property_location', 'hide_e
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Property Status -->
|
||||
<div class="filter-group filter-group-status">
|
||||
<label for="filter-status" class="filter-label">Status</label>
|
||||
<select name="property_status" id="filter-status" class="filter-select">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($property_statuses as $status) : ?>
|
||||
<option value="<?php echo esc_attr($status->slug); ?>" <?php selected($current_status, $status->slug); ?>>
|
||||
<?php echo esc_html($status->name); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="filter-group filter-group-location">
|
||||
<label for="filter-location" class="filter-label">Location</label>
|
||||
@@ -111,17 +98,6 @@ $property_locations = get_terms(array('taxonomy' => 'property_location', 'hide_e
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sort -->
|
||||
<div class="filter-group filter-group-sort">
|
||||
<label for="filter-sort" class="filter-label">Sort</label>
|
||||
<select name="sort" id="filter-sort" class="filter-select">
|
||||
<option value="newest" <?php selected($current_sort, 'newest'); ?>>Newest</option>
|
||||
<option value="oldest" <?php selected($current_sort, 'oldest'); ?>>Oldest</option>
|
||||
<option value="price_low" <?php selected($current_sort, 'price_low'); ?>>Price: Low</option>
|
||||
<option value="price_high" <?php selected($current_sort, 'price_high'); ?>>Price: High</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="filter-group filter-group-actions">
|
||||
<label class="filter-label"> </label>
|
||||
|
||||
@@ -78,13 +78,13 @@
|
||||
@media (min-width: 1024px) {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
height: calc(100vh - 150px);
|
||||
height: calc(50vh - 75px);
|
||||
}
|
||||
}
|
||||
|
||||
.property-map {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
height: 200px;
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
@@ -102,14 +102,6 @@
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,12 +190,12 @@
|
||||
gap: 0.75rem;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
// Type, Status, Location, Beds(narrow), Price(wide), Sort, Actions
|
||||
grid-template-columns: 1fr 1fr 1fr 70px minmax(200px, 1.5fr) 100px auto;
|
||||
@media (min-width: 1024px) {
|
||||
// Type, Location, Beds, Price(wide), Actions
|
||||
grid-template-columns: 1fr 1fr 1fr minmax(220px, 1.5fr) auto;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -215,18 +207,6 @@
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.filter-group-beds {
|
||||
@media (min-width: 1200px) {
|
||||
max-width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group-sort {
|
||||
@media (min-width: 1200px) {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-group-actions {
|
||||
@media (min-width: 1200px) {
|
||||
min-width: 160px;
|
||||
|
||||
@@ -100,30 +100,44 @@ if (!empty($meta_query)) {
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting
|
||||
switch ($current_sort) {
|
||||
case 'oldest':
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_low':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'ASC';
|
||||
break;
|
||||
case 'price_high':
|
||||
$args['meta_key'] = 'property_price';
|
||||
$args['orderby'] = 'meta_value_num';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
case 'newest':
|
||||
default:
|
||||
$args['orderby'] = 'date';
|
||||
$args['order'] = 'DESC';
|
||||
break;
|
||||
}
|
||||
// For status-based sorting, we need to fetch all matching properties and sort in PHP
|
||||
// This is efficient for real estate sites with < 1000 properties
|
||||
$args['posts_per_page'] = -1;
|
||||
$args['orderby'] = 'modified';
|
||||
$args['order'] = 'DESC';
|
||||
|
||||
$properties = new WP_Query($args);
|
||||
$all_properties = get_posts($args);
|
||||
|
||||
// Sort by status (Active > Pending > Sold) then by modified date
|
||||
$sorted_properties = homeproz_sort_properties_by_status($all_properties);
|
||||
|
||||
// Handle pagination manually
|
||||
$per_page = 12;
|
||||
$total = count($sorted_properties);
|
||||
$max_pages = ceil($total / $per_page);
|
||||
$offset = ($paged - 1) * $per_page;
|
||||
$paged_properties = array_slice($sorted_properties, $offset, $per_page);
|
||||
|
||||
// Create a fake WP_Query-like object for compatibility
|
||||
$properties = (object) array(
|
||||
'posts' => $paged_properties,
|
||||
'found_posts' => $total,
|
||||
'max_num_pages' => $max_pages,
|
||||
);
|
||||
|
||||
// Helper function to check if we have posts
|
||||
$properties->have_posts = function() use (&$paged_properties) {
|
||||
static $index = 0;
|
||||
if ($index < count($paged_properties)) {
|
||||
return true;
|
||||
}
|
||||
$index = 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
// Loop through properties manually
|
||||
global $post;
|
||||
$property_index = 0;
|
||||
?>
|
||||
|
||||
<!-- Results Meta -->
|
||||
@@ -138,13 +152,15 @@ $properties = new WP_Query($args);
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($properties->have_posts()) : ?>
|
||||
<?php if (!empty($paged_properties)) : ?>
|
||||
<div class="properties-grid">
|
||||
<?php
|
||||
while ($properties->have_posts()) :
|
||||
$properties->the_post();
|
||||
foreach ($paged_properties as $property_post) :
|
||||
$post = $property_post;
|
||||
setup_postdata($post);
|
||||
get_template_part('template-parts/property/property-card');
|
||||
endwhile;
|
||||
endforeach;
|
||||
wp_reset_postdata();
|
||||
?>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +171,7 @@ $properties = new WP_Query($args);
|
||||
'base' => str_replace($big, '%#%', esc_url(get_pagenum_link($big))),
|
||||
'format' => '?paged=%#%',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $properties->max_num_pages,
|
||||
'total' => $max_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 5.9 KiB |