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:
Hanson.xyz Dev
2025-12-16 13:21:23 -06:00
parent ecdd75c307
commit 0487bd1dcf
13 changed files with 366 additions and 18 deletions
File diff suppressed because one or more lines are too long
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
*
+1
View File
@@ -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 {