Implement launch blockers and MLS state filter

- Add MLS state filter for MN/IA only queries
- Add property inquiry form auto-population with read-only display
- Update broker info and office hours in footer
- Remove Bridge Realty text from about page
- Update service area to Minnesota and Iowa
- Add HomeProz listing identification (is_homeproz column)
- Add dynamic featured listings on front page
- Add gallery thumbnail preloading and loading spinners
- Update 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:07:12 -06:00
parent 15449b9131
commit 07a8d1756e
21 changed files with 680 additions and 91 deletions
+6
View File
@@ -1,3 +1,5 @@
{"id":"html-117","title":"Filter MLS queries to MN and IA only","description":"","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-16T11:53:08.470255883-06:00","updated_at":"2025-12-16T12:45:04.808441903-06:00","closed_at":"2025-12-16T12:45:04.808441903-06:00","close_reason":"State filter implemented for all MLS query methods (get_properties, get_distinct_cities, get_distinct_counties, get_count, has_data, get_property_types, get_price_range). Added MLS_ALLOWED_STATES constant."}
{"id":"html-2fg","title":"Update office hours in footer","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-16T11:53:03.244743372-06:00","updated_at":"2025-12-16T11:54:52.108263115-06:00","closed_at":"2025-12-16T11:54:52.108263115-06:00","close_reason":"Updated office hours to Mon-Fri 9am-4pm, Sat/Sun By Appointment"}
{"id":"html-2fp","title":"Separate Residential and Commercial listings on homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:09.984594683-06:00","updated_at":"2025-11-30T02:33:12.32537052-06:00","closed_at":"2025-11-30T02:33:12.32537052-06:00","close_reason":"Separated Featured Homes and Commercial/Land into distinct homepage sections"}
{"id":"html-3fb","title":"MLS by HansonXyz Plugin - Phase 5: Public API","description":"Query class with filter support, global helper functions (mls_get_properties, etc), integration hooks for themes","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:45.05131653-06:00","updated_at":"2025-12-14T21:21:46.940975819-06:00","closed_at":"2025-12-14T21:21:46.940975819-06:00","dependencies":[{"issue_id":"html-3fb","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:05.308661828-06:00","created_by":"unknown"}]}
{"id":"html-3nq","title":"Enhance footer with office hours, professional logos, license numbers","description":"","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-30T02:24:30.889106857-06:00","updated_at":"2025-11-30T02:46:52.35661921-06:00","closed_at":"2025-11-30T02:46:52.35661921-06:00","close_reason":"Enhanced footer with office hours, professional logos (REALTOR, Equal Housing), and license number"}
@@ -5,6 +7,7 @@
{"id":"html-4za","title":"MLS by HansonXyz Plugin - Phase 2: API Client","description":"API Client class with auth, gzip, error handling. Rate Limiter class. CLI test commands (wp mls test connection/auth)","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:29.352944416-06:00","updated_at":"2025-12-14T21:21:36.854872754-06:00","closed_at":"2025-12-14T21:21:36.854872754-06:00","dependencies":[{"issue_id":"html-4za","depends_on_id":"html-ha4","type":"blocks","created_at":"2025-12-14T21:03:50.090841814-06:00","created_by":"unknown"}]}
{"id":"html-5bw","title":"Add service cards section (Buy/Rent/Sell) to homepage","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:24:04.779591681-06:00","updated_at":"2025-11-30T02:32:12.064316318-06:00","closed_at":"2025-11-30T02:32:12.064316318-06:00","close_reason":"Added service cards section with Buy/Rent/Sell options"}
{"id":"html-5j7","title":"MLS by HansonXyz Plugin - Phase 3: Sync Engine","description":"Sync Engine class, full sync with pagination, incremental sync with ModificationTimestamp, sync state tracking for resume, CLI sync commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:34.581442915-06:00","updated_at":"2025-12-14T21:21:46.933578124-06:00","closed_at":"2025-12-14T21:21:46.933578124-06:00","dependencies":[{"issue_id":"html-5j7","depends_on_id":"html-4za","type":"blocks","created_at":"2025-12-14T21:03:55.163778736-06:00","created_by":"unknown"}]}
{"id":"html-76m","title":"Remove Bridge Realty text from site","description":"","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-16T11:52:42.455604014-06:00","updated_at":"2025-12-16T11:53:51.821637756-06:00","closed_at":"2025-12-16T11:53:51.821637756-06:00","close_reason":"Removed Bridge Realty text from page-about.php, replaced with correct LandProz DBA info"}
{"id":"html-7jz","title":"Add Communities section to navigation and create community pages structure","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:15.204568226-06:00","updated_at":"2025-11-30T02:43:22.075934867-06:00","closed_at":"2025-11-30T02:43:22.075934867-06:00","close_reason":"Created Communities landing page, community page template, 3 community pages, and added to navigation"}
{"id":"html-98b","title":"Add location search dropdown to homepage hero","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T02:23:59.555310037-06:00","updated_at":"2025-11-30T02:30:59.92891882-06:00","closed_at":"2025-11-30T02:30:59.92891882-06:00","close_reason":"Added location search dropdown to hero section with community taxonomy"}
{"id":"html-bfd","title":"Update DESIGN-DOCUMENT.md and IMPLEMENTATION-PLAN.md with RHR structural changes","description":"","status":"closed","priority":0,"issue_type":"task","created_at":"2025-11-30T02:24:40.504170573-06:00","updated_at":"2025-11-30T02:28:50.551587345-06:00","closed_at":"2025-11-30T02:28:50.551587345-06:00","close_reason":"Updated DESIGN-DOCUMENT.md and IMPLEMENTATION-PLAN.md with RHR structural changes"}
@@ -13,6 +16,9 @@
{"id":"html-ha4","title":"MLS by HansonXyz Plugin - Phase 1: Foundation","description":"Create plugin structure, main file, activator/deactivator, database schema with dbDelta, options handling, logger class","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:24.104655824-06:00","updated_at":"2025-12-14T21:21:36.848348568-06:00","closed_at":"2025-12-14T21:21:36.848348568-06:00"}
{"id":"html-k37","title":"MLS by HansonXyz Plugin - Phase 8: Documentation \u0026 Testing","description":"CLAUDE.md, API.md, USAGE.md documentation. Full CLI test sequence. Final verification.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:03:00.728802408-06:00","updated_at":"2025-12-14T21:22:16.755253009-06:00","closed_at":"2025-12-14T21:22:16.755253009-06:00","dependencies":[{"issue_id":"html-k37","depends_on_id":"html-x03","type":"blocks","created_at":"2025-12-14T21:04:20.543019628-06:00","created_by":"unknown"}]}
{"id":"html-lci","title":"Scrape homeprozrealestate.com property listings and import to WordPress","description":"","status":"closed","priority":1,"issue_type":"task","created_at":"2025-11-30T17:37:41.948645374-06:00","updated_at":"2025-11-30T18:06:33.347607321-06:00","closed_at":"2025-11-30T18:06:33.347607321-06:00","close_reason":"Imported 5 properties with images, ACF fields, and external listing URLs"}
{"id":"html-m69","title":"Update service area to MN and IA","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-16T11:52:58.02566345-06:00","updated_at":"2025-12-16T11:54:57.284399486-06:00","closed_at":"2025-12-16T11:54:57.284399486-06:00","close_reason":"Updated footer tagline to include 'Minnesota and Iowa'"}
{"id":"html-p2h","title":"Property inquiry form auto-population","description":"","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-16T11:52:47.656766182-06:00","updated_at":"2025-12-16T12:47:46.436550996-06:00","closed_at":"2025-12-16T12:47:46.436550996-06:00","close_reason":"Implemented property inquiry auto-population: CF7 hidden field, read-only display box, contact link passes property and URL params, office@homeprozrealestate.com recipient"}
{"id":"html-sbh","title":"MLS by HansonXyz Plugin - Phase 4: Media Handler","description":"Media Handler class, download and organize media files, PhotosChangeTimestamp detection, CLI media commands","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:39.793324508-06:00","updated_at":"2025-12-14T21:21:46.94012466-06:00","closed_at":"2025-12-14T21:21:46.94012466-06:00","dependencies":[{"issue_id":"html-sbh","depends_on_id":"html-5j7","type":"blocks","created_at":"2025-12-14T21:04:00.24472923-06:00","created_by":"unknown"}]}
{"id":"html-t8u","title":"Add Resources section to navigation and create resource pages","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T02:24:25.662824938-06:00","updated_at":"2025-11-30T02:45:31.13972652-06:00","closed_at":"2025-11-30T02:45:31.13972652-06:00","close_reason":"Created Resources landing page, resource page template, Buyer's Guide, Seller's Guide, and added to navigation"}
{"id":"html-x03","title":"MLS by HansonXyz Plugin - Phase 7: Cron \u0026 Automation","description":"WP Cron scheduling (configurable interval), standalone cron script for Unix cron","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-14T21:02:55.483012095-06:00","updated_at":"2025-12-14T21:22:06.519310981-06:00","closed_at":"2025-12-14T21:22:06.519310981-06:00","dependencies":[{"issue_id":"html-x03","depends_on_id":"html-4q8","type":"blocks","created_at":"2025-12-14T21:04:15.476790917-06:00","created_by":"unknown"}]}
{"id":"html-yxd","title":"Update broker footer legal text","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-16T11:52:52.82531755-06:00","updated_at":"2025-12-16T11:54:46.914199743-06:00","closed_at":"2025-12-16T11:54:46.914199743-06:00","close_reason":"Updated broker footer to 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC'"}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"database": "beads.db",
"jsonl_export": "issues.jsonl",
"last_bd_version": "0.27.0"
"last_bd_version": "0.27.2"
}
+22 -8
View File
@@ -471,6 +471,8 @@ Sunday By Appointment
**Analysis:** Simple theme options update.
**Status:** COMPLETED 2025-12-16 - Updated site-footer.php: Mon-Fri 9:00am - 4:00pm, Sat/Sun By Appointment
---
## Category 5: Footer & Legal
@@ -734,17 +736,17 @@ Also: "Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
## Priority Summary
### Must-Do (Critical)
1. Broker footer legal text update (5.1)
2. Phone number update (5.2)
3. Property inquiry form auto-population (4.1)
4. Featured photo from MLS (1.2)
5. Logo consistency (2.4)
6. Agent state licenses (3.8)
7. Bridge Realty text removal (9.1)
1. Broker footer legal text update (5.1) - COMPLETED 2025-12-16: Updated site-footer.php with correct broker info
2. Phone number update (5.2) - AWAITING: Client needs to confirm final phone number
3. Property inquiry form auto-population (4.1) - COMPLETED 2025-12-16: CF7 hidden field, read-only display box, sends to office@homeprozrealestate.com
4. Featured photo from MLS (1.2) - ALREADY WORKING: media_order=1 used as primary
5. Logo consistency (2.4) - AWAITING: Client needs to provide condensed HP logo SVG
6. Agent state licenses (3.8) - AWAITING: Client needs to provide license numbers
7. Bridge Realty text removal (9.1) - COMPLETED 2025-12-16: Removed from page-about.php, updated with correct LandProz DBA info
### High Priority (Implement)
1. Co-listing agent support (1.1)
2. Service area MN + IA (5.3)
2. Service area MN + IA (5.3) - COMPLETED 2025-12-16: Updated footer tagline to "Minnesota and Iowa"
3. Expanded search filters (1.5)
4. Property type showcase boxes (1.6)
5. Multi-recipient email routing (4.2)
@@ -821,3 +823,15 @@ Also: "Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
- Sold properties strategy
- Full agent tier system
- Buyers/sellers guide revamp
---
## Technical Notes (2025-12-16)
### MLS State Filter Implementation
All MLS queries now restricted to Minnesota (MN) and Iowa (IA) only:
- Added `MLS_ALLOWED_STATES` constant in `mls-by-hansonxyz.php`
- Added `get_state_filter()` private method in `class-mls-query.php`
- Applied to: `get_properties()`, `get_distinct_cities()`, `get_distinct_counties()`, `get_count()`, `has_data()`, `get_property_types()`, `get_price_range()`
This ensures all property searches, filters, and statistics only include MN/IA listings.
@@ -11,9 +11,9 @@ class MLS_DB {
/**
* Schema version for index migrations
* Increment this when adding new indexes
* Increment this when adding new indexes or columns
*/
const SCHEMA_VERSION = 2;
const SCHEMA_VERSION = 3;
/**
* Get table name with prefix
@@ -126,6 +126,7 @@ class MLS_DB {
list_office_key VARCHAR(50) DEFAULT NULL,
list_office_mls_id VARCHAR(50) DEFAULT NULL,
list_office_name VARCHAR(150) DEFAULT NULL,
is_homeproz TINYINT(1) NOT NULL DEFAULT 0,
photos_count INT(5) DEFAULT 0,
modification_timestamp DATETIME NOT NULL,
@@ -332,8 +333,41 @@ class MLS_DB {
update_option('mls_schema_version', 2);
}
// Migration to schema version 3: Add is_homeproz column and index
if ($current_schema < 3) {
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
// Check if column exists
$column_exists = $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'is_homeproz'",
DB_NAME,
$table_properties
));
if (!$column_exists) {
$wpdb->query("ALTER TABLE {$table_properties} ADD COLUMN is_homeproz TINYINT(1) NOT NULL DEFAULT 0 AFTER list_office_name");
}
// Add index if not exists
$existing_indexes = self::get_existing_indexes($table_properties);
if (!isset($existing_indexes['idx_is_homeproz'])) {
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_is_homeproz (is_homeproz)");
}
// Update existing HomeProz listings
if (defined('MLS_HOMEPROZ_OFFICE_ID')) {
$wpdb->query($wpdb->prepare(
"UPDATE {$table_properties} SET is_homeproz = 1 WHERE list_office_mls_id = %s",
MLS_HOMEPROZ_OFFICE_ID
));
}
update_option('mls_schema_version', 3);
}
// Future migrations go here:
// if ($current_schema < 3) { ... }
// if ($current_schema < 4) { ... }
}
/**
@@ -23,6 +23,23 @@ class MLS_Query {
$this->db = $db;
}
/**
* Get the state filter SQL clause
* Restricts results to MN and IA only
*
* @return string SQL clause
*/
private function get_state_filter() {
if (!defined('MLS_ALLOWED_STATES') || empty(MLS_ALLOWED_STATES)) {
return '';
}
$states = array_map(function($s) {
global $wpdb;
return $wpdb->prepare('%s', $s);
}, MLS_ALLOWED_STATES);
return 'state_or_province IN (' . implode(',', $states) . ')';
}
/**
* Get properties matching criteria
*
@@ -79,6 +96,12 @@ class MLS_Query {
$where = array('mlg_can_view = 1');
$values = array();
// Add state filter (MN and IA only)
$state_filter = $this->get_state_filter();
if ($state_filter) {
$where[] = $state_filter;
}
if ($args['status']) {
$where[] = 'standard_status = %s';
$values[] = $args['status'];
@@ -305,18 +328,20 @@ class MLS_Query {
global $wpdb;
$table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) {
$cities = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT city FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND city IS NOT NULL
WHERE mlg_can_view = 1 AND standard_status = %s AND city IS NOT NULL{$state_clause}
ORDER BY city ASC",
$status
));
} else {
$cities = $wpdb->get_col(
"SELECT DISTINCT city FROM {$table}
WHERE mlg_can_view = 1 AND city IS NOT NULL
WHERE mlg_can_view = 1 AND city IS NOT NULL{$state_clause}
ORDER BY city ASC"
);
}
@@ -334,18 +359,20 @@ class MLS_Query {
global $wpdb;
$table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) {
$counties = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT county FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND county IS NOT NULL
WHERE mlg_can_view = 1 AND standard_status = %s AND county IS NOT NULL{$state_clause}
ORDER BY county ASC",
$status
));
} else {
$counties = $wpdb->get_col(
"SELECT DISTINCT county FROM {$table}
WHERE mlg_can_view = 1 AND county IS NOT NULL
WHERE mlg_can_view = 1 AND county IS NOT NULL{$state_clause}
ORDER BY county ASC"
);
}
@@ -367,6 +394,12 @@ class MLS_Query {
$where = array('mlg_can_view = 1');
$values = array();
// Add state filter (MN and IA only)
$state_filter = $this->get_state_filter();
if ($state_filter) {
$where[] = $state_filter;
}
if (!empty($args['status'])) {
$where[] = 'standard_status = %s';
$values[] = $args['status'];
@@ -437,8 +470,11 @@ class MLS_Query {
public function has_data() {
global $wpdb;
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
$count = $wpdb->get_var(
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE mlg_can_view = 1"
"SELECT COUNT(*) FROM {$this->db->properties_table()} WHERE mlg_can_view = 1{$state_clause}"
);
return (int) $count > 0;
@@ -454,12 +490,14 @@ class MLS_Query {
global $wpdb;
$table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) {
return $wpdb->get_results($wpdb->prepare(
"SELECT property_type, COUNT(*) as count
FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND property_type IS NOT NULL
WHERE mlg_can_view = 1 AND standard_status = %s AND property_type IS NOT NULL{$state_clause}
GROUP BY property_type
ORDER BY count DESC",
$status
@@ -469,7 +507,7 @@ class MLS_Query {
return $wpdb->get_results(
"SELECT property_type, COUNT(*) as count
FROM {$table}
WHERE mlg_can_view = 1 AND property_type IS NOT NULL
WHERE mlg_can_view = 1 AND property_type IS NOT NULL{$state_clause}
GROUP BY property_type
ORDER BY count DESC"
);
@@ -485,12 +523,14 @@ class MLS_Query {
global $wpdb;
$table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) {
return $wpdb->get_row($wpdb->prepare(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table}
WHERE mlg_can_view = 1 AND standard_status = %s AND list_price > 0",
WHERE mlg_can_view = 1 AND standard_status = %s AND list_price > 0{$state_clause}",
$status
));
}
@@ -498,7 +538,7 @@ class MLS_Query {
return $wpdb->get_row(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table}
WHERE mlg_can_view = 1 AND list_price > 0"
WHERE mlg_can_view = 1 AND list_price > 0{$state_clause}"
);
}
@@ -694,6 +694,7 @@ class MLS_Sync_Engine {
'list_office_key' => $property['ListOfficeKey'] ?? null,
'list_office_mls_id' => $property['ListOfficeMlsId'] ?? null,
'list_office_name' => $property['ListOfficeName'] ?? null,
'is_homeproz' => (($property['ListOfficeMlsId'] ?? '') === MLS_HOMEPROZ_OFFICE_ID) ? 1 : 0,
'photos_count' => $property['PhotosCount'] ?? 0,
'modification_timestamp' => $this->format_timestamp($property['ModificationTimestamp'] ?? null),
@@ -32,6 +32,12 @@ define('MLS_TABLE_RATE_LIMITS', 'mls_rate_limits');
define('MLS_TABLE_SYNC_LOG', 'mls_sync_log');
define('MLS_TABLE_MEDIA_LOG', 'mls_media_log');
// HomeProz office MLS ID for identifying our listings
define('MLS_HOMEPROZ_OFFICE_ID', 'NST253235');
// Allowed states for MLS queries (MN and IA only)
define('MLS_ALLOWED_STATES', array('MN', 'IA'));
/**
* Main plugin class
*/
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+17 -36
View File
@@ -12,28 +12,10 @@ if (!defined('ABSPATH')) {
get_header();
// Get featured residential properties (3 most recent active residential listings)
$featured_residential = new WP_Query(array(
'post_type' => 'property',
'posts_per_page' => 3,
'tax_query' => array(
'relation' => 'AND',
array(
'taxonomy' => 'property_status',
'field' => 'slug',
'terms' => 'active',
),
array(
'taxonomy' => 'property_type',
'field' => 'slug',
'terms' => 'residential',
),
),
'orderby' => 'date',
'order' => 'DESC',
));
// Get featured MLS listings for JSON data
$featured_mls_listings = homeproz_get_featured_mls_listings(10); // Get more than needed for random selection
// Get featured commercial/land properties (3 most recent active commercial or land listings)
// Get featured commercial/land properties from WordPress (3 most recent active commercial or land listings)
$featured_commercial = new WP_Query(array(
'post_type' => 'property',
'posts_per_page' => 3,
@@ -130,7 +112,7 @@ $featured_commercial = new WP_Query(array(
get_template_part('template-parts/components/service-cards');
?>
<!-- Featured Residential Properties Section -->
<!-- Featured Homes Section (MLS Listings) -->
<section class="featured-properties-section">
<div class="container">
<header class="section-header">
@@ -138,29 +120,28 @@ $featured_commercial = new WP_Query(array(
<p class="section-subtitle">Browse our residential properties for sale</p>
</header>
<?php if ($featured_residential->have_posts()) : ?>
<div class="property-grid property-grid--3col">
<?php
while ($featured_residential->have_posts()) :
$featured_residential->the_post();
get_template_part('template-parts/property/property-card');
endwhile;
wp_reset_postdata();
?>
<div id="featured-listings-grid" class="property-grid property-grid--3col">
<!-- Populated by JavaScript -->
</div>
<p id="featured-listings-empty" class="no-properties-message" style="display: none;">
No residential properties currently available. Please check back soon.
</p>
<div class="section-footer">
<a href="<?php echo esc_url(home_url('/properties/?type=residential')); ?>" class="btn btn-secondary">
View All Residential
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-secondary">
View All Properties
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</div>
<?php else : ?>
<p class="no-properties-message">No residential properties currently available. Please check back soon.</p>
<?php endif; ?>
</div>
<!-- MLS Listings Data for JavaScript -->
<script type="application/json" id="featured-mls-data">
<?php echo wp_json_encode($featured_mls_listings); ?>
</script>
</section>
<!-- Featured Commercial & Land Properties Section -->
@@ -528,6 +528,13 @@ function homeproz_register_acf_fields() {
'type' => 'text',
'instructions' => 'Real estate license number',
),
array(
'key' => 'field_agent_mls_id',
'label' => 'MLS Agent ID',
'name' => 'agent_mls_id',
'type' => 'text',
'instructions' => 'NorthstarMLS agent ID (e.g., NST503517068)',
),
// Bio Tab
array(
@@ -229,6 +229,165 @@ function homeproz_get_option($key, $default = '') {
return isset($options[$key]) ? $options[$key] : $default;
}
/**
* Get featured MLS listings for homepage
*
* Returns HomeProz listings first, padded with nearby average-priced listings if needed.
* Data is returned as an array ready for JSON encoding.
*
* @param int $count Number of listings to return (default 3)
* @return array Array of listing data for JSON
*/
function homeproz_get_featured_mls_listings($count = 3) {
global $wpdb;
if (!function_exists('mls_get_image_url')) {
return array();
}
$table = $wpdb->prefix . 'mls_properties';
// Albert Lea coordinates and 30-mile bounding box
// 30 miles ~ 0.43 deg latitude, ~0.6 deg longitude at this latitude
$albert_lea_lat = 43.679;
$albert_lea_lon = -93.360;
$lat_range = 0.43;
$lon_range = 0.60;
$min_lat = $albert_lea_lat - $lat_range;
$max_lat = $albert_lea_lat + $lat_range;
$min_lon = $albert_lea_lon - $lon_range;
$max_lon = $albert_lea_lon + $lon_range;
// Get all HomeProz listings (active only, exclude TBD addresses)
// Uses \b word boundary for whole-word match, case-insensitive
$homeproz_listings = $wpdb->get_results(
"SELECT listing_key, list_price, street_number, street_name, street_suffix,
city, state_or_province, postal_code, bedrooms_total, bathrooms_total,
living_area, standard_status, property_type, photos_count
FROM {$table}
WHERE is_homeproz = 1
AND standard_status = 'Active'
AND mlg_can_view = 1
AND COALESCE(street_name, '') NOT REGEXP '\\\\bTBD\\\\b'
AND COALESCE(street_number, '') NOT REGEXP '\\\\bTBD\\\\b'
AND photos_count > 0
ORDER BY modification_timestamp DESC"
);
$listings = array();
// Add HomeProz listings first
foreach ($homeproz_listings as $listing) {
$listings[] = homeproz_format_mls_listing_for_json($listing, true);
}
// If we need padding, get nearby average-priced listings
if (count($listings) < $count) {
$needed = $count - count($listings);
$homeproz_keys = array_column($homeproz_listings, 'listing_key');
// Get average price for residential properties near Albert Lea
$avg_price = $wpdb->get_var($wpdb->prepare(
"SELECT AVG(list_price)
FROM {$table}
WHERE standard_status = 'Active'
AND mlg_can_view = 1
AND property_type = 'Residential'
AND latitude BETWEEN %f AND %f
AND longitude BETWEEN %f AND %f
AND list_price > 0",
$min_lat, $max_lat, $min_lon, $max_lon
));
if ($avg_price > 0) {
// +/- 10% of average price
$min_price = $avg_price * 0.9;
$max_price = $avg_price * 1.1;
// Build exclusion clause for HomeProz listings
$exclude_clause = '';
if (!empty($homeproz_keys)) {
$placeholders = implode(',', array_fill(0, count($homeproz_keys), '%s'));
$exclude_clause = $wpdb->prepare(
" AND listing_key NOT IN ({$placeholders})",
...$homeproz_keys
);
}
// Get random padding listings within price range and distance
$padding_listings = $wpdb->get_results($wpdb->prepare(
"SELECT listing_key, list_price, street_number, street_name, street_suffix,
city, state_or_province, postal_code, bedrooms_total, bathrooms_total,
living_area, standard_status, property_type, photos_count
FROM {$table}
WHERE standard_status = 'Active'
AND mlg_can_view = 1
AND property_type = 'Residential'
AND latitude BETWEEN %f AND %f
AND longitude BETWEEN %f AND %f
AND list_price BETWEEN %f AND %f
AND photos_count > 0
{$exclude_clause}
ORDER BY RAND()
LIMIT %d",
$min_lat, $max_lat, $min_lon, $max_lon,
$min_price, $max_price,
$needed
));
foreach ($padding_listings as $listing) {
$listings[] = homeproz_format_mls_listing_for_json($listing, false);
}
}
}
return $listings;
}
/**
* Format MLS listing data for JSON output
*
* @param object $listing Database row object
* @param bool $is_homeproz Whether this is a HomeProz listing
* @return array Formatted listing data
*/
function homeproz_format_mls_listing_for_json($listing, $is_homeproz = false) {
// Build address
$address_parts = array_filter(array(
$listing->street_number,
$listing->street_name,
$listing->street_suffix
));
$street_address = implode(' ', $address_parts);
$full_address = $street_address;
if ($listing->city) {
$full_address .= ', ' . $listing->city;
}
if ($listing->state_or_province) {
$full_address .= ', ' . $listing->state_or_province;
}
return array(
'listing_key' => $listing->listing_key,
'url' => home_url('/properties/?listing=' . $listing->listing_key),
'image_url' => mls_get_image_url($listing->listing_key, 1, 'thumb'),
'price' => (float) $listing->list_price,
'price_formatted' => homeproz_format_price($listing->list_price),
'address' => $full_address,
'street_address' => $street_address,
'city' => $listing->city,
'state' => $listing->state_or_province,
'bedrooms' => (int) $listing->bedrooms_total,
'bathrooms' => (float) $listing->bathrooms_total,
'sqft' => (int) $listing->living_area,
'status' => $listing->standard_status,
'property_type' => $listing->property_type,
'is_homeproz' => $is_homeproz,
);
}
/**
* Get property locations that have active or pending properties
*
+3 -2
View File
@@ -182,8 +182,9 @@ get_header();
<div class="about-broker-content">
<h3 class="about-broker-title">Broker Information</h3>
<p class="about-broker-text">
HomeProz Real Estate operates as a DBA of Bridge Realty, MN.<br>
Licensed in the State of Minnesota.
HomeProz Real Estate LLC DBA LandProz Real Estate, LLC<br>
111 East Clark Street, Albert Lea, MN 56007<br>
Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
</p>
</div>
</div>
+29 -14
View File
@@ -18,12 +18,9 @@ $phone = homeproz_get_option('phone', '507-516-4870');
$email = homeproz_get_option('email', 'info@homeprozrealestate.com');
$address = homeproz_get_option('address', '111 E Clark St, Albert Lea, MN 56007');
// Check for property inquiry parameter
// Check for property inquiry parameter (passed via URL from single property page)
$property_inquiry = isset($_GET['property']) ? sanitize_text_field(urldecode($_GET['property'])) : '';
$prefilled_message = '';
if ($property_inquiry) {
$prefilled_message = 'I would like to get more information on property: ' . $property_inquiry;
}
$property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['property_url'])) : '';
?>
<main id="primary" class="site-main contact-page-main">
@@ -57,6 +54,19 @@ if ($property_inquiry) {
<div class="contact-form-wrapper">
<h2 class="contact-form-title">Send Us a Message</h2>
<?php if ($property_inquiry) : ?>
<div class="property-inquiry-display">
<div class="property-inquiry-label">Property Inquiry</div>
<div class="property-inquiry-value">
<?php if ($property_link) : ?>
<a href="<?php echo esc_url($property_link); ?>" target="_blank"><?php echo esc_html($property_inquiry); ?></a>
<?php else : ?>
<?php echo esc_html($property_inquiry); ?>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
<?php
// Check for Contact Form 7
if (function_exists('wpcf7_contact_form')) {
@@ -70,6 +80,9 @@ if ($property_inquiry) {
// Show default form markup as fallback
?>
<form class="contact-form" action="" method="post">
<?php if ($property_inquiry) : ?>
<input type="hidden" name="property-inquiry" value="<?php echo esc_attr($property_inquiry); ?>">
<?php endif; ?>
<div class="form-group">
<label for="contact-name">Name <span class="required">*</span></label>
<input type="text" id="contact-name" name="name" required>
@@ -84,7 +97,7 @@ if ($property_inquiry) {
</div>
<div class="form-group">
<label for="contact-message">Message <span class="required">*</span></label>
<textarea id="contact-message" name="message" rows="5" required><?php echo esc_textarea($prefilled_message); ?></textarea>
<textarea id="contact-message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
@@ -94,6 +107,9 @@ if ($property_inquiry) {
// Contact Form 7 not installed - show placeholder form
?>
<form class="contact-form" action="" method="post">
<?php if ($property_inquiry) : ?>
<input type="hidden" name="property-inquiry" value="<?php echo esc_attr($property_inquiry); ?>">
<?php endif; ?>
<div class="form-group">
<label for="contact-name">Name <span class="required">*</span></label>
<input type="text" id="contact-name" name="name" required>
@@ -108,7 +124,7 @@ if ($property_inquiry) {
</div>
<div class="form-group">
<label for="contact-message">Message <span class="required">*</span></label>
<textarea id="contact-message" name="message" rows="5" required><?php echo esc_textarea($prefilled_message); ?></textarea>
<textarea id="contact-message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
@@ -228,19 +244,18 @@ if ($property_inquiry) {
</main>
<?php if ($prefilled_message) : ?>
<?php if ($property_inquiry) : ?>
<script>
(function($) {
if (!$('.Contact_Page').length) return;
$(document).ready(function() {
var prefilledMessage = <?php echo json_encode($prefilled_message); ?>;
var propertyInquiry = <?php echo json_encode($property_inquiry); ?>;
// Try to find textarea in Contact Form 7 or fallback form
var $textarea = $('textarea[name="your-message"], textarea[name="message"], textarea#contact-message').first();
if ($textarea.length && !$textarea.val()) {
$textarea.val(prefilledMessage);
// Populate CF7 hidden field for property inquiry
var $hiddenField = $('input[name="property-inquiry"]');
if ($hiddenField.length) {
$hiddenField.val(propertyInquiry);
}
});
})(jQuery);
+1
View File
@@ -9,6 +9,7 @@ import './main.scss';
// Import component JS
import '../template-parts/header/navigation.js';
import '../template-parts/components/hero-section.js';
import '../template-parts/home/featured-listings.js';
import '../template-parts/property/property-filters.js';
import '../template-parts/property/property-gallery.js';
import '../template-parts/content/content-mortgage-calculator.js';
@@ -50,6 +50,40 @@
margin-bottom: 1.5rem;
}
// Property Inquiry Display (read-only)
.property-inquiry-display {
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-left: 3px solid var(--color-accent);
padding: 1rem 1.25rem;
margin-bottom: 1.5rem;
border-radius: 0.25rem;
}
.property-inquiry-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
margin-bottom: 0.25rem;
}
.property-inquiry-value {
font-size: 0.9375rem;
color: var(--color-text);
line-height: 1.4;
a {
color: var(--color-accent-light);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.contact-form {
.form-group {
margin-bottom: 1.25rem;
@@ -18,13 +18,13 @@ $tiktok = homeproz_get_option('tiktok');
// Office hours - can be moved to theme options later
$office_hours = array(
'Mon-Fri' => '9:00am - 5:00pm',
'Mon-Fri' => '9:00am - 4:00pm',
'Saturday' => 'By Appointment',
'Sunday' => 'Closed',
'Sunday' => 'By Appointment',
);
// License info - can be moved to theme options later
$license_info = 'MN License #40229984';
// Broker info
$broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
?>
<footer id="colophon" class="site-footer">
@@ -39,7 +39,7 @@ $license_info = 'MN License #40229984';
<span class="site-title"><?php bloginfo('name'); ?></span>
<?php endif; ?>
</div>
<p class="footer-tagline">Your trusted partner in Minnesota real estate. Finding homes, building futures.</p>
<p class="footer-tagline">Your trusted partner in Minnesota and Iowa real estate. Finding homes, building futures.</p>
<?php if ($facebook || $tiktok) : ?>
<div class="footer-social">
@@ -173,7 +173,7 @@ $license_info = 'MN License #40229984';
</a>
</div>
<p class="footer-license"><?php echo esc_html($license_info); ?></p>
<p class="footer-license"><?php echo esc_html($broker_info); ?></p>
</div>
<!-- Footer Bottom -->
@@ -0,0 +1,156 @@
/**
* Featured Listings - Home Page
*
* Randomly selects and displays MLS listings on the home page.
*/
(function($) {
'use strict';
// Early return if not on home page
if (!$('body').hasClass('Home_Page')) {
return;
}
var FeaturedListings = {
grid: null,
emptyMessage: null,
listings: [],
init: function() {
this.grid = $('#featured-listings-grid');
this.emptyMessage = $('#featured-listings-empty');
if (!this.grid.length) {
return;
}
// Load listings data from JSON
this.loadListingsData();
// Render random selection
this.renderListings();
},
loadListingsData: function() {
var dataElement = document.getElementById('featured-mls-data');
if (!dataElement) {
this.listings = [];
return;
}
try {
this.listings = JSON.parse(dataElement.textContent);
} catch (e) {
console.error('Failed to parse featured listings data:', e);
this.listings = [];
}
},
/**
* Shuffle array using Fisher-Yates algorithm
*/
shuffleArray: function(array) {
var shuffled = array.slice();
for (var i = shuffled.length - 1; i > 0; i--) {
var j = Math.floor(Math.random() * (i + 1));
var temp = shuffled[i];
shuffled[i] = shuffled[j];
shuffled[j] = temp;
}
return shuffled;
},
renderListings: function() {
if (!this.listings || this.listings.length === 0) {
this.grid.hide();
this.emptyMessage.show();
return;
}
// Shuffle and take up to 3 listings
var shuffled = this.shuffleArray(this.listings);
var selected = shuffled.slice(0, 3);
// Build HTML for each listing
var html = '';
for (var i = 0; i < selected.length; i++) {
html += this.buildPropertyCard(selected[i]);
}
this.grid.html(html);
this.grid.show();
this.emptyMessage.hide();
},
buildPropertyCard: function(listing) {
var bedsLabel = listing.bedrooms === 1 ? 'Bed' : 'Beds';
var bathsLabel = listing.bathrooms === 1 ? 'Bath' : 'Baths';
var specsHtml = '';
if (listing.bedrooms || listing.bathrooms || listing.sqft) {
specsHtml = '<ul class="property-card-specs">';
if (listing.bedrooms) {
specsHtml += '<li class="spec-item">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
'<path d="M3 7v11h18V7M3 7V4a1 1 0 0 1 1-1h16a1 1 0 0 1 1 1v3M3 7h18M7 11h4v4H7zM14 11h3"/>' +
'</svg>' +
'<span>' + listing.bedrooms + ' ' + bedsLabel + '</span>' +
'</li>';
}
if (listing.bathrooms) {
specsHtml += '<li class="spec-item">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
'<path d="M4 12h16M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7M4 12V6a2 2 0 0 1 2-2h3v2a1 1 0 0 0 1 1h1a1 1 0 0 0 1-1V4"/>' +
'</svg>' +
'<span>' + listing.bathrooms + ' ' + bathsLabel + '</span>' +
'</li>';
}
if (listing.sqft) {
specsHtml += '<li class="spec-item">' +
'<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"/>' +
'<path d="M3 9h18M9 3v18"/>' +
'</svg>' +
'<span>' + listing.sqft.toLocaleString() + ' sqft</span>' +
'</li>';
}
specsHtml += '</ul>';
}
return '<article class="property-card card mls-property" data-listing-key="' + this.escapeHtml(listing.listing_key) + '">' +
'<a href="' + this.escapeHtml(listing.url) + '" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>' +
'<div class="property-card-image has-image" style="background-image: url(' + this.escapeHtml(listing.image_url) + ')">' +
'<span class="property-card-badge badge badge-active">Active</span>' +
'</div>' +
'<div class="property-card-content">' +
'<div class="property-card-price">' + this.escapeHtml(listing.price_formatted) + '</div>' +
'<h3 class="property-card-title">' + this.escapeHtml(listing.address) + '</h3>' +
specsHtml +
'<span class="property-card-link">' +
'View Details' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">' +
'<path d="M5 12h14M12 5l7 7-7 7"/>' +
'</svg>' +
'</span>' +
'</div>' +
'</article>';
},
escapeHtml: function(text) {
if (!text) return '';
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on DOM ready
$(document).ready(function() {
FeaturedListings.init();
});
})(jQuery);
@@ -83,6 +83,10 @@
this.bindSwipeEvents();
this.updateThumbnailNavigation();
// Setup thumbnail loading states and preload first two pages
this.setupThumbnailLoading();
this.preloadThumbnailPages(0, 2);
// Start autoplay only if more than 1 image
if (this.images.length > 1) {
this.startAutoplay();
@@ -125,12 +129,14 @@
}
});
// Thumbnail navigation buttons
// Thumbnail navigation buttons - stop autoplay when paginating
this.$prevBtn.on('click', function() {
self.stopAutoplay();
self.prevThumbnailPage();
});
this.$nextBtn.on('click', function() {
self.stopAutoplay();
self.nextThumbnailPage();
});
@@ -528,6 +534,7 @@
if (this.thumbnailPage > 0) {
this.thumbnailPage--;
this.scrollThumbnails();
this.preloadPrevThumbnailPage();
}
},
@@ -539,6 +546,7 @@
if (this.thumbnailPage < totalPages - 1) {
this.thumbnailPage++;
this.scrollThumbnails();
this.preloadNextThumbnailPage();
}
},
@@ -665,6 +673,87 @@
this.$lightboxImage.attr('src', image.url);
this.$lightboxImage.attr('alt', image.alt || 'Property photo');
this.$lightboxCounter.text(this.currentIndex + 1);
},
/**
* Setup thumbnail loading states
* Adds loading class and spinner to each thumbnail
*/
setupThumbnailLoading: function() {
this.$thumbnails.each(function() {
var $thumb = $(this);
var $img = $thumb.find('img');
// Add loading state
$thumb.addClass('is-loading');
// Add spinner element
if (!$thumb.find('.thumbnail-spinner').length) {
$thumb.append('<div class="thumbnail-spinner"><div class="spinner"></div></div>');
}
// Handle image load
if ($img[0].complete) {
$thumb.removeClass('is-loading');
} else {
$img.on('load', function() {
$thumb.removeClass('is-loading');
});
$img.on('error', function() {
$thumb.removeClass('is-loading');
});
}
});
},
/**
* Preload thumbnail pages
* @param {number} startPage - First page to preload (0-indexed)
* @param {number} numPages - Number of pages to preload
*/
preloadThumbnailPages: function(startPage, numPages) {
var self = this;
var startIndex = startPage * this.thumbnailsPerPage;
var endIndex = Math.min((startPage + numPages) * this.thumbnailsPerPage, this.images.length);
for (var i = startIndex; i < endIndex; i++) {
(function(index) {
var $thumb = self.$thumbnails.filter('[data-index="' + index + '"]');
var $img = $thumb.find('img');
// Remove lazy loading to force immediate load
$img.removeAttr('loading');
// If not already loaded, preload
if (!$img[0].complete) {
var preloader = new Image();
preloader.src = $img.attr('src');
}
})(i);
}
},
/**
* Preload next page of thumbnails when navigating
*/
preloadNextThumbnailPage: function() {
var totalPages = Math.ceil(this.images.length / this.thumbnailsPerPage);
var nextPage = this.thumbnailPage + 1;
if (nextPage < totalPages) {
this.preloadThumbnailPages(nextPage, 1);
}
},
/**
* Preload previous page of thumbnails when navigating
*/
preloadPrevThumbnailPage: function() {
var prevPage = this.thumbnailPage - 1;
if (prevPage >= 0) {
this.preloadThumbnailPages(prevPage, 1);
}
}
};
@@ -117,7 +117,7 @@
width: calc((100% - 2rem) / 5);
padding: 0;
border: 2px solid transparent;
background: none;
background: var(--color-bg-card);
cursor: pointer;
border-radius: 0.25rem;
overflow: hidden;
@@ -135,6 +135,44 @@
aspect-ratio: 1;
object-fit: cover;
display: block;
opacity: 1;
transition: opacity 0.2s ease;
}
// Loading state
&.is-loading {
img {
opacity: 0;
}
.thumbnail-spinner {
display: flex;
}
}
}
// Thumbnail spinner
.thumbnail-spinner {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: var(--color-bg-card);
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: thumbnail-spin 0.8s linear infinite;
}
}
@keyframes thumbnail-spin {
to {
transform: rotate(360deg);
}
}
@@ -372,7 +372,14 @@ get_header();
<p class="office-name"><?php echo esc_html($office_name); ?></p>
<?php endif; ?>
<a href="<?php echo esc_url(home_url('/contact/')); ?>" class="btn btn-primary btn-block">
<?php
$contact_url = add_query_arg(array(
'property' => urlencode($full_address),
'property_url' => urlencode(home_url('/properties/?listing=' . $listing_key)),
'property-inquiry' => urlencode($full_address),
), home_url('/contact/'));
?>
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-primary btn-block">
Contact About This Property
</a>
</div>