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-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-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"} {"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-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-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-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-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-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"} {"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-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-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-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-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-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-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", "database": "beads.db",
"jsonl_export": "issues.jsonl", "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. **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 ## Category 5: Footer & Legal
@@ -734,17 +736,17 @@ Also: "Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
## Priority Summary ## Priority Summary
### Must-Do (Critical) ### Must-Do (Critical)
1. Broker footer legal text update (5.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) 2. Phone number update (5.2) - AWAITING: Client needs to confirm final phone number
3. Property inquiry form auto-population (4.1) 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) 4. Featured photo from MLS (1.2) - ALREADY WORKING: media_order=1 used as primary
5. Logo consistency (2.4) 5. Logo consistency (2.4) - AWAITING: Client needs to provide condensed HP logo SVG
6. Agent state licenses (3.8) 6. Agent state licenses (3.8) - AWAITING: Client needs to provide license numbers
7. Bridge Realty text removal (9.1) 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) ### High Priority (Implement)
1. Co-listing agent support (1.1) 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) 3. Expanded search filters (1.5)
4. Property type showcase boxes (1.6) 4. Property type showcase boxes (1.6)
5. Multi-recipient email routing (4.2) 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 - Sold properties strategy
- Full agent tier system - Full agent tier system
- Buyers/sellers guide revamp - 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 * 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 * Get table name with prefix
@@ -126,6 +126,7 @@ class MLS_DB {
list_office_key VARCHAR(50) DEFAULT NULL, list_office_key VARCHAR(50) DEFAULT NULL,
list_office_mls_id VARCHAR(50) DEFAULT NULL, list_office_mls_id VARCHAR(50) DEFAULT NULL,
list_office_name VARCHAR(150) DEFAULT NULL, list_office_name VARCHAR(150) DEFAULT NULL,
is_homeproz TINYINT(1) NOT NULL DEFAULT 0,
photos_count INT(5) DEFAULT 0, photos_count INT(5) DEFAULT 0,
modification_timestamp DATETIME NOT NULL, modification_timestamp DATETIME NOT NULL,
@@ -332,8 +333,41 @@ class MLS_DB {
update_option('mls_schema_version', 2); 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: // Future migrations go here:
// if ($current_schema < 3) { ... } // if ($current_schema < 4) { ... }
} }
/** /**
@@ -23,6 +23,23 @@ class MLS_Query {
$this->db = $db; $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 * Get properties matching criteria
* *
@@ -79,6 +96,12 @@ class MLS_Query {
$where = array('mlg_can_view = 1'); $where = array('mlg_can_view = 1');
$values = array(); $values = array();
// Add state filter (MN and IA only)
$state_filter = $this->get_state_filter();
if ($state_filter) {
$where[] = $state_filter;
}
if ($args['status']) { if ($args['status']) {
$where[] = 'standard_status = %s'; $where[] = 'standard_status = %s';
$values[] = $args['status']; $values[] = $args['status'];
@@ -305,18 +328,20 @@ class MLS_Query {
global $wpdb; global $wpdb;
$table = $this->db->properties_table(); $table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) { if ($status) {
$cities = $wpdb->get_col($wpdb->prepare( $cities = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT city FROM {$table} "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", ORDER BY city ASC",
$status $status
)); ));
} else { } else {
$cities = $wpdb->get_col( $cities = $wpdb->get_col(
"SELECT DISTINCT city FROM {$table} "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" ORDER BY city ASC"
); );
} }
@@ -334,18 +359,20 @@ class MLS_Query {
global $wpdb; global $wpdb;
$table = $this->db->properties_table(); $table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) { if ($status) {
$counties = $wpdb->get_col($wpdb->prepare( $counties = $wpdb->get_col($wpdb->prepare(
"SELECT DISTINCT county FROM {$table} "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", ORDER BY county ASC",
$status $status
)); ));
} else { } else {
$counties = $wpdb->get_col( $counties = $wpdb->get_col(
"SELECT DISTINCT county FROM {$table} "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" ORDER BY county ASC"
); );
} }
@@ -367,6 +394,12 @@ class MLS_Query {
$where = array('mlg_can_view = 1'); $where = array('mlg_can_view = 1');
$values = array(); $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'])) { if (!empty($args['status'])) {
$where[] = 'standard_status = %s'; $where[] = 'standard_status = %s';
$values[] = $args['status']; $values[] = $args['status'];
@@ -437,8 +470,11 @@ class MLS_Query {
public function has_data() { public function has_data() {
global $wpdb; global $wpdb;
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
$count = $wpdb->get_var( $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; return (int) $count > 0;
@@ -454,12 +490,14 @@ class MLS_Query {
global $wpdb; global $wpdb;
$table = $this->db->properties_table(); $table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) { if ($status) {
return $wpdb->get_results($wpdb->prepare( return $wpdb->get_results($wpdb->prepare(
"SELECT property_type, COUNT(*) as count "SELECT property_type, COUNT(*) as count
FROM {$table} 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 GROUP BY property_type
ORDER BY count DESC", ORDER BY count DESC",
$status $status
@@ -469,7 +507,7 @@ class MLS_Query {
return $wpdb->get_results( return $wpdb->get_results(
"SELECT property_type, COUNT(*) as count "SELECT property_type, COUNT(*) as count
FROM {$table} 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 GROUP BY property_type
ORDER BY count DESC" ORDER BY count DESC"
); );
@@ -485,12 +523,14 @@ class MLS_Query {
global $wpdb; global $wpdb;
$table = $this->db->properties_table(); $table = $this->db->properties_table();
$state_filter = $this->get_state_filter();
$state_clause = $state_filter ? " AND {$state_filter}" : '';
if ($status) { if ($status) {
return $wpdb->get_row($wpdb->prepare( return $wpdb->get_row($wpdb->prepare(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price "SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table} 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 $status
)); ));
} }
@@ -498,7 +538,7 @@ class MLS_Query {
return $wpdb->get_row( return $wpdb->get_row(
"SELECT MIN(list_price) as min_price, MAX(list_price) as max_price "SELECT MIN(list_price) as min_price, MAX(list_price) as max_price
FROM {$table} 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_key' => $property['ListOfficeKey'] ?? null,
'list_office_mls_id' => $property['ListOfficeMlsId'] ?? null, 'list_office_mls_id' => $property['ListOfficeMlsId'] ?? null,
'list_office_name' => $property['ListOfficeName'] ?? null, 'list_office_name' => $property['ListOfficeName'] ?? null,
'is_homeproz' => (($property['ListOfficeMlsId'] ?? '') === MLS_HOMEPROZ_OFFICE_ID) ? 1 : 0,
'photos_count' => $property['PhotosCount'] ?? 0, 'photos_count' => $property['PhotosCount'] ?? 0,
'modification_timestamp' => $this->format_timestamp($property['ModificationTimestamp'] ?? null), '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_SYNC_LOG', 'mls_sync_log');
define('MLS_TABLE_MEDIA_LOG', 'mls_media_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 * 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_header();
// Get featured residential properties (3 most recent active residential listings) // Get featured MLS listings for JSON data
$featured_residential = new WP_Query(array( $featured_mls_listings = homeproz_get_featured_mls_listings(10); // Get more than needed for random selection
'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 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( $featured_commercial = new WP_Query(array(
'post_type' => 'property', 'post_type' => 'property',
'posts_per_page' => 3, 'posts_per_page' => 3,
@@ -130,7 +112,7 @@ $featured_commercial = new WP_Query(array(
get_template_part('template-parts/components/service-cards'); get_template_part('template-parts/components/service-cards');
?> ?>
<!-- Featured Residential Properties Section --> <!-- Featured Homes Section (MLS Listings) -->
<section class="featured-properties-section"> <section class="featured-properties-section">
<div class="container"> <div class="container">
<header class="section-header"> <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> <p class="section-subtitle">Browse our residential properties for sale</p>
</header> </header>
<?php if ($featured_residential->have_posts()) : ?> <div id="featured-listings-grid" class="property-grid property-grid--3col">
<div class="property-grid property-grid--3col"> <!-- Populated by JavaScript -->
<?php
while ($featured_residential->have_posts()) :
$featured_residential->the_post();
get_template_part('template-parts/property/property-card');
endwhile;
wp_reset_postdata();
?>
</div> </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"> <div class="section-footer">
<a href="<?php echo esc_url(home_url('/properties/?type=residential')); ?>" class="btn btn-secondary"> <a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-secondary">
View All Residential View All Properties
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> <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"/> <path d="M5 12h14M12 5l7 7-7 7"/>
</svg> </svg>
</a> </a>
</div> </div>
<?php else : ?>
<p class="no-properties-message">No residential properties currently available. Please check back soon.</p>
<?php endif; ?>
</div> </div>
<!-- MLS Listings Data for JavaScript -->
<script type="application/json" id="featured-mls-data">
<?php echo wp_json_encode($featured_mls_listings); ?>
</script>
</section> </section>
<!-- Featured Commercial & Land Properties Section --> <!-- Featured Commercial & Land Properties Section -->
@@ -528,6 +528,13 @@ function homeproz_register_acf_fields() {
'type' => 'text', 'type' => 'text',
'instructions' => 'Real estate license number', '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 // Bio Tab
array( array(
@@ -229,6 +229,165 @@ function homeproz_get_option($key, $default = '') {
return isset($options[$key]) ? $options[$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 * Get property locations that have active or pending properties
* *
+3 -2
View File
@@ -182,8 +182,9 @@ get_header();
<div class="about-broker-content"> <div class="about-broker-content">
<h3 class="about-broker-title">Broker Information</h3> <h3 class="about-broker-title">Broker Information</h3>
<p class="about-broker-text"> <p class="about-broker-text">
HomeProz Real Estate operates as a DBA of Bridge Realty, MN.<br> HomeProz Real Estate LLC DBA LandProz Real Estate, LLC<br>
Licensed in the State of Minnesota. 111 East Clark Street, Albert Lea, MN 56007<br>
Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
</p> </p>
</div> </div>
</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'); $email = homeproz_get_option('email', 'info@homeprozrealestate.com');
$address = homeproz_get_option('address', '111 E Clark St, Albert Lea, MN 56007'); $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'])) : ''; $property_inquiry = isset($_GET['property']) ? sanitize_text_field(urldecode($_GET['property'])) : '';
$prefilled_message = ''; $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['property_url'])) : '';
if ($property_inquiry) {
$prefilled_message = 'I would like to get more information on property: ' . $property_inquiry;
}
?> ?>
<main id="primary" class="site-main contact-page-main"> <main id="primary" class="site-main contact-page-main">
@@ -57,6 +54,19 @@ if ($property_inquiry) {
<div class="contact-form-wrapper"> <div class="contact-form-wrapper">
<h2 class="contact-form-title">Send Us a Message</h2> <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 <?php
// Check for Contact Form 7 // Check for Contact Form 7
if (function_exists('wpcf7_contact_form')) { if (function_exists('wpcf7_contact_form')) {
@@ -70,6 +80,9 @@ if ($property_inquiry) {
// Show default form markup as fallback // Show default form markup as fallback
?> ?>
<form class="contact-form" action="" method="post"> <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"> <div class="form-group">
<label for="contact-name">Name <span class="required">*</span></label> <label for="contact-name">Name <span class="required">*</span></label>
<input type="text" id="contact-name" name="name" required> <input type="text" id="contact-name" name="name" required>
@@ -84,7 +97,7 @@ if ($property_inquiry) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="contact-message">Message <span class="required">*</span></label> <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> </div>
<button type="submit" class="btn btn-primary">Send Message</button> <button type="submit" class="btn btn-primary">Send Message</button>
</form> </form>
@@ -94,6 +107,9 @@ if ($property_inquiry) {
// Contact Form 7 not installed - show placeholder form // Contact Form 7 not installed - show placeholder form
?> ?>
<form class="contact-form" action="" method="post"> <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"> <div class="form-group">
<label for="contact-name">Name <span class="required">*</span></label> <label for="contact-name">Name <span class="required">*</span></label>
<input type="text" id="contact-name" name="name" required> <input type="text" id="contact-name" name="name" required>
@@ -108,7 +124,7 @@ if ($property_inquiry) {
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="contact-message">Message <span class="required">*</span></label> <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> </div>
<button type="submit" class="btn btn-primary">Send Message</button> <button type="submit" class="btn btn-primary">Send Message</button>
</form> </form>
@@ -228,19 +244,18 @@ if ($property_inquiry) {
</main> </main>
<?php if ($prefilled_message) : ?> <?php if ($property_inquiry) : ?>
<script> <script>
(function($) { (function($) {
if (!$('.Contact_Page').length) return; if (!$('.Contact_Page').length) return;
$(document).ready(function() { $(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 // Populate CF7 hidden field for property inquiry
var $textarea = $('textarea[name="your-message"], textarea[name="message"], textarea#contact-message').first(); var $hiddenField = $('input[name="property-inquiry"]');
if ($hiddenField.length) {
if ($textarea.length && !$textarea.val()) { $hiddenField.val(propertyInquiry);
$textarea.val(prefilledMessage);
} }
}); });
})(jQuery); })(jQuery);
+1
View File
@@ -9,6 +9,7 @@ import './main.scss';
// Import component JS // Import component JS
import '../template-parts/header/navigation.js'; import '../template-parts/header/navigation.js';
import '../template-parts/components/hero-section.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-filters.js';
import '../template-parts/property/property-gallery.js'; import '../template-parts/property/property-gallery.js';
import '../template-parts/content/content-mortgage-calculator.js'; import '../template-parts/content/content-mortgage-calculator.js';
@@ -50,6 +50,40 @@
margin-bottom: 1.5rem; 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 { .contact-form {
.form-group { .form-group {
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
@@ -18,13 +18,13 @@ $tiktok = homeproz_get_option('tiktok');
// Office hours - can be moved to theme options later // Office hours - can be moved to theme options later
$office_hours = array( $office_hours = array(
'Mon-Fri' => '9:00am - 5:00pm', 'Mon-Fri' => '9:00am - 4:00pm',
'Saturday' => 'By Appointment', 'Saturday' => 'By Appointment',
'Sunday' => 'Closed', 'Sunday' => 'By Appointment',
); );
// License info - can be moved to theme options later // Broker info
$license_info = 'MN License #40229984'; $broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
?> ?>
<footer id="colophon" class="site-footer"> <footer id="colophon" class="site-footer">
@@ -39,7 +39,7 @@ $license_info = 'MN License #40229984';
<span class="site-title"><?php bloginfo('name'); ?></span> <span class="site-title"><?php bloginfo('name'); ?></span>
<?php endif; ?> <?php endif; ?>
</div> </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) : ?> <?php if ($facebook || $tiktok) : ?>
<div class="footer-social"> <div class="footer-social">
@@ -173,7 +173,7 @@ $license_info = 'MN License #40229984';
</a> </a>
</div> </div>
<p class="footer-license"><?php echo esc_html($license_info); ?></p> <p class="footer-license"><?php echo esc_html($broker_info); ?></p>
</div> </div>
<!-- Footer Bottom --> <!-- 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.bindSwipeEvents();
this.updateThumbnailNavigation(); 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 // Start autoplay only if more than 1 image
if (this.images.length > 1) { if (this.images.length > 1) {
this.startAutoplay(); this.startAutoplay();
@@ -125,12 +129,14 @@
} }
}); });
// Thumbnail navigation buttons // Thumbnail navigation buttons - stop autoplay when paginating
this.$prevBtn.on('click', function() { this.$prevBtn.on('click', function() {
self.stopAutoplay();
self.prevThumbnailPage(); self.prevThumbnailPage();
}); });
this.$nextBtn.on('click', function() { this.$nextBtn.on('click', function() {
self.stopAutoplay();
self.nextThumbnailPage(); self.nextThumbnailPage();
}); });
@@ -528,6 +534,7 @@
if (this.thumbnailPage > 0) { if (this.thumbnailPage > 0) {
this.thumbnailPage--; this.thumbnailPage--;
this.scrollThumbnails(); this.scrollThumbnails();
this.preloadPrevThumbnailPage();
} }
}, },
@@ -539,6 +546,7 @@
if (this.thumbnailPage < totalPages - 1) { if (this.thumbnailPage < totalPages - 1) {
this.thumbnailPage++; this.thumbnailPage++;
this.scrollThumbnails(); this.scrollThumbnails();
this.preloadNextThumbnailPage();
} }
}, },
@@ -665,6 +673,87 @@
this.$lightboxImage.attr('src', image.url); this.$lightboxImage.attr('src', image.url);
this.$lightboxImage.attr('alt', image.alt || 'Property photo'); this.$lightboxImage.attr('alt', image.alt || 'Property photo');
this.$lightboxCounter.text(this.currentIndex + 1); 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); width: calc((100% - 2rem) / 5);
padding: 0; padding: 0;
border: 2px solid transparent; border: 2px solid transparent;
background: none; background: var(--color-bg-card);
cursor: pointer; cursor: pointer;
border-radius: 0.25rem; border-radius: 0.25rem;
overflow: hidden; overflow: hidden;
@@ -135,6 +135,44 @@
aspect-ratio: 1; aspect-ratio: 1;
object-fit: cover; object-fit: cover;
display: block; 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> <p class="office-name"><?php echo esc_html($office_name); ?></p>
<?php endif; ?> <?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 Contact About This Property
</a> </a>
</div> </div>