Implement high priority features: filters, property type boxes, email routing
- Expanded search filters: MLS property types dropdown, MLS cities (50+ listings), zip code text input - Property type showcase boxes: 5-category grid on homepage with icons, descriptions, counts - Multi-recipient email: Primary to office@, CC to info@, sender confirmation receipt enabled - Added helper functions: homeproz_get_mls_property_types(), homeproz_get_mls_cities() - Updated FEATURES_PENDING with completion status 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
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
@@ -110,6 +110,9 @@ $featured_commercial = new WP_Query(array(
|
||||
<?php
|
||||
// Service Cards Section (Buy/Rent/Sell)
|
||||
get_template_part('template-parts/components/service-cards');
|
||||
|
||||
// Property Type Showcase Boxes
|
||||
get_template_part('template-parts/components/property-type-boxes');
|
||||
?>
|
||||
|
||||
<!-- Featured Homes Section (MLS Listings) -->
|
||||
|
||||
@@ -28,6 +28,7 @@ function homeproz_ajax_filter_properties() {
|
||||
$property_type = isset($_POST['property_type']) ? sanitize_text_field($_POST['property_type']) : '';
|
||||
$property_status = isset($_POST['property_status']) ? sanitize_text_field($_POST['property_status']) : '';
|
||||
$property_location = isset($_POST['property_location']) ? sanitize_text_field($_POST['property_location']) : '';
|
||||
$zip = isset($_POST['zip']) ? sanitize_text_field($_POST['zip']) : '';
|
||||
$min_price = isset($_POST['min_price']) ? intval($_POST['min_price']) : '';
|
||||
$max_price = isset($_POST['max_price']) ? intval($_POST['max_price']) : '';
|
||||
$beds = isset($_POST['beds']) ? intval($_POST['beds']) : '';
|
||||
@@ -60,6 +61,9 @@ function homeproz_ajax_filter_properties() {
|
||||
if ($property_location) {
|
||||
$filter_args['city'] = $property_location;
|
||||
}
|
||||
if ($zip) {
|
||||
$filter_args['postal_code'] = $zip;
|
||||
}
|
||||
if ($min_price) {
|
||||
$filter_args['min_price'] = $min_price;
|
||||
}
|
||||
@@ -151,6 +155,7 @@ function homeproz_ajax_filter_properties() {
|
||||
$filter_args = array();
|
||||
if ($property_type) $filter_args['property_type'] = $property_type;
|
||||
if ($property_location) $filter_args['property_location'] = $property_location;
|
||||
if ($zip) $filter_args['zip'] = $zip;
|
||||
if ($min_price) $filter_args['min_price'] = $min_price;
|
||||
if ($max_price) $filter_args['max_price'] = $max_price;
|
||||
if ($beds) $filter_args['beds'] = $beds;
|
||||
|
||||
@@ -388,6 +388,71 @@ function homeproz_format_mls_listing_for_json($listing, $is_homeproz = false) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MLS property types for filter dropdowns
|
||||
*
|
||||
* Returns property types with listing counts from MLS database.
|
||||
* Filters to only show types with active listings.
|
||||
*
|
||||
* @return array Array of objects with property_type and count
|
||||
*/
|
||||
function homeproz_get_mls_property_types() {
|
||||
if (!function_exists('mls_plugin')) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$plugin = mls_plugin();
|
||||
$query = $plugin->get_query();
|
||||
|
||||
if (!$query) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $query->get_property_types('Active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MLS cities for filter dropdowns
|
||||
*
|
||||
* Returns distinct cities from MLS database with active listings.
|
||||
* Limited to cities with significant listing counts for usability.
|
||||
*
|
||||
* @param int $min_listings Minimum listings to include city (default 5)
|
||||
* @return array Array of city names
|
||||
*/
|
||||
function homeproz_get_mls_cities($min_listings = 5) {
|
||||
global $wpdb;
|
||||
|
||||
if (!function_exists('mls_plugin')) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$plugin = mls_plugin();
|
||||
$db = $plugin->get_db();
|
||||
|
||||
if (!$db) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$table = $db->properties_table();
|
||||
|
||||
// Get cities with at least $min_listings active properties
|
||||
// Also filter to MN and IA only
|
||||
$cities = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT city FROM {$table}
|
||||
WHERE mlg_can_view = 1
|
||||
AND standard_status = 'Active'
|
||||
AND city IS NOT NULL
|
||||
AND state_or_province IN ('MN', 'IA')
|
||||
GROUP BY city
|
||||
HAVING COUNT(*) >= %d
|
||||
ORDER BY city ASC",
|
||||
$min_listings
|
||||
));
|
||||
|
||||
return $cities ?: array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get property locations that have active or pending properties
|
||||
*
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
@import '../template-parts/components/testimonial.scss';
|
||||
@import '../template-parts/components/feature-block.scss';
|
||||
@import '../template-parts/components/service-cards.scss';
|
||||
@import '../template-parts/components/property-type-boxes.scss';
|
||||
|
||||
// ============================================
|
||||
// CSS Custom Properties (Design Tokens)
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* Property Type Showcase Boxes
|
||||
*
|
||||
* Displays category boxes for browsing properties by type.
|
||||
* Links to the property archive with type filter applied.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get property types with counts from MLS
|
||||
$property_types = homeproz_get_mls_property_types();
|
||||
|
||||
if (empty($property_types)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Define display configuration for each type
|
||||
// Icons use Feather Icons (already in use throughout the site)
|
||||
$type_config = array(
|
||||
'Residential' => array(
|
||||
'icon' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><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>',
|
||||
'description' => 'Single family homes, condos & townhomes',
|
||||
),
|
||||
'Land' => array(
|
||||
'icon' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 22h20"/><path d="M6.36 17.4L4 22h16l-4.24-7.94a.5.5 0 0 0-.88 0L12.5 18.1l-2.5-4.5a.5.5 0 0 0-.88 0z"/><circle cx="13.5" cy="5.5" r="2.5"/></svg>',
|
||||
'description' => 'Vacant lots & acreages',
|
||||
),
|
||||
'Commercial Sale' => array(
|
||||
'icon' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="4" y="2" width="16" height="20" rx="2" ry="2"/><path d="M9 22v-4h6v4"/><path d="M8 6h.01M12 6h.01M16 6h.01M8 10h.01M12 10h.01M16 10h.01M8 14h.01M12 14h.01M16 14h.01"/></svg>',
|
||||
'label' => 'Commercial',
|
||||
'description' => 'Office, retail & industrial',
|
||||
),
|
||||
'Farm' => array(
|
||||
'icon' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 21h18"/><path d="M5 21V7l7-4 7 4v14"/><path d="M9 21v-6h6v6"/><path d="M10 9h4"/></svg>',
|
||||
'description' => 'Agricultural & hobby farms',
|
||||
),
|
||||
'Residential Income' => array(
|
||||
'icon' => '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 12h6"/><path d="M9 16h6"/></svg>',
|
||||
'label' => 'Multi-Family',
|
||||
'description' => 'Duplexes, triplexes & apartments',
|
||||
),
|
||||
);
|
||||
|
||||
// Filter to only show configured types and sort by count
|
||||
$display_types = array();
|
||||
foreach ($property_types as $type) {
|
||||
if (isset($type_config[$type->property_type])) {
|
||||
$display_types[] = $type;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="property-type-boxes">
|
||||
<div class="container">
|
||||
<header class="section-header">
|
||||
<h2 class="section-title">Browse by Property Type</h2>
|
||||
<p class="section-subtitle">Find the perfect property for your needs</p>
|
||||
</header>
|
||||
|
||||
<div class="type-boxes-grid">
|
||||
<?php foreach ($display_types as $type) :
|
||||
$type_name = $type->property_type;
|
||||
$config = $type_config[$type_name];
|
||||
$label = isset($config['label']) ? $config['label'] : $type_name;
|
||||
$count = number_format($type->count);
|
||||
$url = add_query_arg('property_type', urlencode($type_name), home_url('/properties/'));
|
||||
?>
|
||||
<a href="<?php echo esc_url($url); ?>" class="type-box">
|
||||
<div class="type-box-icon">
|
||||
<?php echo $config['icon']; ?>
|
||||
</div>
|
||||
<div class="type-box-content">
|
||||
<h3 class="type-box-title"><?php echo esc_html($label); ?></h3>
|
||||
<p class="type-box-description"><?php echo esc_html($config['description']); ?></p>
|
||||
<span class="type-box-count"><?php echo esc_html($count); ?> listings</span>
|
||||
</div>
|
||||
<div class="type-box-arrow">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Property Type Boxes Styles
|
||||
*
|
||||
* Grid of clickable category boxes for browsing by property type.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
.property-type-boxes {
|
||||
padding: 4rem 0;
|
||||
background-color: var(--color-bg-card);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.type-boxes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 1.5rem;
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.type-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1.5rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.5rem;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-2px);
|
||||
|
||||
.type-box-icon {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.type-box-arrow {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.type-box-icon {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
svg {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.type-box-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.type-box-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.type-box-description {
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 0.75rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.type-box-count {
|
||||
display: inline-block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-accent-light);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.type-box-arrow {
|
||||
align-self: flex-end;
|
||||
color: var(--color-accent);
|
||||
opacity: 0;
|
||||
transform: translateX(-8px);
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 0.75rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -599,6 +599,15 @@
|
||||
}
|
||||
});
|
||||
|
||||
// Also handle text inputs (like zip)
|
||||
this.$form.find('input[type="text"]').each(function() {
|
||||
var name = $(this).attr('name');
|
||||
if (params.has(name)) {
|
||||
$(this).val(params.get(name));
|
||||
hasFilters = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Check for page in hash
|
||||
var page = this.getPageFromHash();
|
||||
|
||||
@@ -664,6 +673,7 @@
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_location: formData.property_location,
|
||||
zip: formData.zip,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
@@ -737,6 +747,7 @@
|
||||
return {
|
||||
property_type: this.$form.find('[name="property_type"]').val() || '',
|
||||
property_location: this.$form.find('[name="property_location"]').val() || '',
|
||||
zip: this.$form.find('[name="zip"]').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() || ''
|
||||
@@ -1239,6 +1250,7 @@
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_location: formData.property_location,
|
||||
zip: formData.zip,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
/**
|
||||
* Property Filters Template Part
|
||||
*
|
||||
* Uses MLS database for property types and cities.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
@@ -12,17 +14,15 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
// Get current filter values from URL
|
||||
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
|
||||
$current_status = isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : '';
|
||||
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '';
|
||||
$current_zip = isset($_GET['zip']) ? sanitize_text_field($_GET['zip']) : '';
|
||||
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
|
||||
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
|
||||
$current_beds = isset($_GET['beds']) ? intval($_GET['beds']) : '';
|
||||
$current_sort = isset($_GET['sort']) ? sanitize_text_field($_GET['sort']) : 'newest';
|
||||
|
||||
// Get taxonomy terms
|
||||
$property_types = get_terms(array('taxonomy' => 'property_type', 'hide_empty' => false));
|
||||
$property_statuses = get_terms(array('taxonomy' => 'property_status', 'hide_empty' => false));
|
||||
$property_locations = homeproz_get_active_locations();
|
||||
// Get MLS property types and cities
|
||||
$property_types = homeproz_get_mls_property_types();
|
||||
$mls_cities = homeproz_get_mls_cities(50); // Cities with 50+ listings
|
||||
?>
|
||||
|
||||
<div class="property-filters" id="property-filters" data-ajax-url="<?php echo esc_url(admin_url('admin-ajax.php')); ?>">
|
||||
@@ -33,25 +33,30 @@ $property_locations = homeproz_get_active_locations();
|
||||
<select name="property_type" id="filter-type" class="filter-select">
|
||||
<option value="">All Types</option>
|
||||
<?php foreach ($property_types as $type) : ?>
|
||||
<option value="<?php echo esc_attr($type->slug); ?>" <?php selected($current_type, $type->slug); ?>>
|
||||
<?php echo esc_html($type->name); ?>
|
||||
<option value="<?php echo esc_attr($type->property_type); ?>" <?php selected($current_type, $type->property_type); ?>>
|
||||
<?php echo esc_html($type->property_type); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="filter-location" class="filter-label">Location</label>
|
||||
<label for="filter-location" class="filter-label">City</label>
|
||||
<select name="property_location" id="filter-location" class="filter-select">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($property_locations as $location) : ?>
|
||||
<option value="<?php echo esc_attr($location->slug); ?>" <?php selected($current_location, $location->slug); ?>>
|
||||
<?php echo esc_html($location->name); ?>
|
||||
<option value="">All Cities</option>
|
||||
<?php foreach ($mls_cities as $city) : ?>
|
||||
<option value="<?php echo esc_attr($city); ?>" <?php selected($current_location, $city); ?>>
|
||||
<?php echo esc_html($city); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="filter-item filter-item-zip">
|
||||
<label for="filter-zip" class="filter-label">Zip Code</label>
|
||||
<input type="text" name="zip" id="filter-zip" class="filter-input" placeholder="e.g. 55401" value="<?php echo esc_attr($current_zip); ?>" maxlength="10" pattern="[0-9\-]*">
|
||||
</div>
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="filter-beds" class="filter-label">Beds</label>
|
||||
<select name="beds" id="filter-beds" class="filter-select">
|
||||
|
||||
@@ -485,6 +485,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--color-text);
|
||||
font-size: 0.9375rem;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
|
||||
// Zip code filter - narrower width
|
||||
.filter-item-zip {
|
||||
max-width: 120px;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Loading State
|
||||
.property-filters.is-loading {
|
||||
|
||||
Reference in New Issue
Block a user