Snapshot: MLS sync fixes, image refresh, plugin/theme updates

MLS plugin fixes from this session:
- Fix silent insert failures: location column NOT NULL was rejecting wpdb->insert calls,
  causing ~18k new properties since Dec 2025 to be lost. Inserts now build raw SQL
  with ST_PointFromText so the spatial column is populated atomically.
- Auto-refresh expired media URLs in MLS_Media_Handler::fetch_and_cache(), guarded by
  a property-level GET_LOCK so concurrent fetches share one API refresh.
- Normalize WP_Error to null in mls_get_property_image() so callers can rely on the
  documented string|null contract.
- Support comma-separated property_type filters in MLS_Query and MLS_Cluster so the
  homepage "View All Commercial" link (?property_type=Commercial+Sale,Land,Farm)
  actually filters correctly.
- Incremental sync now looks back 10 minutes past the latest modification timestamp
  as a safety margin against missed records.
- Smart sync exits silently (info-level, not warning) when a full sync is in progress.

Operational:
- New cron: weekly full sync Sundays at 3 AM (/usr/local/bin/mls-full-sync).
- New cron: hourly 2GB cap on mls-thumbnails/ and cache/transformed-images/
  (/usr/local/bin/mls-image-cache-cap).
- Logrotate config for wp-content/debug.log (2-day retention, daily rotation,
  delaycompress).

Repo policy:
- CLAUDE.md updated with explicit "commit everything except build artifacts" policy.
- .gitignore: untrack runtime image caches and debug.log rotations.

Other modifications in this snapshot are pre-existing in-flight theme/plugin/db_content_updates work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
root
2026-04-29 15:32:23 +00:00
parent 57b752f54e
commit b6df4dbb92
5385 changed files with 838580 additions and 2416 deletions
+31 -30
View File
@@ -11,40 +11,41 @@ if (!defined('ABSPATH')) {
}
get_header();
// Determine archive title and subtitle
$archive_title = 'News & Insights';
$archive_subtitle = 'Stay informed with real estate tips, market updates, and community news';
if (is_category()) {
$archive_title = single_cat_title('', false);
$archive_subtitle = category_description() ?: 'Browse posts in this category';
} elseif (is_tag()) {
$archive_title = 'Tagged: ' . single_tag_title('', false);
$archive_subtitle = tag_description() ?: 'Browse posts with this tag';
} elseif (is_author()) {
$archive_title = 'Posts by ' . get_the_author();
$archive_subtitle = get_the_author_meta('description') ?: 'View all posts by this author';
} elseif (is_date()) {
if (is_year()) {
$archive_title = 'Archive: ' . get_the_date('Y');
} elseif (is_month()) {
$archive_title = 'Archive: ' . get_the_date('F Y');
} else {
$archive_title = 'Archive: ' . get_the_date();
}
$archive_subtitle = 'Browse posts from this time period';
}
?>
<main id="primary" class="site-main archive-main">
<?php
// Hero Section
$archive_title = 'News & Insights';
$archive_subtitle = 'Stay informed with real estate tips, market updates, and community news';
if (is_category()) {
$archive_title = single_cat_title('', false);
$archive_subtitle = category_description();
} elseif (is_tag()) {
$archive_title = 'Tagged: ' . single_tag_title('', false);
$archive_subtitle = tag_description();
} elseif (is_author()) {
$archive_title = 'Posts by ' . get_the_author();
$archive_subtitle = get_the_author_meta('description');
} elseif (is_date()) {
if (is_year()) {
$archive_title = 'Archive: ' . get_the_date('Y');
} elseif (is_month()) {
$archive_title = 'Archive: ' . get_the_date('F Y');
} else {
$archive_title = 'Archive: ' . get_the_date();
}
}
get_template_part('template-parts/components/hero-section', null, array(
'title' => $archive_title,
'subtitle' => $archive_subtitle ?: 'Browse our articles and updates',
'size' => 'small',
));
?>
<!-- Archive Hero -->
<section class="archive-hero">
<div class="container">
<h1 class="archive-hero-title"><?php echo esc_html($archive_title); ?></h1>
<p class="archive-hero-subtitle"><?php echo esc_html($archive_subtitle); ?></p>
</div>
</section>
<section class="archive-content-section">
<div class="container">
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+32
View File
@@ -53,6 +53,9 @@ require_once HOMEPROZ_DIR . '/inc/yoast-seo.php';
// Favicon management
require_once HOMEPROZ_DIR . '/inc/favicon.php';
// Social sharing meta tags (Open Graph, Twitter Cards)
require_once HOMEPROZ_DIR . '/inc/social-sharing.php';
/**
* Send no-cache headers for HTML pages
* Prevents browser/proxy caching of dynamic content
@@ -73,3 +76,32 @@ function homeproz_nocache_headers() {
header('Expires: 0');
}
add_action('send_headers', 'homeproz_nocache_headers');
/**
* Whitelist Contact Form 7 REST routes for AIOS plugin
*/
add_filter('aios_whitelisted_rest_routes', function($routes) {
$routes[] = 'contact-form-7';
return $routes;
});
/**
* Disable comments site-wide
*/
// Close comments on the frontend
add_filter('comments_open', '__return_false', 20, 2);
add_filter('pings_open', '__return_false', 20, 2);
// Hide existing comments
add_filter('comments_array', '__return_empty_array', 10, 2);
// Remove comments from admin menu
add_action('admin_menu', function() {
remove_menu_page('edit-comments.php');
});
// Remove comments from admin bar
add_action('wp_before_admin_bar_render', function() {
global $wp_admin_bar;
$wp_admin_bar->remove_menu('comments');
});
+7 -8
View File
@@ -16,14 +16,13 @@ get_header();
<main id="primary" class="site-main archive-main">
<?php
// Hero Section
get_template_part('template-parts/components/hero-section', null, array(
'title' => 'News & Insights',
'subtitle' => 'Stay informed with real estate tips, market updates, and community news',
'size' => 'small',
));
?>
<!-- Blog Hero -->
<section class="archive-hero">
<div class="container">
<h1 class="archive-hero-title">News & Insights</h1>
<p class="archive-hero-subtitle">Stay informed with real estate tips, market updates, and community news</p>
</div>
</section>
<section class="archive-content-section">
<div class="container">
+58 -323
View File
@@ -2,7 +2,7 @@
/**
* ACF Field Groups
*
* Registers custom fields for properties using ACF
* Registers custom fields for agents, theme options, and pages using ACF
*
* @package HomeProz
*/
@@ -13,297 +13,13 @@ if (!defined('ABSPATH')) {
}
/**
* Register Property Details field group
* Register ACF field groups
*/
function homeproz_register_acf_fields() {
if (!function_exists('acf_add_local_field_group')) {
return;
}
// Property Details Field Group
acf_add_local_field_group(array(
'key' => 'group_property_details',
'title' => 'Property Details',
'fields' => array(
// Pricing Tab
array(
'key' => 'field_property_tab_pricing',
'label' => 'Pricing & Status',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_price',
'label' => 'Price',
'name' => 'property_price',
'type' => 'number',
'instructions' => 'Enter the listing price (numbers only, no commas or $)',
'required' => 1,
'prepend' => '$',
'min' => 0,
),
array(
'key' => 'field_property_mls_number',
'label' => 'MLS Number',
'name' => 'mls_number',
'type' => 'text',
'instructions' => 'Enter the MLS listing number',
),
array(
'key' => 'field_property_external_url',
'label' => 'External Listing URL',
'name' => 'external_listing_url',
'type' => 'url',
'instructions' => 'Link to external listing page (Zillow, Homefinder, etc.)',
),
// Address Tab
array(
'key' => 'field_property_tab_address',
'label' => 'Address',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_street_address',
'label' => 'Street Address',
'name' => 'street_address',
'type' => 'text',
'required' => 1,
),
array(
'key' => 'field_property_city',
'label' => 'City',
'name' => 'city',
'type' => 'text',
'required' => 1,
'default_value' => 'Albert Lea',
),
array(
'key' => 'field_property_state',
'label' => 'State',
'name' => 'state',
'type' => 'text',
'required' => 1,
'default_value' => 'MN',
),
array(
'key' => 'field_property_zip',
'label' => 'ZIP Code',
'name' => 'zip_code',
'type' => 'text',
'required' => 1,
),
// Property Details Tab
array(
'key' => 'field_property_tab_details',
'label' => 'Property Details',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_bedrooms',
'label' => 'Bedrooms',
'name' => 'bedrooms',
'type' => 'number',
'min' => 0,
'max' => 20,
),
array(
'key' => 'field_property_bathrooms',
'label' => 'Bathrooms',
'name' => 'bathrooms',
'type' => 'number',
'min' => 0,
'max' => 20,
'step' => 0.5,
),
array(
'key' => 'field_property_square_feet',
'label' => 'Square Feet',
'name' => 'square_feet',
'type' => 'number',
'min' => 0,
),
array(
'key' => 'field_property_lot_size',
'label' => 'Lot Size',
'name' => 'lot_size',
'type' => 'text',
'instructions' => 'e.g., "0.25 acres" or "10,890 sq ft"',
),
array(
'key' => 'field_property_year_built',
'label' => 'Year Built',
'name' => 'year_built',
'type' => 'number',
'min' => 1800,
'max' => 2100,
),
array(
'key' => 'field_property_garage',
'label' => 'Garage',
'name' => 'garage',
'type' => 'select',
'choices' => array(
'' => 'None',
'1' => '1 Car',
'2' => '2 Car',
'3' => '3 Car',
'4+' => '4+ Car',
),
),
// Features Tab
array(
'key' => 'field_property_tab_features',
'label' => 'Features',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_short_description',
'label' => 'Short Description',
'name' => 'short_description',
'type' => 'textarea',
'instructions' => 'Brief description for property cards (1-2 sentences)',
'rows' => 3,
'maxlength' => 250,
),
array(
'key' => 'field_property_features',
'label' => 'Property Features',
'name' => 'property_features',
'type' => 'checkbox',
'instructions' => 'Select all features that apply',
'choices' => array(
'central_air' => 'Central Air',
'central_heat' => 'Central Heat',
'fireplace' => 'Fireplace',
'hardwood_floors' => 'Hardwood Floors',
'updated_kitchen' => 'Updated Kitchen',
'updated_bathrooms' => 'Updated Bathrooms',
'basement' => 'Basement',
'finished_basement' => 'Finished Basement',
'deck_patio' => 'Deck/Patio',
'pool' => 'Pool',
'fenced_yard' => 'Fenced Yard',
'sprinkler_system' => 'Sprinkler System',
'smart_home' => 'Smart Home Features',
'solar_panels' => 'Solar Panels',
'new_roof' => 'New Roof',
'new_windows' => 'New Windows',
'waterfront' => 'Waterfront',
'lake_access' => 'Lake Access',
),
'layout' => 'horizontal',
),
// Gallery Tab - ACF Pro Gallery field
array(
'key' => 'field_property_tab_gallery',
'label' => 'Photo Gallery',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_gallery',
'label' => 'Property Gallery',
'name' => 'property_gallery',
'type' => 'gallery',
'instructions' => 'Upload or select property photos. First image will be the main photo if no featured image is set.',
'min' => 0,
'max' => 30,
'return_format' => 'id',
'preview_size' => 'medium',
'library' => 'all',
'mime_types' => 'jpg, jpeg, png, webp',
),
// Documents Tab - ACF Pro Repeater
array(
'key' => 'field_property_tab_documents',
'label' => 'Documents',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_documents',
'label' => 'Property Documents',
'name' => 'property_documents',
'type' => 'repeater',
'instructions' => 'Upload supporting documents (floor plans, disclosures, surveys, etc.)',
'min' => 0,
'max' => 10,
'layout' => 'table',
'button_label' => 'Add Document',
'sub_fields' => array(
array(
'key' => 'field_document_file',
'label' => 'File',
'name' => 'file',
'type' => 'file',
'required' => 1,
'return_format' => 'array',
'library' => 'all',
'mime_types' => 'pdf, doc, docx, xls, xlsx',
),
array(
'key' => 'field_document_label',
'label' => 'Button Label',
'name' => 'label',
'type' => 'text',
'required' => 1,
'instructions' => 'e.g., "Floor Plan", "Property Survey"',
),
),
),
// Agent Tab
array(
'key' => 'field_property_tab_agent',
'label' => 'Listing Agent',
'name' => '',
'type' => 'tab',
'placement' => 'top',
),
array(
'key' => 'field_property_listing_agent',
'label' => 'Listing Agent',
'name' => 'listing_agent',
'type' => 'post_object',
'instructions' => 'Select the listing agent for this property. Leave empty to show generic contact information.',
'post_type' => array('agent'),
'allow_null' => 1,
'multiple' => 0,
'return_format' => 'id',
'ui' => 1,
),
),
'location' => array(
array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'property',
),
),
),
'menu_order' => 0,
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
'active' => true,
));
// Theme Options Field Group (for global settings)
acf_add_local_field_group(array(
'key' => 'group_theme_options',
@@ -1739,46 +1455,27 @@ function homeproz_register_acf_fields() {
));
// ===========================================
// MLS OVERRIDE FIELD GROUP
// BLOG POST AUTHOR FIELD GROUP
// ===========================================
// MLS Override Fields
// Blog Post Author (Agent) Field
acf_add_local_field_group(array(
'key' => 'group_mls_override',
'title' => 'MLS Override Settings',
'key' => 'group_post_author',
'title' => 'Post Author',
'fields' => array(
array(
'key' => 'field_mls_override_id',
'label' => 'MLS ID',
'name' => 'mls_override_id',
'type' => 'text',
'instructions' => 'Enter the MLS listing ID (the number shown on the property, e.g., "12345678"). This is NOT the listing key.',
'required' => 1,
'placeholder' => 'e.g., 12345678',
),
array(
'key' => 'field_mls_override_featured_photo',
'label' => 'Featured Photo',
'name' => 'mls_override_featured_photo',
'type' => 'image',
'instructions' => 'Upload a custom featured photo to use instead of the MLS photo. This will appear on property cards and as the first image in the gallery.',
'key' => 'field_post_agent_author',
'label' => 'Author',
'name' => 'post_agent_author',
'type' => 'post_object',
'instructions' => 'Select the agent who authored this post. Only active agents are shown.',
'required' => 0,
'return_format' => 'array',
'preview_size' => 'medium',
'library' => 'all',
'mime_types' => 'jpg, jpeg, png, webp',
),
array(
'key' => 'field_mls_override_featured',
'label' => 'Featured Property',
'name' => 'mls_override_featured',
'type' => 'true_false',
'instructions' => 'Check to feature this property on the homepage rotation and prioritize it in property listings.',
'required' => 0,
'default_value' => 0,
'post_type' => array('agent'),
'taxonomy' => '',
'allow_null' => 1,
'multiple' => 0,
'return_format' => 'id',
'ui' => 1,
'ui_on_text' => 'Featured',
'ui_off_text' => '',
),
),
'location' => array(
@@ -1786,15 +1483,15 @@ function homeproz_register_acf_fields() {
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'mls_override',
'value' => 'post',
),
),
),
'menu_order' => 0,
'position' => 'normal',
'menu_order' => 5,
'position' => 'side',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
'instruction_placement' => 'field',
'active' => true,
));
@@ -1807,6 +1504,17 @@ function homeproz_register_acf_fields() {
'key' => 'group_about_page',
'title' => 'About Page Content',
'fields' => array(
// Additional Content Section (below story)
array(
'key' => 'field_about_additional_content',
'label' => 'Additional Content',
'name' => 'about_additional_content',
'type' => 'wysiwyg',
'instructions' => 'Content displayed in a separate section below the main story section. The main page content (above) is shown next to the featured image.',
'tabs' => 'all',
'toolbar' => 'full',
'media_upload' => 1,
),
// Team Section
array(
'key' => 'field_about_team_title',
@@ -2113,6 +1821,33 @@ function homeproz_register_acf_options_page() {
}
add_action('acf/init', 'homeproz_register_acf_options_page');
/**
* Filter post_object field to only show active (non-disabled) agents
*/
function homeproz_filter_agent_post_object($args, $field, $post_id) {
// Only filter the post_agent_author field
if ($field['key'] !== 'field_post_agent_author') {
return $args;
}
// Add meta query to exclude disabled agents
$args['meta_query'] = array(
'relation' => 'OR',
array(
'key' => 'agent_disabled',
'compare' => 'NOT EXISTS',
),
array(
'key' => 'agent_disabled',
'value' => '1',
'compare' => '!=',
),
);
return $args;
}
add_filter('acf/fields/post_object/query', 'homeproz_filter_agent_post_object', 10, 3);
/**
* Enable deep-linking to ACF tabs via URL parameter
* Usage: /wp-admin/admin.php?page=theme-options&tab=properties_page
@@ -10,6 +10,7 @@ if (!defined('ABSPATH')) {
exit;
}
/**
* Register Agent Custom Post Type
*/
@@ -73,186 +74,11 @@ function homeproz_enable_agent_sorting($sortable, $post_type) {
}
add_filter('simple_page_ordering_is_sortable', 'homeproz_enable_agent_sorting', 10, 2);
/**
* Register MLS Override Custom Post Type
*
* Allows CMS users to override MLS property settings (featured photo, etc.)
*/
function homeproz_register_mls_override_cpt() {
$labels = array(
'name' => _x('MLS Editor', 'Post type general name', 'homeproz'),
'singular_name' => _x('MLS Override', 'Post type singular name', 'homeproz'),
'menu_name' => _x('MLS Editor', 'Admin Menu text', 'homeproz'),
'name_admin_bar' => _x('MLS Override', 'Add New on Toolbar', 'homeproz'),
'add_new' => __('Add New', 'homeproz'),
'add_new_item' => __('Add New MLS Override', 'homeproz'),
'new_item' => __('New MLS Override', 'homeproz'),
'edit_item' => __('Edit MLS Override', 'homeproz'),
'view_item' => __('View MLS Override', 'homeproz'),
'all_items' => __('All Overrides', 'homeproz'),
'search_items' => __('Search Overrides', 'homeproz'),
'not_found' => __('No overrides found.', 'homeproz'),
'not_found_in_trash' => __('No overrides found in Trash.', 'homeproz'),
);
$args = array(
'labels' => $labels,
'public' => false,
'publicly_queryable' => false,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => false,
'rewrite' => false,
'capability_type' => 'post',
'has_archive' => false,
'hierarchical' => false,
'menu_position' => 7,
'menu_icon' => 'dashicons-edit-page',
'supports' => array('title'),
'show_in_rest' => false,
);
register_post_type('mls_override', $args);
}
add_action('init', 'homeproz_register_mls_override_cpt');
/**
* Add admin columns for MLS Override CPT
*/
function homeproz_mls_override_admin_columns($columns) {
$new_columns = array();
foreach ($columns as $key => $value) {
$new_columns[$key] = $value;
if ($key === 'title') {
$new_columns['mls_id'] = __('MLS ID', 'homeproz');
$new_columns['featured_photo'] = __('Featured Photo', 'homeproz');
}
}
return $new_columns;
}
add_filter('manage_mls_override_posts_columns', 'homeproz_mls_override_admin_columns');
/**
* Populate admin columns for MLS Override CPT
*/
function homeproz_mls_override_admin_column_content($column, $post_id) {
switch ($column) {
case 'mls_id':
$mls_id = get_field('mls_override_id', $post_id);
echo $mls_id ? esc_html($mls_id) : '&mdash;';
break;
case 'featured_photo':
$photo = get_field('mls_override_featured_photo', $post_id);
if ($photo && isset($photo['sizes']['thumbnail'])) {
echo '<img src="' . esc_url($photo['sizes']['thumbnail']) . '" width="50" height="50" style="object-fit: cover; border-radius: 4px;">';
} else {
echo '&mdash;';
}
break;
}
}
add_action('manage_mls_override_posts_custom_column', 'homeproz_mls_override_admin_column_content', 10, 2);
/**
* Make MLS ID column sortable
*/
function homeproz_mls_override_sortable_columns($columns) {
$columns['mls_id'] = 'mls_id';
return $columns;
}
add_filter('manage_edit-mls_override_sortable_columns', 'homeproz_mls_override_sortable_columns');
/**
* Get MLS override data for a given MLS ID
*
* @param string $mls_id The MLS listing ID (e.g., "12345678")
* @return array|null Override data or null if not found
*/
function homeproz_get_mls_override($mls_id) {
static $cache = array();
if (!$mls_id) {
return null;
}
// Check cache first
if (isset($cache[$mls_id])) {
return $cache[$mls_id];
}
// Query for override by MLS ID
$overrides = get_posts(array(
'post_type' => 'mls_override',
'posts_per_page' => 1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'mls_override_id',
'value' => $mls_id,
'compare' => '=',
),
),
));
if (empty($overrides)) {
$cache[$mls_id] = null;
return null;
}
$override_post = $overrides[0];
$override_data = array(
'post_id' => $override_post->ID,
'mls_id' => $mls_id,
'featured_photo' => get_field('mls_override_featured_photo', $override_post->ID),
'featured' => (bool) get_field('mls_override_featured', $override_post->ID),
);
$cache[$mls_id] = $override_data;
return $override_data;
}
/**
* Get all MLS IDs that are marked as featured
*
* @return array Array of MLS IDs (listing_id format, e.g., "12345678")
*/
function homeproz_get_featured_mls_ids() {
static $cached_ids = null;
if ($cached_ids !== null) {
return $cached_ids;
}
$overrides = get_posts(array(
'post_type' => 'mls_override',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'mls_override_featured',
'value' => '1',
'compare' => '=',
),
),
));
$cached_ids = array();
foreach ($overrides as $override) {
$mls_id = get_field('mls_override_id', $override->ID);
if ($mls_id) {
$cached_ids[] = $mls_id;
}
}
return $cached_ids;
}
/**
* Flush rewrite rules on theme activation
*/
function homeproz_rewrite_flush() {
homeproz_register_agent_cpt();
homeproz_register_mls_override_cpt();
flush_rewrite_rules();
}
add_action('after_switch_theme', 'homeproz_rewrite_flush');
View File
@@ -0,0 +1,359 @@
<?php
/**
* Social Sharing Meta Tags Framework
*
* Centralized system for Open Graph and Twitter Card meta tags.
* Uses a fallback chain: Page-specific -> WordPress defaults -> Site defaults
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Global storage for social meta overrides and computed values
*/
global $homeproz_social_meta, $homeproz_social_wp_defaults;
$homeproz_social_meta = array();
$homeproz_social_wp_defaults = array();
/**
* Set social meta override for current page
*
* Call this from your template before wp_head() to override specific fields.
* Any fields not set will use the fallback chain.
*
* @param string $key Field name: 'title', 'description', 'image', 'url', 'type'
* @param string $value The value to set
*/
function homeproz_set_social_meta($key, $value) {
global $homeproz_social_meta;
$homeproz_social_meta[$key] = $value;
}
/**
* Set multiple social meta fields at once
*
* @param array $meta Associative array of field => value pairs
*/
function homeproz_set_social_meta_array($meta) {
global $homeproz_social_meta;
$homeproz_social_meta = array_merge($homeproz_social_meta, $meta);
}
/**
* Compute WordPress context defaults early (during template_redirect)
* This ensures is_singular(), get_queried_object() etc. work correctly.
*/
function homeproz_compute_wp_context_defaults() {
global $homeproz_social_wp_defaults;
$meta = array();
// Singular content (posts, pages, CPTs)
if (is_singular()) {
$post = get_queried_object();
if ($post && isset($post->ID)) {
$post_id = $post->ID;
// Title
$meta['title'] = get_the_title($post_id) . ' - ' . get_bloginfo('name');
// Description from excerpt or content
if (has_excerpt($post_id)) {
$meta['description'] = get_the_excerpt($post_id);
} elseif (!empty($post->post_content)) {
$meta['description'] = wp_trim_words(strip_shortcodes($post->post_content), 30, '...');
}
// Featured image
if (has_post_thumbnail($post_id)) {
$image = wp_get_attachment_image_src(get_post_thumbnail_id($post_id), 'large');
if ($image) {
$meta['image'] = $image[0];
}
}
// Type
$meta['type'] = 'article';
}
}
// Homepage - let defaults handle title/description
if (is_front_page()) {
$meta['type'] = 'website';
unset($meta['title'], $meta['description']);
}
// Archives
if (is_archive()) {
if (is_post_type_archive()) {
$post_type = get_queried_object();
if ($post_type) {
$meta['title'] = post_type_archive_title('', false) . ' - ' . get_bloginfo('name');
}
} elseif (is_category() || is_tag() || is_tax()) {
$term = get_queried_object();
if ($term) {
$meta['title'] = $term->name . ' - ' . get_bloginfo('name');
if (!empty($term->description)) {
$meta['description'] = $term->description;
}
}
}
}
// Store computed values
$homeproz_social_wp_defaults = array_filter($meta);
}
add_action('template_redirect', 'homeproz_compute_wp_context_defaults', 1);
/**
* Get the complete social meta data for current page
*
* Builds the final meta array using the fallback chain:
* 1. Explicit overrides (via homeproz_set_social_meta)
* 2. WordPress context (computed during template_redirect)
* 3. Site defaults (site name, tagline, hero image)
*
* @return array Complete social meta data
*/
function homeproz_get_social_meta() {
global $homeproz_social_meta, $homeproz_social_wp_defaults;
// Start with site-wide defaults
$defaults = homeproz_get_social_defaults();
// Layer pre-computed WordPress context defaults
$wp_defaults = $homeproz_social_wp_defaults ?: array();
// Merge: overrides > wp context > site defaults
$meta = array_merge($defaults, $wp_defaults, array_filter($homeproz_social_meta));
// Always set URL to current page unless explicitly overridden
if (empty($homeproz_social_meta['url'])) {
$meta['url'] = homeproz_get_current_url();
}
// Truncate description to ~160 chars for social
if (!empty($meta['description'])) {
$meta['description'] = homeproz_truncate_social_description($meta['description']);
}
// Ensure image is absolute URL
if (!empty($meta['image']) && strpos($meta['image'], 'http') !== 0) {
$meta['image'] = home_url($meta['image']);
}
return $meta;
}
/**
* Get site-wide default social meta (fallback for all pages)
*
* @return array Default meta values
*/
function homeproz_get_social_defaults() {
$site_name = get_bloginfo('name');
$tagline = get_bloginfo('description');
return array(
'title' => $site_name . ($tagline ? ' - ' . $tagline : ''),
'description' => $tagline ?: 'Expert real estate services in Albert Lea and surrounding Minnesota communities.',
'image' => get_template_directory_uri() . '/assets/images/hero.webp',
'url' => home_url('/'),
'type' => 'website',
'site_name' => $site_name,
);
}
/**
* Get the current page URL (handles query strings)
*
* @return string Current URL
*/
function homeproz_get_current_url() {
// Use REQUEST_URI for most accurate URL including query strings
if (!empty($_SERVER['REQUEST_URI'])) {
return home_url($_SERVER['REQUEST_URI']);
}
// Fallback to WordPress method
global $wp;
$current_url = home_url($wp->request);
if (!empty($_SERVER['QUERY_STRING'])) {
$current_url .= '?' . $_SERVER['QUERY_STRING'];
}
return $current_url;
}
/**
* Truncate description to social media friendly length
*
* @param string $text Text to truncate
* @param int $length Max length (default 160)
* @return string Truncated text
*/
function homeproz_truncate_social_description($text, $length = 160) {
$text = wp_strip_all_tags($text);
$text = preg_replace('/\s+/', ' ', $text); // Normalize whitespace
if (strlen($text) <= $length) {
return $text;
}
$text = substr($text, 0, $length);
$last_space = strrpos($text, ' ');
if ($last_space !== false) {
$text = substr($text, 0, $last_space);
}
return $text . '...';
}
/**
* Output social meta tags in wp_head
*
* Outputs Open Graph and Twitter Card meta tags.
*/
function homeproz_output_social_meta() {
$meta = homeproz_get_social_meta();
echo "\n<!-- HomeProz Social Sharing -->\n";
// Open Graph tags
if (!empty($meta['title'])) {
echo '<meta property="og:title" content="' . esc_attr($meta['title']) . '" />' . "\n";
}
if (!empty($meta['description'])) {
echo '<meta property="og:description" content="' . esc_attr($meta['description']) . '" />' . "\n";
}
if (!empty($meta['image'])) {
echo '<meta property="og:image" content="' . esc_url($meta['image']) . '" />' . "\n";
}
if (!empty($meta['url'])) {
echo '<meta property="og:url" content="' . esc_url($meta['url']) . '" />' . "\n";
}
if (!empty($meta['type'])) {
echo '<meta property="og:type" content="' . esc_attr($meta['type']) . '" />' . "\n";
}
if (!empty($meta['site_name'])) {
echo '<meta property="og:site_name" content="' . esc_attr($meta['site_name']) . '" />' . "\n";
}
echo '<meta property="og:locale" content="en_US" />' . "\n";
// Twitter Card tags
echo '<meta name="twitter:card" content="summary_large_image" />' . "\n";
if (!empty($meta['title'])) {
echo '<meta name="twitter:title" content="' . esc_attr($meta['title']) . '" />' . "\n";
}
if (!empty($meta['description'])) {
echo '<meta name="twitter:description" content="' . esc_attr($meta['description']) . '" />' . "\n";
}
if (!empty($meta['image'])) {
echo '<meta name="twitter:image" content="' . esc_url($meta['image']) . '" />' . "\n";
}
echo "<!-- / HomeProz Social Sharing -->\n\n";
}
add_action('wp_head', 'homeproz_output_social_meta', 1);
/**
* Disable Yoast SEO's Open Graph and Twitter output to prevent duplicates
*
* We handle all social meta ourselves for consistency across all page types.
*/
function homeproz_disable_yoast_social() {
// Disable Yoast's Open Graph and Twitter presenters entirely
add_filter('wpseo_frontend_presenters', function($presenters) {
return array_filter($presenters, function($presenter) {
$class_name = get_class($presenter);
// Remove all Open Graph and Twitter presenters
if (strpos($class_name, 'Open_Graph') !== false ||
strpos($class_name, 'Twitter') !== false) {
return false;
}
return true;
});
}, 99);
// Also disable Twitter reading time labels
add_filter('wpseo_enhanced_slack_data', '__return_empty_array');
}
add_action('init', 'homeproz_disable_yoast_social');
/**
* Helper: Set social meta for MLS property
*
* Call from single-property-mls.php before get_header()
*
* @param object $property MLS property object
* @param string $image_url Full-size image URL
* @param string $full_address Formatted address
*/
function homeproz_set_mls_property_social_meta($property, $image_url, $full_address) {
$price = !empty($property->list_price) ? '$' . number_format($property->list_price) : '';
$title = $price ? $price . ' - ' . $full_address : $full_address;
$description = '';
if (!empty($property->public_remarks)) {
$description = $property->public_remarks;
} else {
// Build description from specs
$specs = array();
if (!empty($property->bedrooms_total)) {
$specs[] = $property->bedrooms_total . ' bed' . ($property->bedrooms_total != 1 ? 's' : '');
}
if (!empty($property->bathrooms_total)) {
$specs[] = $property->bathrooms_total . ' bath' . ($property->bathrooms_total != 1 ? 's' : '');
}
if (!empty($property->living_area)) {
$specs[] = number_format($property->living_area) . ' sqft';
}
if (!empty($specs)) {
$description = implode(', ', $specs) . ' in ' . ($property->city ?: 'Minnesota');
}
}
homeproz_set_social_meta_array(array(
'title' => $title . ' - HomeProz',
'description' => $description,
'image' => $image_url,
'type' => 'article',
));
}
/**
* Helper: Set social meta for Agent profile
*
* Call from single-agent.php before get_header()
*
* @param int $agent_id Agent post ID
*/
function homeproz_set_agent_social_meta($agent_id) {
$name = get_the_title($agent_id);
$bio = get_field('agent_short_bio', $agent_id);
$image = '';
if (has_post_thumbnail($agent_id)) {
$img = wp_get_attachment_image_src(get_post_thumbnail_id($agent_id), 'large');
if ($img) {
$image = $img[0];
}
}
homeproz_set_social_meta_array(array(
'title' => $name . ' - HomeProz Real Estate Agent',
'description' => $bio ?: 'Meet ' . $name . ', a real estate professional at HomeProz serving Albert Lea and surrounding communities.',
'image' => $image,
'type' => 'profile',
'url' => get_permalink($agent_id),
));
}
@@ -693,6 +693,73 @@ function homeproz_admin_bar_edit_link($wp_admin_bar) {
}
add_action('admin_bar_menu', 'homeproz_admin_bar_edit_link', 80);
/**
* Check if the current request is from a search engine spider/bot
*
* Detects Googlebot, Bingbot, Baiduspider, Yandex, and other common crawlers.
* Used to serve placeholder images to bots while keeping real images for users.
*
* @return bool True if request is from a bot/spider
*/
function homeproz_is_spider() {
if (!isset($_SERVER['HTTP_USER_AGENT'])) {
return false;
}
$user_agent = strtolower($_SERVER['HTTP_USER_AGENT']);
// Common search engine bots and crawlers
$bot_patterns = array(
'googlebot',
'bingbot',
'slurp', // Yahoo
'duckduckbot',
'baiduspider',
'yandexbot',
'sogou',
'exabot',
'facebot', // Facebook
'facebookexternalhit',
'ia_archiver', // Alexa
'mj12bot', // Majestic
'ahrefsbot',
'semrushbot',
'dotbot',
'rogerbot',
'screaming frog',
'sistrix',
'seokicks',
'petalbot', // Huawei/Petal
'applebot',
'twitterbot',
'linkedinbot',
'bot', // Generic bot pattern
'spider', // Generic spider pattern
'crawler', // Generic crawler pattern
'scraper',
);
foreach ($bot_patterns as $pattern) {
if (strpos($user_agent, $pattern) !== false) {
return true;
}
}
return false;
}
/**
* Get placeholder image URL for spider/bot requests
*
* Returns the hero image from the theme as a generic placeholder
* to serve to search engine crawlers instead of actual property images.
*
* @return string Placeholder image URL
*/
function homeproz_get_spider_placeholder_image() {
return get_template_directory_uri() . '/assets/images/hero.webp';
}
/**
* Format property description with smart paragraph breaks and auto-linked URLs
*
+29 -2
View File
@@ -43,6 +43,31 @@ function homeproz_wpcf7_send_agent_copy($contact_form) {
}
add_action('wpcf7_mail_sent', 'homeproz_wpcf7_send_agent_copy');
/**
* Limit reCAPTCHA to pages with contact forms
*
* CF7 loads reCAPTCHA on every page by default. This dequeues
* the scripts on pages that don't have forms.
*/
function homeproz_limit_recaptcha_to_form_pages() {
// Pages that have CF7 forms and need reCAPTCHA
$form_pages = array(
'contact',
'contact-agent',
'property-inquiry',
);
// Check if we're on a page that needs reCAPTCHA
if (is_page($form_pages)) {
return;
}
// Dequeue reCAPTCHA scripts on all other pages
wp_dequeue_script('google-recaptcha');
wp_dequeue_script('wpcf7-recaptcha');
}
add_action('wp_enqueue_scripts', 'homeproz_limit_recaptcha_to_form_pages', 30);
/**
* Send copy of agent contact form to the specific agent
*
@@ -127,8 +152,10 @@ function homeproz_send_property_inquiry_agent_copy($posted_data, $contact_form)
return;
}
// Only send agent copy for HomeProz listings
if (empty($property->is_homeproz) || !$property->is_homeproz) {
// Only send agent copy for HomeProz listings (check office name)
$office_name = $property->list_office_name ?? '';
$is_homeproz_listing = (stripos($office_name, 'HomeProz') !== false || stripos($office_name, 'Home Proz') !== false);
if (!$is_homeproz_listing) {
return;
}
View File
@@ -303,23 +303,6 @@ PAGE HERO FIELDS
Type: text
Default: Thank you for your interest in:
*NEW MLS EDITOR FIELDS
Location: MLS Editor > [Override] > Edit
Allows overriding MLS property settings (featured photo, etc.)
Overrides are matched by MLS ID (listing_id, not listing_key)
mls_override_id MLS Listing ID to override
Type: text
Required: yes
Example: 12345678
mls_override_featured_photo Custom featured photo
Type: image (array)
Required: no
Usage: Replaces MLS photo on cards
and prepends to gallery
PROPERTY FIELDS
Location: Properties > [Property] > Edit
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
+23 -130
View File
@@ -1,7 +1,7 @@
<?php
/**
* Template Name: About Page
* Template for displaying the About/Team page
* Template for displaying the About page
*
* @package HomeProz
*/
@@ -46,13 +46,24 @@ get_header();
$about_image = get_field('about_featured_image');
$about_image_url = $about_image ?: get_template_directory_uri() . '/assets/images/about-us.webp';
?>
<img src="<?php echo esc_url($about_image_url); ?>" alt="HomeProz Real Estate Team">
<img src="<?php echo esc_url($about_image_url); ?>" alt="HomeProz Real Estate">
</div>
<div class="about-story-content">
<?php
while (have_posts()) :
the_post();
the_content();
if (get_the_content()) :
the_content();
else :
// Default content if page body is empty
?>
<h2>Our Story</h2>
<p>HomeProz Real Estate was founded with a simple mission: to provide exceptional real estate services to the Albert Lea area and surrounding Minnesota and Iowa communities. We believe that buying or selling a home should be an exciting journey, not a stressful experience.</p>
<h2>Our Mission</h2>
<p>We are dedicated to helping families find their dream homes and sellers achieve the best possible results. Our team combines deep local knowledge with a commitment to personalized service, ensuring every client receives the attention they deserve.</p>
<p>Whether you're a first-time homebuyer, looking to upgrade, or ready to sell, our experienced agents are here to guide you every step of the way.</p>
<?php
endif;
endwhile;
?>
</div>
@@ -60,137 +71,19 @@ get_header();
</div>
</section>
<!-- Meet the Team Section -->
<section class="about-team-section Agents_Archive">
<?php
// Additional Content Section (from ACF field)
$additional_content = get_field('about_additional_content');
if ($additional_content) :
?>
<section class="about-content-section">
<div class="container">
<?php
$team_title = get_field('about_team_title') ?: 'Meet Our Team';
$team_subtitle = get_field('about_team_subtitle') ?: 'A dedicated group of real estate professionals committed to your success';
?>
<header class="about-team-header">
<h2 class="about-team-title"><?php echo esc_html($team_title); ?></h2>
<p class="about-team-subtitle"><?php echo esc_html($team_subtitle); ?></p>
</header>
<?php
// Query all published agents, ordered by menu_order (drag-drop sortable in admin)
$all_agents = get_posts([
'post_type' => 'agent',
'posts_per_page' => -1,
'post_status' => 'publish',
'orderby' => 'menu_order',
'order' => 'ASC',
]);
// Filter out disabled agents
$agents_data = [];
foreach ($all_agents as $agent) {
$disabled = get_field('agent_disabled', $agent->ID);
if ($disabled) continue;
$agents_data[] = [
'post' => $agent,
];
}
?>
<?php if (!empty($agents_data)) : ?>
<div class="agents-grid">
<?php foreach ($agents_data as $agent_item) :
$agent = $agent_item['post'];
$agent_id = $agent->ID;
$agent_phone = get_field('agent_phone', $agent_id);
$agent_email = get_field('agent_email', $agent_id);
$agent_title = get_field('agent_title', $agent_id);
$agent_short_bio = get_field('agent_short_bio', $agent_id);
$agent_permalink = get_permalink($agent_id);
// Get featured image
$agent_photo_id = get_post_thumbnail_id($agent_id);
$agent_photo_url = $agent_photo_id ? wp_get_attachment_image_url($agent_photo_id, 'medium_large') : '';
?>
<article class="agent-card-item">
<a href="<?php echo esc_url($agent_permalink); ?>" class="agent-card-link">
<div class="agent-card-image">
<?php if ($agent_photo_url) : ?>
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent->post_title); ?>">
<?php else : ?>
<div class="agent-card-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="agent-card-content">
<?php if ($agent_title) : ?>
<p class="agent-card-title-label"><?php echo esc_html($agent_title); ?></p>
<?php endif; ?>
<h3 class="agent-card-name"><?php echo esc_html($agent->post_title); ?></h3>
<?php if ($agent_short_bio) : ?>
<p class="agent-card-bio"><?php echo esc_html($agent_short_bio); ?></p>
<?php endif; ?>
</div>
</a>
<div class="agent-card-actions">
<?php if ($agent_phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $agent_phone)); ?>" class="agent-action-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<span class="sr-only">Call</span>
</a>
<?php endif; ?>
<?php if ($agent_email) : ?>
<a href="mailto:<?php echo esc_attr($agent_email); ?>" class="agent-action-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<span class="sr-only">Email</span>
</a>
<?php endif; ?>
<a href="<?php echo esc_url($agent_permalink); ?>" class="agent-action-btn agent-action-profile">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>Profile</span>
</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else : ?>
<div class="no-agents-message">
<p>No team members to display at this time.</p>
</div>
<?php endif; ?>
</div>
</section>
<!-- Broker Information Section -->
<section class="about-broker-section">
<div class="container">
<?php
$broker_title = get_field('about_broker_title') ?: 'Broker Information';
$broker_text = get_field('about_broker_text') ?: "HomeProz Real Estate LLC DBA LandProz Real Estate, LLC<br>\n111 East Clark Street, Albert Lea, MN 56007<br>\nBroker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21";
?>
<div class="about-broker-content">
<h3 class="about-broker-title"><?php echo esc_html($broker_title); ?></h3>
<div class="about-broker-text">
<?php echo wp_kses_post($broker_text); ?>
</div>
<div class="about-additional-content">
<?php echo wp_kses_post($additional_content); ?>
</div>
</div>
</section>
<?php endif; ?>
<?php
// Contact CTA Section
View File
View File
@@ -31,6 +31,42 @@ if (!$property) {
// Extract property data
$price = $property->list_price;
$listing_id = $property->listing_id;
$agent_mls_id = $property->list_agent_mls_id;
$office_name = $property->list_office_name;
// Check if this is a HomeProz listing
$is_homeproz_listing = false;
if ($office_name) {
$is_homeproz_listing = (stripos($office_name, 'HomeProz') !== false || stripos($office_name, 'Home Proz') !== false);
}
// For HomeProz listings, look up the agent from Agent CPT by MLS ID
$homeproz_agent_id = null;
$homeproz_agent_email = '';
$homeproz_agent_name = '';
if ($is_homeproz_listing && $agent_mls_id) {
$agent_query = new WP_Query(array(
'post_type' => 'agent',
'posts_per_page' => 1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'agent_mls_id',
'value' => $agent_mls_id,
'compare' => '=',
),
),
));
if ($agent_query->have_posts()) {
$homeproz_agent_id = $agent_query->posts[0]->ID;
$homeproz_agent_name = get_the_title($homeproz_agent_id);
$agent_disabled = get_field('agent_disabled', $homeproz_agent_id);
if (!$agent_disabled) {
$homeproz_agent_email = get_field('agent_email', $homeproz_agent_id);
}
}
wp_reset_postdata();
}
// Format address
$address_parts = array();
@@ -62,8 +98,8 @@ if ($property->postal_code) {
// Property URL
$property_url = home_url('/properties/?listing=' . $listing_key);
// Default message
$default_message = "Hello,\n\nI would like to get additional information on property " . $full_address . " (MLS# " . $listing_id . ").";
// Display message (shown to user - no MLS#)
$display_message = "Hello,\n\nI would like to get additional information on property " . $full_address . ".";
get_header();
?>
@@ -104,15 +140,13 @@ get_header();
<input type="hidden" name="property-address" value="<?php echo esc_attr($full_address); ?>">
<input type="hidden" name="property-price" value="<?php echo esc_attr($price); ?>">
<input type="hidden" name="property-url" value="<?php echo esc_attr($property_url); ?>">
<input type="hidden" name="agent-email" value="<?php echo esc_attr($homeproz_agent_email); ?>">
<input type="hidden" name="agent-name" value="<?php echo esc_attr($homeproz_agent_name); ?>">
<input type="hidden" name="default-message" value="">
<div class="form-group">
<label for="inquiry-message">Your Inquiry</label>
<textarea id="inquiry-message" name="default-message" rows="4" readonly class="readonly-message"><?php echo esc_textarea($default_message); ?></textarea>
</div>
<div class="form-group">
<label for="inquiry-comments">Additional Comments</label>
<textarea id="inquiry-comments" name="comments" rows="4" placeholder="Any specific questions or information you'd like to know..."></textarea>
<textarea id="inquiry-message" rows="4" readonly class="readonly-message"><?php echo esc_textarea($display_message); ?></textarea>
</div>
<div class="form-row">
@@ -133,6 +167,11 @@ get_header();
</div>
</div>
<div class="form-group">
<label for="inquiry-comments">Additional Comments</label>
<textarea id="inquiry-comments" name="comments" rows="4" placeholder="Any specific questions or information you'd like to know..."></textarea>
</div>
<button type="submit" class="btn btn-primary btn-lg">Send Inquiry</button>
</form>
<?php
@@ -149,6 +188,13 @@ get_header();
set_query_var('minimal_property', $property);
get_template_part('template-parts/property/property-card-minimal');
?>
<?php if ($homeproz_agent_id) : ?>
<?php
set_query_var('minimal_agent_id', $homeproz_agent_id);
get_template_part('template-parts/property/agent-card-minimal');
?>
<?php endif; ?>
</div>
</div>
</div>
@@ -159,19 +205,33 @@ get_header();
<script>
(function() {
var listingKey = <?php echo json_encode($listing_key); ?>;
var listingId = <?php echo json_encode($listing_id); ?>;
var propertyAddress = <?php echo json_encode($full_address); ?>;
var propertyUrl = <?php echo json_encode($property_url); ?>;
var thankYouUrl = <?php echo json_encode(home_url('/inquiry-thank-you/?listing=' . $listing_key)); ?>;
var defaultMessage = <?php echo json_encode($default_message); ?>;
var displayMessage = <?php echo json_encode($display_message); ?>;
// Build submission message (includes MLS#, URL, and will include user comments)
function buildSubmissionMessage(userComments) {
var msg = "Hello,\n\nI would like to get additional information on property " + propertyAddress + " (MLS# " + listingId + ").\n\n";
msg += propertyUrl + "\n\n";
if (userComments && userComments.trim()) {
msg += "User Comments:\n\n" + userComments.trim();
}
return msg;
}
// Populate CF7 hidden fields if the form exists
var form = document.querySelector('.wpcf7-form');
if (form) {
var fields = {
'listing-key': listingKey,
'listing-id': <?php echo json_encode($listing_id); ?>,
'property-address': <?php echo json_encode($full_address); ?>,
'listing-id': listingId,
'property-address': propertyAddress,
'property-price': <?php echo json_encode(number_format($price)); ?>,
'property-url': <?php echo json_encode($property_url); ?>,
'default-message': defaultMessage
'property-url': propertyUrl,
'agent-email': <?php echo json_encode($homeproz_agent_email); ?>,
'agent-name': <?php echo json_encode($homeproz_agent_name); ?>
};
for (var name in fields) {
@@ -181,14 +241,37 @@ get_header();
}
}
// Populate the display div with the message
var displayDiv = form.querySelector('.readonly-message-display');
if (displayDiv) {
displayDiv.textContent = defaultMessage;
// Populate the display textarea with the user-friendly message
var displayTextarea = form.querySelector('.readonly-message-display, .readonly-message');
if (displayTextarea) {
displayTextarea.textContent = displayMessage;
}
// Build and set the submission message before form submit
form.addEventListener('submit', function() {
var commentsField = form.querySelector('textarea[name="comments"]');
var userComments = commentsField ? commentsField.value : '';
var messageField = form.querySelector('input[name="default-message"]');
if (messageField) {
messageField.value = buildSubmissionMessage(userComments);
}
});
}
// Redirect to thank you page after successful submission
// Handle fallback form submission message building
var fallbackForm = document.querySelector('.property-inquiry-form');
if (fallbackForm && !fallbackForm.classList.contains('wpcf7-form')) {
fallbackForm.addEventListener('submit', function() {
var commentsField = fallbackForm.querySelector('#inquiry-comments');
var userComments = commentsField ? commentsField.value : '';
var messageField = fallbackForm.querySelector('input[name="default-message"]');
if (messageField) {
messageField.value = buildSubmissionMessage(userComments);
}
});
}
// Redirect to thank you page after successful CF7 submission
document.addEventListener('wpcf7mailsent', function(event) {
window.location.href = thankYouUrl;
}, false);
+126
View File
@@ -0,0 +1,126 @@
<?php
/**
* Template Name: Results Page
* Template for displaying sold properties
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
get_header();
// Get hero content from ACF or defaults
$hero_title = get_field('hero_title') ?: 'Our Results';
$hero_subtitle = get_field('hero_subtitle') ?: 'Properties we have successfully sold for our clients.';
$hero_bg = get_field('hero_background');
$has_bg_class = $hero_bg ? 'has-background' : '';
$bg_style = $hero_bg ? 'style="background-image: url(' . esc_url($hero_bg) . ');"' : '';
?>
<main id="primary" class="site-main results-page-main">
<!-- Results Hero -->
<section class="archive-hero <?php echo esc_attr($has_bg_class); ?>" <?php echo $bg_style; ?>>
<?php if ($hero_bg) : ?>
<div class="archive-hero-overlay"></div>
<?php endif; ?>
<div class="container">
<h1 class="archive-hero-title"><?php echo esc_html($hero_title); ?></h1>
<?php if ($hero_subtitle) : ?>
<p class="archive-hero-subtitle"><?php echo esc_html($hero_subtitle); ?></p>
<?php endif; ?>
</div>
</section>
<!-- Sold Properties Grid -->
<section class="results-content-section">
<div class="container">
<?php
// Query sold/closed properties
$sold_properties = array();
if (function_exists('mls_get_properties')) {
// Get Closed status properties (MLS standard for sold)
$closed = mls_get_properties(array(
'status' => 'Closed',
'limit' => 100,
'orderby' => 'close_date',
'order' => 'DESC',
));
if (!empty($closed)) {
$sold_properties = array_merge($sold_properties, $closed);
}
// Also check for 'Sold' status (in case manual entries use this)
$sold = mls_get_properties(array(
'status' => 'Sold',
'limit' => 100,
'orderby' => 'close_date',
'order' => 'DESC',
));
if (!empty($sold)) {
$sold_properties = array_merge($sold_properties, $sold);
}
// Sort combined results by close_date descending
usort($sold_properties, function($a, $b) {
$date_a = $a->close_date ?? $a->modification_timestamp ?? '1970-01-01';
$date_b = $b->close_date ?? $b->modification_timestamp ?? '1970-01-01';
return strtotime($date_b) - strtotime($date_a);
});
}
?>
<?php if (!empty($sold_properties)) : ?>
<div class="results-stats">
<p class="results-count"><?php echo count($sold_properties); ?> <?php echo count($sold_properties) === 1 ? 'property' : 'properties'; ?> sold</p>
</div>
<div class="results-grid">
<?php
foreach ($sold_properties as $property) :
set_query_var('mls_property', $property);
get_template_part('template-parts/property/property-card', 'mls');
endforeach;
?>
</div>
<?php else : ?>
<div class="no-results-message">
<h2>No Sold Properties Yet</h2>
<p>Check back soon to see our successful sales. In the meantime, browse our current listings.</p>
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-primary">
View Current Listings
<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 endif; ?>
</div>
</section>
<?php
// Contact CTA Section
$cta_title = get_field('cta_title') ?: 'Ready to Sell Your Property?';
$cta_text = get_field('cta_text') ?: 'Our team has a proven track record of successful sales. Let us help you achieve the best results.';
$cta_button_text = get_field('cta_button_text') ?: 'Get a Free Valuation';
$cta_button_url = get_field('cta_button_url') ?: home_url('/contact/');
get_template_part('template-parts/components/cta-section', null, array(
'title' => $cta_title,
'text' => $cta_text,
'button_text' => $cta_button_text,
'button_url' => $cta_button_url,
'style' => 'accent',
));
?>
</main>
<?php
get_footer();
+181
View File
@@ -0,0 +1,181 @@
<?php
/**
* Template Name: Team Page
* Template for displaying the Team/Agents page
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
get_header();
?>
<main id="primary" class="site-main team-page-main">
<?php
// Get hero content from ACF or defaults
$hero_title = get_field('hero_title') ?: 'Our Team';
$hero_subtitle = get_field('hero_subtitle') ?: 'A dedicated group of real estate professionals committed to your success.';
$hero_bg = get_field('hero_background');
$has_bg_class = $hero_bg ? 'has-background' : '';
$bg_style = $hero_bg ? 'style="background-image: url(' . esc_url($hero_bg) . ');"' : '';
?>
<!-- Archive Hero -->
<section class="archive-hero <?php echo esc_attr($has_bg_class); ?>" <?php echo $bg_style; ?>>
<?php if ($hero_bg) : ?>
<div class="archive-hero-overlay"></div>
<?php endif; ?>
<div class="container">
<h1 class="archive-hero-title"><?php echo esc_html($hero_title); ?></h1>
<?php if ($hero_subtitle) : ?>
<p class="archive-hero-subtitle"><?php echo esc_html($hero_subtitle); ?></p>
<?php endif; ?>
</div>
</section>
<!-- Meet the Team Section -->
<section class="team-agents-section Agents_Archive">
<div class="container">
<?php
// Query all published agents, ordered by menu_order (drag-drop sortable in admin)
$all_agents = get_posts([
'post_type' => 'agent',
'posts_per_page' => -1,
'post_status' => 'publish',
'orderby' => 'menu_order',
'order' => 'ASC',
]);
// Filter out disabled agents
$agents_data = [];
foreach ($all_agents as $agent) {
$disabled = get_field('agent_disabled', $agent->ID);
if ($disabled) continue;
$agents_data[] = [
'post' => $agent,
];
}
?>
<?php if (!empty($agents_data)) : ?>
<div class="agents-grid">
<?php foreach ($agents_data as $agent_item) :
$agent = $agent_item['post'];
$agent_id = $agent->ID;
$agent_phone = get_field('agent_phone', $agent_id);
$agent_email = get_field('agent_email', $agent_id);
$agent_title = get_field('agent_title', $agent_id);
$agent_short_bio = get_field('agent_short_bio', $agent_id);
$agent_permalink = get_permalink($agent_id);
// Get featured image
$agent_photo_id = get_post_thumbnail_id($agent_id);
$agent_photo_url = $agent_photo_id ? wp_get_attachment_image_url($agent_photo_id, 'medium_large') : '';
?>
<article class="agent-card-item">
<a href="<?php echo esc_url($agent_permalink); ?>" class="agent-card-link">
<div class="agent-card-image">
<?php if ($agent_photo_url) : ?>
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent->post_title); ?>">
<?php else : ?>
<div class="agent-card-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="agent-card-content">
<?php if ($agent_title) : ?>
<p class="agent-card-title-label"><?php echo esc_html($agent_title); ?></p>
<?php endif; ?>
<h3 class="agent-card-name"><?php echo esc_html($agent->post_title); ?></h3>
<?php if ($agent_short_bio) : ?>
<p class="agent-card-bio"><?php echo esc_html($agent_short_bio); ?></p>
<?php endif; ?>
</div>
</a>
<div class="agent-card-actions">
<?php if ($agent_phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $agent_phone)); ?>" class="agent-action-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<span class="sr-only">Call</span>
</a>
<?php endif; ?>
<?php if ($agent_email) : ?>
<a href="mailto:<?php echo esc_attr($agent_email); ?>" class="agent-action-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
<span class="sr-only">Email</span>
</a>
<?php endif; ?>
<a href="<?php echo esc_url($agent_permalink); ?>" class="agent-action-btn agent-action-profile">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<span>Profile</span>
</a>
</div>
</article>
<?php endforeach; ?>
</div>
<?php else : ?>
<div class="no-agents-message">
<p>No team members to display at this time.</p>
</div>
<?php endif; ?>
</div>
</section>
<!-- Broker Information Section -->
<section class="team-broker-section">
<div class="container">
<?php
$broker_title = get_field('broker_title') ?: 'Broker Information';
$broker_text = get_field('broker_text') ?: "HomeProz Real Estate LLC DBA LandProz Real Estate, LLC<br>\n111 East Clark Street, Albert Lea, MN 56007<br>\nBroker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21";
?>
<div class="team-broker-content">
<h3 class="team-broker-title"><?php echo esc_html($broker_title); ?></h3>
<div class="team-broker-text">
<?php echo wp_kses_post($broker_text); ?>
</div>
</div>
</div>
</section>
<?php
// Contact CTA Section
$cta_title = get_field('cta_title') ?: 'Ready to Work With Us?';
$cta_text = get_field('cta_text') ?: 'Contact our team today to start your real estate journey.';
$cta_button_text = get_field('cta_button_text') ?: 'Get in Touch';
$cta_button_url = get_field('cta_button_url') ?: home_url('/contact/');
get_template_part('template-parts/components/cta-section', null, array(
'title' => $cta_title,
'text' => $cta_text,
'button_text' => $cta_button_text,
'button_url' => $cta_button_url,
'style' => 'accent',
));
?>
</main>
<?php
get_footer();
+27 -20
View File
@@ -21,6 +21,12 @@ if (have_posts()) {
get_template_part('404');
exit;
}
// Set social sharing meta for agent profile
if (function_exists('homeproz_set_agent_social_meta')) {
homeproz_set_agent_social_meta(get_the_ID());
}
rewind_posts();
}
@@ -212,38 +218,39 @@ while (have_posts()) :
}
?>
<!-- Agent's Listings -->
<!-- Agent's MLS Listings -->
<?php
// Query properties assigned to this agent
$agent_properties = new WP_Query([
'post_type' => 'property',
'posts_per_page' => 6,
'meta_query' => [
[
'key' => 'listing_agent',
'value' => $agent_id,
'compare' => '=',
],
],
]);
$agent_mls_id = get_field('agent_mls_id', $agent_id);
if ($agent_mls_id && function_exists('mls_get_properties')) :
$agent_listings = mls_get_properties(array(
'agent_mls_id' => $agent_mls_id,
'status' => array('Active', 'Pending'),
'limit' => 6,
'orderby' => 'modification_timestamp',
'order' => 'DESC',
));
if ($agent_properties->have_posts()) :
if (!empty($agent_listings)) :
$listing_count = count($agent_listings);
$few_listings_class = $listing_count <= 2 ? 'few-listings' : '';
?>
<section class="agent-section agent-listings">
<h2 class="section-title">Current Listings</h2>
<div class="agent-listings-grid">
<?php while ($agent_properties->have_posts()) : $agent_properties->the_post();
get_template_part('template-parts/property/property-card');
endwhile; ?>
<div class="agent-listings-grid <?php echo esc_attr($few_listings_class); ?>" data-listing-count="<?php echo esc_attr($listing_count); ?>">
<?php foreach ($agent_listings as $listing) :
set_query_var('agent_listing', $listing);
get_template_part('template-parts/property/property-card-agent');
endforeach; ?>
</div>
<div class="agent-listings-cta">
<a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-secondary">View All Properties</a>
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-secondary">View All Properties</a>
</div>
</section>
<?php
wp_reset_postdata();
endif;
endif;
?>
</div>
<!-- Sidebar -->
@@ -1,268 +0,0 @@
<?php
/**
* Single Property Template
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
get_header();
while (have_posts()) :
the_post();
$property_id = get_the_ID();
// Get property data
$price = get_field('property_price', $property_id);
$street_address = get_field('street_address', $property_id);
$city = get_field('city', $property_id);
$state = get_field('state', $property_id);
$zip_code = get_field('zip_code', $property_id);
$mls_number = get_field('mls_number', $property_id);
$bedrooms = get_field('bedrooms', $property_id);
$bathrooms = get_field('bathrooms', $property_id);
$square_feet = get_field('square_feet', $property_id);
$lot_size = get_field('lot_size', $property_id);
$year_built = get_field('year_built', $property_id);
$garage = get_field('garage', $property_id);
$short_description = get_field('short_description', $property_id);
$property_features = get_field('property_features', $property_id);
$gallery = get_field('property_gallery', $property_id);
$listing_agent = get_field('listing_agent', $property_id);
$property_documents = get_field('property_documents', $property_id);
// Get status from taxonomy
$status_terms = get_the_terms($property_id, 'property_status');
$status = $status_terms && !is_wp_error($status_terms) ? $status_terms[0]->name : '';
$status_class = homeproz_get_status_class($status);
// Get type from taxonomy
$type_terms = get_the_terms($property_id, 'property_type');
$type = $type_terms && !is_wp_error($type_terms) ? $type_terms[0]->name : '';
// Format full address
$full_address = $street_address;
if ($city) $full_address .= ', ' . $city;
if ($state) $full_address .= ', ' . $state;
if ($zip_code) $full_address .= ' ' . $zip_code;
?>
<main id="primary" class="site-main single-property-main">
<div class="container">
<!-- Property Address Header -->
<header class="property-address-header">
<h1 class="property-address-title"><?php echo esc_html($full_address ?: get_the_title()); ?></h1>
</header>
<div class="single-property-layout">
<!-- Main Content -->
<div class="single-property-content">
<!-- Gallery -->
<?php get_template_part('template-parts/property/property-gallery', null, array('gallery' => $gallery, 'property_id' => $property_id)); ?>
<!-- Property Specs -->
<?php if ($bedrooms || $bathrooms || $square_feet || $lot_size || $year_built || $garage) : ?>
<section class="property-specs-section">
<h2 class="section-title">Property Details</h2>
<ul class="property-specs-grid">
<?php if ($bedrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bedrooms</span>
<span class="spec-value"><?php echo esc_html($bedrooms); ?></span>
</li>
<?php endif; ?>
<?php if ($bathrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bathrooms</span>
<span class="spec-value"><?php echo esc_html($bathrooms); ?></span>
</li>
<?php endif; ?>
<?php if ($square_feet) : ?>
<li class="spec-item">
<span class="spec-label">Square Feet</span>
<span class="spec-value"><?php echo esc_html(number_format($square_feet)); ?></span>
</li>
<?php endif; ?>
<?php if ($lot_size) : ?>
<li class="spec-item">
<span class="spec-label">Lot Size</span>
<span class="spec-value"><?php echo esc_html($lot_size); ?></span>
</li>
<?php endif; ?>
<?php if ($year_built) : ?>
<li class="spec-item">
<span class="spec-label">Year Built</span>
<span class="spec-value"><?php echo esc_html($year_built); ?></span>
</li>
<?php endif; ?>
<?php if ($garage) : ?>
<li class="spec-item">
<span class="spec-label">Garage</span>
<span class="spec-value"><?php echo esc_html($garage); ?> Car</span>
</li>
<?php endif; ?>
</ul>
</section>
<?php endif; ?>
<!-- Documents -->
<?php if ($property_documents && is_array($property_documents)) : ?>
<section class="property-documents">
<ul class="documents-list">
<?php foreach ($property_documents as $doc) :
if (!$doc['file']) continue;
$file = $doc['file'];
$label = $doc['label'] ?: $file['filename'];
$ext = pathinfo($file['filename'], PATHINFO_EXTENSION);
?>
<li class="document-item">
<a href="<?php echo esc_url($file['url']); ?>" class="document-link btn btn-secondary" target="_blank" download>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="12" y1="18" x2="12" y2="12"/>
<polyline points="9 15 12 18 15 15"/>
</svg>
<?php echo esc_html($label); ?>
<span class="document-ext">(<?php echo esc_html(strtoupper($ext)); ?>)</span>
</a>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
<!-- Description -->
<section class="property-description">
<h2 class="section-title">Description</h2>
<?php if ($short_description) : ?>
<p class="property-short-desc"><?php echo esc_html($short_description); ?></p>
<?php endif; ?>
<div class="property-full-desc">
<?php the_content(); ?>
</div>
</section>
<!-- Features -->
<?php if ($property_features && is_array($property_features)) : ?>
<section class="property-features">
<h2 class="section-title">Features & Amenities</h2>
<ul class="features-list">
<?php
$feature_labels = array(
'central_air' => 'Central Air',
'central_heat' => 'Central Heat',
'fireplace' => 'Fireplace',
'hardwood_floors' => 'Hardwood Floors',
'updated_kitchen' => 'Updated Kitchen',
'updated_bathrooms' => 'Updated Bathrooms',
'basement' => 'Basement',
'finished_basement' => 'Finished Basement',
'deck_patio' => 'Deck/Patio',
'pool' => 'Pool',
'fenced_yard' => 'Fenced Yard',
'sprinkler_system' => 'Sprinkler System',
'smart_home' => 'Smart Home Features',
'solar_panels' => 'Solar Panels',
'new_roof' => 'New Roof',
'new_windows' => 'New Windows',
'waterfront' => 'Waterfront',
'lake_access' => 'Lake Access',
);
foreach ($property_features as $feature) :
$label = isset($feature_labels[$feature]) ? $feature_labels[$feature] : ucwords(str_replace('_', ' ', $feature));
?>
<li class="feature-item">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true">
<polyline points="20 6 9 17 4 12"/>
</svg>
<?php echo esc_html($label); ?>
</li>
<?php endforeach; ?>
</ul>
</section>
<?php endif; ?>
</div>
<!-- Sidebar -->
<aside class="single-property-sidebar">
<!-- Property Header -->
<div class="sidebar-widget property-header-widget">
<div class="property-header-top">
<?php if ($type) : ?>
<?php
$type_slug = sanitize_title($type);
$type_class = 'badge-type-' . $type_slug;
?>
<span class="badge <?php echo esc_attr($type_class); ?>"><?php echo esc_html($type); ?></span>
<?php endif; ?>
<?php if ($status) : ?>
<span class="badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html($status); ?></span>
<?php endif; ?>
</div>
<div class="property-title"><?php echo esc_html(homeproz_format_price($price)); ?></div>
<?php if ($mls_number) : ?>
<p class="property-mls">MLS# <?php echo esc_html($mls_number); ?></p>
<?php endif; ?>
</div>
<!-- Property Documents -->
<div class="sidebar-widget property-documents-widget">
<h3 class="widget-title">Property Documents (SAMPLE)</h3>
<?php if ($property_documents && is_array($property_documents) && !empty($property_documents)) : ?>
<div class="sidebar-documents-list">
<?php foreach ($property_documents as $doc) :
if (!$doc['file']) continue;
$file = $doc['file'];
$label = $doc['label'] ?: $file['filename'];
$ext = strtoupper(pathinfo($file['filename'], PATHINFO_EXTENSION));
?>
<a href="<?php echo esc_url($file['url']); ?>" class="document-btn" target="_blank">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<span class="document-btn-label"><?php echo esc_html($label); ?></span>
<span class="document-btn-ext"><?php echo esc_html($ext); ?></span>
</a>
<?php endforeach; ?>
</div>
<?php else : ?>
<!-- Mock documents for display -->
<div class="sidebar-documents-list">
<a href="#" class="document-btn" onclick="return false;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<span class="document-btn-label">Property Disclosure</span>
<span class="document-btn-ext">PDF</span>
</a>
<a href="#" class="document-btn" onclick="return false;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
</svg>
<span class="document-btn-label">Floor Plan</span>
<span class="document-btn-ext">PDF</span>
</a>
</div>
<?php endif; ?>
</div>
<!-- Contact Agent -->
<?php get_template_part('template-parts/property/property-agent', null, array('agent' => $listing_agent, 'property_id' => $property_id)); ?>
</aside>
</div>
</div>
</main>
<?php
endwhile;
get_footer();
+127 -16
View File
@@ -30,17 +30,6 @@ get_header();
while (have_posts()) :
the_post();
get_template_part('template-parts/content/content', 'single');
// Post navigation
the_post_navigation(array(
'prev_text' => '<span class="nav-subtitle"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"/></svg> Previous</span><span class="nav-title">%title</span>',
'next_text' => '<span class="nav-subtitle">Next <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><span class="nav-title">%title</span>',
));
// Comments
if (comments_open() || get_comments_number()) :
comments_template();
endif;
endwhile;
?>
</article>
@@ -53,10 +42,81 @@ get_header();
</div>
</section>
<!-- Related Posts -->
<!-- Enhanced Post Navigation -->
<?php
$prev_post = get_previous_post();
$next_post = get_next_post();
if ($prev_post || $next_post) :
?>
<section class="post-nav-section">
<div class="container">
<div class="post-nav-grid">
<?php if ($prev_post) : ?>
<a href="<?php echo get_permalink($prev_post); ?>" class="post-nav-item post-nav-prev">
<div class="post-nav-thumb">
<?php if (has_post_thumbnail($prev_post)) : ?>
<?php echo get_the_post_thumbnail($prev_post, 'thumbnail'); ?>
<?php else : ?>
<div class="post-nav-placeholder">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="post-nav-content">
<span class="post-nav-label">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Previous
</span>
<span class="post-nav-title"><?php echo esc_html(get_the_title($prev_post)); ?></span>
</div>
</a>
<?php else : ?>
<div class="post-nav-item post-nav-empty"></div>
<?php endif; ?>
<?php if ($next_post) : ?>
<a href="<?php echo get_permalink($next_post); ?>" class="post-nav-item post-nav-next">
<div class="post-nav-content">
<span class="post-nav-label">
Next
<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>
<span class="post-nav-title"><?php echo esc_html(get_the_title($next_post)); ?></span>
</div>
<div class="post-nav-thumb">
<?php if (has_post_thumbnail($next_post)) : ?>
<?php echo get_the_post_thumbnail($next_post, 'thumbnail'); ?>
<?php else : ?>
<div class="post-nav-placeholder">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<path d="M21 15l-5-5L5 21"/>
</svg>
</div>
<?php endif; ?>
</div>
</a>
<?php else : ?>
<div class="post-nav-item post-nav-empty"></div>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
<!-- Related Posts Section -->
<?php
// Try to get related posts by category first
$categories = get_the_category();
if ($categories) :
$related_posts = null;
if ($categories) {
$category_ids = array();
foreach ($categories as $cat) {
$category_ids[] = $cat->term_id;
@@ -67,15 +127,28 @@ get_header();
'posts_per_page' => 3,
'post__not_in' => array(get_the_ID()),
'category__in' => $category_ids,
'orderby' => 'rand',
'orderby' => 'date',
'order' => 'DESC',
));
}
if ($related_posts->have_posts()) :
// Fallback to recent posts if no related posts found
if (!$related_posts || !$related_posts->have_posts()) {
$related_posts = new WP_Query(array(
'post_type' => 'post',
'posts_per_page' => 3,
'post__not_in' => array(get_the_ID()),
'orderby' => 'date',
'order' => 'DESC',
));
}
if ($related_posts->have_posts()) :
?>
<section class="related-posts-section">
<div class="container">
<header class="related-posts-header">
<h2 class="related-posts-title">Related Posts</h2>
<h2 class="related-posts-title">More Articles</h2>
</header>
<div class="related-posts-grid">
@@ -89,6 +162,44 @@ get_header();
</div>
</div>
</section>
<?php endif; ?>
<!-- Featured Properties Section -->
<?php
if (function_exists('mls_get_properties')) :
$featured_properties = mls_get_properties(array(
'limit' => 3,
'status' => 'Active',
));
if (!empty($featured_properties)) :
?>
<section class="blog-featured-properties">
<div class="container">
<header class="blog-featured-header">
<h2 class="blog-featured-title">Featured Properties</h2>
<p class="blog-featured-subtitle">Browse our latest listings in southern Minnesota</p>
</header>
<div class="blog-featured-grid">
<?php
foreach ($featured_properties as $property) :
set_query_var('mls_property', $property);
get_template_part('template-parts/property/property-card', 'mls');
endforeach;
?>
</div>
<div class="blog-featured-cta">
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-primary">
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>
</div>
</section>
<?php
endif;
endif;
+3 -1
View File
@@ -16,6 +16,8 @@
@import '../template-parts/content/content-404.scss';
@import '../template-parts/content/content-homepage.scss';
@import '../template-parts/content/content-about.scss';
@import '../template-parts/content/content-team.scss';
@import '../template-parts/content/content-results.scss';
@import '../template-parts/content/content-join.scss';
@import '../template-parts/content/content-contact.scss';
@import '../template-parts/content/content-archive.scss';
@@ -29,7 +31,7 @@
@import '../template-parts/property/property-filters.scss';
@import '../template-parts/property/mobile-map.scss';
@import '../template-parts/property/property-gallery.scss';
@import '../template-parts/property/single-property.scss';
@import '../template-parts/property/single-property-mls.scss';
@import '../template-parts/agent/single-agent.scss';
@import '../template-parts/agent/archive-agent.scss';
@@ -329,7 +329,7 @@
p {
margin: 0;
padding-left: 2rem;
font-size: 1rem;
font-size: 0.85rem;
line-height: 1.7;
color: var(--color-text);
font-style: italic;
@@ -409,6 +409,47 @@
@media (min-width: 1200px) {
grid-template-columns: repeat(3, 1fr);
}
// Horizontal layout for 2 or fewer listings on wide screens
&.few-listings {
@media (min-width: 1450px) {
grid-template-columns: 1fr;
.property-card {
display: flex;
flex-direction: row;
.property-card-image {
width: 280px;
min-width: 280px;
height: auto;
aspect-ratio: 4 / 3;
img,
.wp-post-image {
height: 100%;
object-fit: cover;
}
}
.property-card-content {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 1.5rem 2rem;
}
.property-card-price {
font-size: 1.75rem;
}
.property-card-title {
font-size: 1.25rem;
}
}
}
}
}
.agent-listings-cta {
@@ -63,6 +63,76 @@
}
}
// Additional Content Section (Block Editor)
.about-content-section {
padding: 4rem 0;
background-color: var(--color-bg-card);
@media (max-width: 768px) {
padding: 3rem 0;
}
}
.about-additional-content {
max-width: 900px;
margin: 0 auto;
h2, h3, h4 {
font-family: var(--font-display);
color: var(--color-text);
margin-top: 2rem;
margin-bottom: 1rem;
&:first-child {
margin-top: 0;
}
}
h2 {
font-size: 2rem;
@media (max-width: 768px) {
font-size: 1.75rem;
}
}
h3 {
font-size: 1.5rem;
}
p {
font-size: 1.0625rem;
line-height: 1.8;
color: var(--color-text-muted);
margin-bottom: 1.25rem;
}
ul, ol {
margin-bottom: 1.5rem;
padding-left: 1.5rem;
color: var(--color-text-muted);
li {
margin-bottom: 0.5rem;
line-height: 1.7;
}
}
// Support for WordPress block editor blocks
.wp-block-columns {
gap: 2rem;
margin-bottom: 2rem;
}
.wp-block-image {
margin-bottom: 1.5rem;
img {
border-radius: 0.5rem;
}
}
}
// Values Section
.about-values-section {
padding: 4rem 0;
@@ -122,12 +192,15 @@
margin: 0 auto;
}
// Team grid - centered even when not full width
.agents-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
// Team grid - override Agents_Archive grid with centered flexbox layout
// Using .about-team-section for higher specificity over .Agents_Archive
.about-team-section.Agents_Archive {
.agents-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
}
.agent-card-item {
width: 100%;
@@ -230,18 +230,11 @@
.property-card-minimal-image {
position: relative;
width: 140px;
min-width: 140px;
height: 120px;
overflow: hidden;
width: 100px;
min-width: 100px;
background-color: var(--color-bg-dark);
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
background-size: cover;
background-position: center;
.badge {
position: absolute;
@@ -255,6 +248,7 @@
.property-card-minimal-placeholder {
width: 100%;
height: 100%;
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
@@ -262,7 +256,7 @@
}
.property-card-minimal-content {
padding: 1rem;
padding: 0.5rem 1rem;
flex: 1;
display: flex;
flex-direction: column;
@@ -315,6 +309,78 @@
}
}
// ============================================
// Minimal Agent Card (matches property-card-minimal style)
// ============================================
.agent-card-minimal {
background-color: var(--color-bg-card);
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid var(--color-border);
margin-top: 1rem;
.agent-card-minimal-link {
display: flex;
text-decoration: none;
color: inherit;
}
.agent-card-minimal-image {
position: relative;
width: 100px;
min-width: 100px;
height: 100px;
overflow: hidden;
background-color: var(--color-bg-dark);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.agent-card-minimal-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
}
.agent-card-minimal-content {
padding: 1rem;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.agent-card-minimal-label {
display: block;
font-size: 0.625rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-accent);
margin-bottom: 0.25rem;
font-weight: 600;
}
.agent-card-minimal-name {
font-family: var(--font-display);
font-size: 1.125rem;
color: var(--color-text);
margin-bottom: 0.125rem;
}
.agent-card-minimal-title {
font-size: 0.8125rem;
color: var(--color-text-muted);
}
}
// Badge styles (shared)
.badge-active {
background-color: var(--color-success);
@@ -0,0 +1,75 @@
/**
* Results Page Styles
*
* @package HomeProz
*/
.Results_Page {
.results-page-main {
padding: 0;
}
// Results Content Section
.results-content-section {
padding: 4rem 0;
background-color: var(--color-bg-dark);
@media (max-width: 768px) {
padding: 3rem 0;
}
}
// Results Stats
.results-stats {
margin-bottom: 2rem;
}
.results-count {
font-size: 1.125rem;
color: var(--color-text-muted);
margin: 0;
}
// Results Grid
.results-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
@media (max-width: 1280px) {
grid-template-columns: repeat(3, 1fr);
}
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
// No Results Message
.no-results-message {
text-align: center;
padding: 4rem 2rem;
background-color: var(--color-bg-card);
border-radius: 0.5rem;
h2 {
font-family: var(--font-display);
font-size: 1.875rem;
color: var(--color-text);
margin-bottom: 0.75rem;
}
p {
font-size: 1.0625rem;
color: var(--color-text-muted);
margin-bottom: 1.5rem;
max-width: 500px;
margin-left: auto;
margin-right: auto;
}
}
}
@@ -32,6 +32,19 @@ if (!defined('ABSPATH')) {
?>
</span>
<?php endif; ?>
<?php
// Show agent author if set
$agent_author_id = get_field('post_agent_author');
if ($agent_author_id) :
$agent_name = get_the_title($agent_author_id);
$agent_url = get_permalink($agent_author_id);
$agent_disabled = get_field('agent_disabled', $agent_author_id);
?>
<span class="meta-separator">|</span>
<span class="post-author">
By <?php if (!$agent_disabled) : ?><a href="<?php echo esc_url($agent_url); ?>"><?php endif; ?><?php echo esc_html($agent_name); ?><?php if (!$agent_disabled) : ?></a><?php endif; ?>
</span>
<?php endif; ?>
</div>
<?php the_title('<h1 class="post-title">', '</h1>'); ?>
@@ -97,6 +97,7 @@
margin-bottom: 1rem;
font-size: 0.875rem;
color: var(--color-text-muted);
flex-wrap: wrap;
time {
color: var(--color-text-muted);
@@ -106,7 +107,8 @@
color: var(--color-border);
}
.post-categories a {
.post-categories a,
.post-author a {
color: var(--color-accent-light);
text-decoration: none;
@@ -297,28 +299,160 @@
margin-bottom: 1rem;
}
.comment-form {
label {
display: block;
margin-bottom: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text);
// Enhanced Post Navigation Section
.post-nav-section {
padding: 3rem 0;
background-color: var(--color-bg-card);
border-top: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
}
input[type="text"],
input[type="email"],
input[type="url"],
textarea {
margin-bottom: 1rem;
}
.post-nav-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
.form-submit {
margin-top: 1rem;
input[type="submit"] {
@extend .btn;
@extend .btn-primary;
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: 1rem;
}
}
}
.post-nav-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background-color: var(--color-bg-dark);
border-radius: 0.5rem;
text-decoration: none;
transition: background-color 0.15s ease;
&:hover {
background-color: rgba(159, 55, 48, 0.1);
}
&.post-nav-empty {
visibility: hidden;
@media (max-width: 768px) {
display: none;
}
}
&.post-nav-next {
justify-content: flex-end;
text-align: right;
.post-nav-label {
justify-content: flex-end;
}
}
}
.post-nav-thumb {
flex-shrink: 0;
width: 80px;
height: 80px;
border-radius: 0.375rem;
overflow: hidden;
background-color: var(--color-bg-card);
img {
width: 100%;
height: 100%;
object-fit: cover;
}
@media (max-width: 480px) {
width: 60px;
height: 60px;
}
}
.post-nav-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
}
.post-nav-content {
flex: 1;
min-width: 0;
}
.post-nav-label {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.8125rem;
color: var(--color-text-muted);
margin-bottom: 0.25rem;
svg {
flex-shrink: 0;
}
}
.post-nav-title {
display: block;
font-size: 0.9375rem;
font-weight: 600;
color: var(--color-text);
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
// Featured Properties on Blog
.blog-featured-properties {
padding: 4rem 0;
background-color: var(--color-bg-dark);
@media (max-width: 768px) {
padding: 3rem 0;
}
}
.blog-featured-header {
text-align: center;
margin-bottom: 2rem;
}
.blog-featured-title {
font-family: var(--font-display);
font-size: 1.875rem;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.blog-featured-subtitle {
font-size: 1rem;
color: var(--color-text-muted);
margin-bottom: 0;
}
.blog-featured-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
margin-bottom: 2rem;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
.blog-featured-cta {
text-align: center;
}
@@ -0,0 +1,71 @@
/**
* Team Page Styles
*
* @package HomeProz
*/
.Team_Page {
.team-page-main {
padding: 0;
}
// Team Agents Section
.team-agents-section {
padding: 4rem 0;
background-color: var(--color-bg-dark);
@media (max-width: 768px) {
padding: 3rem 0;
}
}
// Team grid - override Agents_Archive grid with centered flexbox layout
.team-agents-section.Agents_Archive {
.agents-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
}
.agent-card-item {
width: 100%;
@media (min-width: 640px) {
width: calc(50% - 1rem);
}
@media (min-width: 1024px) {
width: calc(33.333% - 1.334rem);
}
@media (min-width: 1280px) {
width: calc(25% - 1.5rem);
}
}
}
// Broker Section
.team-broker-section {
padding: 3rem 0;
background-color: var(--color-bg-card);
border-top: 1px solid var(--color-border);
}
.team-broker-content {
text-align: center;
}
.team-broker-title {
font-family: var(--font-display);
font-size: 1.25rem;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.team-broker-text {
font-size: 0.9375rem;
color: var(--color-text-muted);
margin-bottom: 0;
}
}
@@ -14,6 +14,7 @@ $phone = homeproz_get_option('phone');
$email = homeproz_get_option('email');
$address = homeproz_get_option('address');
$facebook = homeproz_get_option('facebook');
$instagram = homeproz_get_option('instagram');
$tiktok = homeproz_get_option('tiktok');
// Footer content from Theme Options
@@ -38,7 +39,7 @@ $office_hours = homeproz_get_office_hours();
</div>
<p class="footer-tagline"><?php echo esc_html($footer_tagline); ?></p>
<?php if ($facebook || $tiktok) : ?>
<?php if ($facebook || $instagram || $tiktok) : ?>
<div class="footer-social">
<?php if ($facebook) : ?>
<a href="<?php echo esc_url($facebook); ?>" target="_blank" rel="noopener noreferrer" aria-label="Facebook">
@@ -47,6 +48,13 @@ $office_hours = homeproz_get_office_hours();
</svg>
</a>
<?php endif; ?>
<?php if ($instagram) : ?>
<a href="<?php echo esc_url($instagram); ?>" target="_blank" rel="noopener noreferrer" aria-label="Instagram">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
<?php endif; ?>
<?php if ($tiktok) : ?>
<a href="<?php echo esc_url($tiktok); ?>" target="_blank" rel="noopener noreferrer" aria-label="TikTok">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
@@ -48,6 +48,7 @@
<?php
$facebook = homeproz_get_option('facebook');
$tiktok = homeproz_get_option('tiktok');
$instagram = homeproz_get_option('instagram');
?>
<?php if ($facebook) : ?>
<a href="<?php echo esc_url($facebook); ?>" class="header-social" target="_blank" rel="noopener noreferrer" aria-label="Facebook">
@@ -56,6 +57,13 @@
</svg>
</a>
<?php endif; ?>
<?php if ($instagram) : ?>
<a href="<?php echo esc_url($instagram); ?>" class="header-social" target="_blank" rel="noopener noreferrer" aria-label="Instagram">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
<?php endif; ?>
<?php if ($tiktok) : ?>
<a href="<?php echo esc_url($tiktok); ?>" class="header-social" target="_blank" rel="noopener noreferrer" aria-label="TikTok">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
@@ -1,6 +1,6 @@
# Property Archive System
Property listing and filtering system for the real estate website.
MLS-based property listing and filtering system for the real estate website.
## Files
@@ -9,9 +9,19 @@ Property listing and filtering system for the real estate website.
| `property-filters.php` | Filter form UI (Type, Location, Beds, Price Range) |
| `property-filters.js` | AJAX filtering without page reload |
| `property-filters.scss` | Filter bar and map view styles |
| `property-results.php` | Property grid with pagination |
| `property-card.php` | Individual property card component |
| `property-filters-sticky.php` | Sticky filter bar variant |
| `property-results.php` | Property grid with pagination (MLS data) |
| `property-card-mls.php` | Individual MLS property card component |
| `property-card-minimal.php` | Compact property card for featured sections |
| `property-agent.php` | Agent sidebar widget on single property |
| `property-gallery.php` | Gallery component for property detail |
| `single-property-mls.php` | Single MLS property detail page |
| `agent-card-minimal.php` | Compact agent card for property pages |
## Data Source
All property data comes from the MLS plugin (`wp-content/plugins/mls-by-hansonxyz/`).
Use `mls_get_properties()` to query listings.
## Sorting Logic
@@ -19,27 +29,18 @@ Properties are sorted by:
1. **Status**: Active first, Pending second, Sold third
2. **Modified Date**: Most recently modified first within each status group
This sorting is handled in PHP via `homeproz_sort_properties_by_status()` in `inc/template-functions.php`.
## Filter Fields
- **Type**: Taxonomy `property_type` (House, Land, Commercial, etc.)
- **Location**: Taxonomy `property_location` (City names)
- **Beds**: ACF field `bedrooms` (minimum bedrooms filter)
- **Price Range**: ACF field `property_price` (min/max numeric filter)
- **Type**: MLS property type (House, Land, Commercial, etc.)
- **Location**: MLS city
- **Beds**: Minimum bedrooms filter
- **Price Range**: Min/max price filter
Status filter was removed - all properties (Active, Pending, Sold) are always shown.
## AJAX Handler
`homeproz_ajax_filter_properties()` in `inc/ajax-handlers.php`
- Receives filter params via POST
- Returns HTML for `#property-results` container
- Uses same sorting logic as initial page load
## Map View
Toggle between Grid and Map views. Map view:
- Uses Leaflet.js with OpenStreetMap tiles
- City-based coordinates (no geocoding API)
- City-based coordinates from MLS data
- Properties displayed in half-height map + 2-column grid
@@ -0,0 +1,58 @@
<?php
/**
* Minimal Agent Card Template
*
* A compact agent card for use in inquiry pages, styled to match property-card-minimal
* Expects agent_id to be passed via set_query_var('minimal_agent_id', $agent_id)
*
* @package HomeProz
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$agent_id = get_query_var('minimal_agent_id');
if (!$agent_id || get_post_type($agent_id) !== 'agent') {
return;
}
// Get agent data
$agent_name = get_the_title($agent_id);
$agent_title = get_field('agent_title', $agent_id) ?: 'Real Estate Agent';
$agent_disabled = get_field('agent_disabled', $agent_id);
// Get agent profile URL (only if not disabled)
$agent_url = !$agent_disabled ? get_permalink($agent_id) : '';
// Get agent photo
$agent_photo_url = '';
$agent_photo_id = get_post_thumbnail_id($agent_id);
if ($agent_photo_id) {
$agent_photo_url = wp_get_attachment_image_url($agent_photo_id, 'medium');
}
?>
<div class="agent-card-minimal">
<a href="<?php echo esc_url($agent_url); ?>" class="agent-card-minimal-link" target="_blank" rel="noopener">
<div class="agent-card-minimal-image">
<?php if ($agent_photo_url) : ?>
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent_name); ?>" loading="lazy">
<?php else : ?>
<div class="agent-card-minimal-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="agent-card-minimal-content">
<span class="agent-card-minimal-label">Your HomeProz Agent</span>
<div class="agent-card-minimal-name"><?php echo esc_html($agent_name); ?></div>
<div class="agent-card-minimal-title"><?php echo esc_html($agent_title); ?></div>
</div>
</a>
</div>
+562 -39
View File
@@ -43,6 +43,14 @@
lastTime: 0,
velocity: 0,
// Property list state (for infinite scroll)
currentPage: 1,
maxPages: 1,
totalProperties: 0,
isLoadingProperties: false,
scrollPosition: 0,
propertiesLoaded: false,
/**
* Initialize the sheet
*/
@@ -63,8 +71,8 @@
// Bind events
this.bindEvents();
// Set initial state
this.setState('collapsed');
// Set initial state (skip URL update, will be set by MobileMap init if hash exists)
this.setState('collapsed', true);
},
/**
@@ -136,6 +144,11 @@
e.stopPropagation();
}
});
// Infinite scroll and scroll position tracking
this.$content.on('scroll', function() {
self.onScroll();
});
},
/**
@@ -230,24 +243,20 @@
/**
* Set sheet state
* @param {string} state - 'collapsed' or 'expanded'
* @param {boolean} skipUrlUpdate - Don't update URL hash (for restore from hash)
*/
setState: function(state, skipAnimation) {
setState: function(state, skipUrlUpdate) {
this.currentState = state;
this.$sheet.attr('data-state', state);
var height = this.states[state];
if (skipAnimation) {
this.$sheet.addClass('is-dragging');
}
this.$sheet.css('height', height + 'px');
if (skipAnimation) {
// Remove after a frame to allow CSS to apply
var self = this;
requestAnimationFrame(function() {
self.$sheet.removeClass('is-dragging');
});
}
// Update URL hash when user changes state (not on restore)
if (!skipUrlUpdate && MobileMap.map) {
MobileMap.updateUrlHash();
}
},
/**
@@ -261,6 +270,9 @@
if (this.$filters.hasClass('is-visible') && this.currentState === 'collapsed') {
this.setState('expanded');
}
// Scroll to top when toggling filters
this.$content.scrollTop(0);
},
/**
@@ -304,11 +316,221 @@
},
/**
* Render property cards in the list
* Handle scroll events for infinite scroll and position tracking
*/
onScroll: function() {
var self = this;
// Track scroll position for URL hash
this.scrollPosition = this.$content.scrollTop();
// Debounce URL update
clearTimeout(this._scrollUpdateTimer);
this._scrollUpdateTimer = setTimeout(function() {
MobileMap.updateUrlHash();
}, 300);
// Check for infinite scroll trigger
if (this.isLoadingProperties || this.currentPage >= this.maxPages) {
return;
}
var scrollTop = this.$content.scrollTop();
var scrollHeight = this.$content[0].scrollHeight;
var clientHeight = this.$content[0].clientHeight;
// Load more when 200px from bottom
if (scrollTop + clientHeight >= scrollHeight - 200) {
this.loadMoreProperties();
}
},
/**
* Load properties from server (initial or page load)
*/
loadProperties: function(append) {
if (this.isLoadingProperties) return;
var self = this;
this.isLoadingProperties = true;
if (!append) {
this.currentPage = 1;
this.showLoading();
} else {
// Show loading indicator at bottom
this.$propertyList.append(
'<div class="sheet-property-loading sheet-loading-more">' +
'<div class="spinner"></div>' +
'</div>'
);
}
var center = MobileMap.map ? MobileMap.map.getCenter() : null;
var bounds = MobileMap.map ? MobileMap.map.getBounds() : null;
var data = {
action: 'homeproz_filter_properties',
nonce: homeprozAjax.nonce,
property_status: 'Active',
property_type: MobileMap.currentFilters.property_type || '',
city: MobileMap.currentFilters.city || '',
min_price: MobileMap.currentFilters.min_price || '',
max_price: MobileMap.currentFilters.max_price || '',
beds: MobileMap.currentFilters.min_beds || '',
paged: this.currentPage,
cards_only: 'true'
};
// Add map center for distance-based sorting
if (center) {
data.center = [center.lat, center.lng];
}
// Add bounds to filter to viewport
if (bounds) {
data.bounds = [
bounds.getSouthWest().lat,
bounds.getSouthWest().lng,
bounds.getNorthEast().lat,
bounds.getNorthEast().lng
];
}
$.ajax({
url: homeprozAjax.ajaxUrl,
type: 'POST',
data: data,
success: function(response) {
self.isLoadingProperties = false;
if (response.success && response.data) {
self.maxPages = response.data.max_pages || 1;
self.totalProperties = response.data.found_posts || 0;
// Remove loading indicator
self.$propertyList.find('.sheet-loading-more').remove();
if (append) {
// Append new cards
self.$propertyList.append(response.data.html);
} else {
// Replace content
if (response.data.html && response.data.html.trim()) {
self.$propertyList.html(response.data.html);
} else {
self.$propertyList.html(
'<div class="sheet-no-properties">' +
'<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>' +
'<polyline points="9 22 9 12 15 12 15 22"/>' +
'</svg>' +
'<p>No properties found in this area</p>' +
'</div>'
);
}
}
self.updateCount(self.totalProperties);
self.propertiesLoaded = true;
// Load images for newly added cards
self.loadImages();
}
},
error: function() {
self.isLoadingProperties = false;
self.$propertyList.find('.sheet-loading-more').remove();
if (!append) {
self.$propertyList.html(
'<div class="sheet-no-properties">' +
'<p>Error loading properties</p>' +
'</div>'
);
}
}
});
},
/**
* Load more properties (infinite scroll)
*/
loadMoreProperties: function() {
if (this.currentPage >= this.maxPages) return;
this.currentPage++;
this.loadProperties(true);
},
/**
* Reset property list (on filter change or map move)
*/
resetPropertyList: function() {
this.currentPage = 1;
this.propertiesLoaded = false;
this.loadProperties(false);
},
/**
* Restore scroll position (after back navigation)
*/
restoreScrollPosition: function(position) {
var self = this;
if (position > 0) {
// Wait for content to render, then scroll
setTimeout(function() {
self.$content.scrollTop(position);
}, 100);
}
},
/**
* Load images for property cards in the sheet
* Processes data-bg attributes since the main lazy loader doesn't work in scrollable containers
*/
loadImages: function() {
var $images = this.$propertyList.find('.property-card-image[data-bg]');
$images.each(function() {
var $el = $(this);
var bgUrl = $el.data('bg');
if (!bgUrl) return;
// Remove data-bg to prevent re-processing
$el.removeAttr('data-bg');
// Create image to preload
var img = new Image();
img.onload = function() {
$el.css('background-image', 'url("' + bgUrl + '")');
$el.removeClass('is-loading').addClass('is-loaded');
};
img.onerror = function() {
$el.removeClass('is-loading');
};
img.src = bgUrl;
});
},
/**
* Render property cards in the list (legacy - kept for cluster marker data)
* @param {Array} properties - Property data array
* @param {boolean} skipCountUpdate - Don't update count (for density/cluster mode)
*/
renderProperties: function(properties, skipCountUpdate) {
// This is now only used for updating count from cluster data
// Actual property loading is done via loadProperties()
if (skipCountUpdate) {
// Just update count from cluster total, don't change list
return;
}
// For markers mode, we still get property data directly
// but prefer AJAX loading for consistent experience
var self = this;
var html = '';
@@ -318,11 +540,9 @@
'<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>' +
'<polyline points="9 22 9 12 15 12 15 22"/>' +
'</svg>' +
'<p>Zoom in to see individual properties</p>' +
'<p>No properties found</p>' +
'</div>';
if (!skipCountUpdate) {
this.updateCount(0);
}
this.updateCount(0);
} else {
properties.forEach(function(prop) {
html += self.renderPropertyCard(prop);
@@ -382,6 +602,10 @@
currentFilters: {},
currentMode: null,
debounceTimer: null,
urlUpdateTimer: null,
isRestoringFromHash: false,
isNearMeMode: false,
userLocation: null,
/**
* Initialize the mobile map
@@ -392,8 +616,15 @@
return;
}
// Get initial filters from URL params
this.currentFilters = this.getFiltersFromUrl();
var self = this;
// Check if we're in near-me mode
var urlParams = new URLSearchParams(window.location.search);
this.isNearMeMode = urlParams.get('near_me') === '1';
// Get initial state from hash or URL params
var hashState = this.getStateFromHash();
this.currentFilters = this.getFiltersFromUrl(hashState);
// Initialize map
this.map = L.map('mobile-property-map', {
@@ -416,26 +647,193 @@
this.markerLayer = L.layerGroup().addTo(this.map);
// Bind map events
var self = this;
this.map.on('moveend zoomend', function() {
self.onMapMove();
});
// Initial load - fit to all properties
// Listen for browser back/forward
$(window).on('hashchange', function() {
self.onHashChange();
});
// Handle near-me mode - request geolocation
if (this.isNearMeMode) {
this.requestGeolocation();
return; // Don't load properties until we have location
}
// Check if we have a saved position in hash
if (hashState && hashState.lat !== null && hashState.lng !== null && hashState.zoom !== null) {
// Restore from hash
this.isRestoringFromHash = true;
this.map.setView([hashState.lat, hashState.lng], hashState.zoom);
this.isRestoringFromHash = false;
// Restore sheet state from hash
if (hashState.sheet && (hashState.sheet === 'expanded' || hashState.sheet === 'collapsed')) {
MobileSheet.setState(hashState.sheet, true);
}
// Restore scroll position after properties load
if (hashState.scroll > 0) {
// Wait for properties to load before restoring scroll
var scrollToRestore = hashState.scroll;
var checkLoaded = setInterval(function() {
if (MobileSheet.propertiesLoaded) {
clearInterval(checkLoaded);
MobileSheet.restoreScrollPosition(scrollToRestore);
}
}, 100);
// Timeout after 5 seconds
setTimeout(function() {
clearInterval(checkLoaded);
}, 5000);
}
} else {
// Initial load - fit to all properties
this.fitToAllProperties();
}
},
/**
* Request user's geolocation for near-me mode
*/
requestGeolocation: function() {
var self = this;
// Show loading state
MobileSheet.showLoading();
MobileSheet.$propertyCount.text('Locating');
// Check if geolocation is supported
if (!navigator.geolocation) {
console.log('[MobileMap] Geolocation not supported');
this.onGeolocationError({ code: 0, message: 'Geolocation not supported' });
return;
}
// Request location
navigator.geolocation.getCurrentPosition(
function(position) {
self.onGeolocationSuccess(position);
},
function(error) {
console.log('[MobileMap] Geolocation error:', error.code, error.message);
self.onGeolocationError(error);
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 300000 // 5 minutes cache
}
);
},
/**
* Handle successful geolocation
*/
onGeolocationSuccess: function(position) {
var lat = position.coords.latitude;
var lng = position.coords.longitude;
var self = this;
console.log('[MobileMap] Location:', lat.toFixed(4), lng.toFixed(4));
// Store user location
this.userLocation = { lat: lat, lng: lng };
// Calculate optimal zoom level based on nearby properties
$.ajax({
url: homeprozAjax.ajaxUrl,
type: 'POST',
data: {
action: 'homeproz_calculate_near_me_zoom',
lat: lat,
lng: lng
},
success: function(response) {
var zoomLevel = 10; // Default fallback
if (response.success && response.data && response.data.zoom) {
zoomLevel = response.data.zoom;
console.log('[MobileMap] Near-me zoom:', zoomLevel, '(' + response.data.count + ' properties)');
}
self.centerOnLocation(lat, lng, zoomLevel);
},
error: function() {
// Fall back to default zoom
self.centerOnLocation(lat, lng, 10);
}
});
},
/**
* Handle geolocation error
*/
onGeolocationError: function(error) {
var message = 'Unable to get your location.';
switch (error.code) {
case 1: // PERMISSION_DENIED
message = 'Location access was denied. Please enable location services.';
break;
case 2: // POSITION_UNAVAILABLE
message = 'Location information is unavailable.';
break;
case 3: // TIMEOUT
message = 'Location request timed out.';
break;
}
console.log('[MobileMap] Geolocation failed:', message);
// Show error in property list
MobileSheet.$propertyList.html(
'<div class="sheet-no-properties">' +
'<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">' +
'<circle cx="12" cy="12" r="10"/>' +
'<line x1="12" y1="8" x2="12" y2="12"/>' +
'<line x1="12" y1="16" x2="12.01" y2="16"/>' +
'</svg>' +
'<p>' + message + '</p>' +
'<a href="' + window.location.pathname + '" class="btn btn-primary btn-sm" style="margin-top: 1rem;">View All Properties</a>' +
'</div>'
);
MobileSheet.$propertyCount.text('0');
// Fall back to showing all properties
this.fitToAllProperties();
},
/**
* Get filters from URL params
* Center map on user's location
*/
getFiltersFromUrl: function() {
centerOnLocation: function(lat, lng, zoom) {
this.isRestoringFromHash = true;
this.map.setView([lat, lng], zoom);
this.isRestoringFromHash = false;
// Clear near-me mode flag and load properties normally
this.isNearMeMode = false;
// Remove near_me from URL to prevent re-triggering on refresh
var url = new URL(window.location);
url.searchParams.delete('near_me');
history.replaceState(null, '', url.pathname + url.search + window.location.hash);
},
/**
* Get filters from URL params or hash state
*/
getFiltersFromUrl: function(hashState) {
var params = new URLSearchParams(window.location.search);
// Hash state takes priority over query params
return {
property_type: params.get('property_type') || '',
city: params.get('city') || '',
min_beds: params.get('beds') || '',
min_price: params.get('min_price') || '',
max_price: params.get('max_price') || '',
property_type: (hashState && hashState.property_type) || params.get('property_type') || '',
city: (hashState && hashState.city) || params.get('city') || '',
min_beds: (hashState && hashState.beds) || params.get('beds') || '',
min_price: (hashState && hashState.min_price) || params.get('min_price') || '',
max_price: (hashState && hashState.max_price) || params.get('max_price') || '',
status: 'Active'
};
},
@@ -448,6 +846,10 @@
clearTimeout(this.debounceTimer);
this.debounceTimer = setTimeout(function() {
self.loadClusters();
// Update URL hash (unless we're restoring from hash)
if (!self.isRestoringFromHash) {
self.updateUrlHash();
}
}, 150);
},
@@ -495,6 +897,7 @@
*/
updateFilters: function(filters) {
this.currentFilters = $.extend({}, this.currentFilters, filters);
this.updateUrlHash();
this.fitToAllProperties();
},
@@ -515,8 +918,6 @@
bounds.getNorthEast().lng
];
MobileSheet.showLoading();
$.ajax({
url: homeprozAjax.ajaxUrl,
type: 'GET',
@@ -539,23 +940,21 @@
switch (data.type) {
case 'density':
self.renderDensity(data.dots);
MobileSheet.updateCount(data.total || 0);
MobileSheet.renderProperties([], true); // Skip count update
break;
case 'clusters':
self.renderClusters(data.clusters);
MobileSheet.updateCount(data.total || 0);
MobileSheet.renderProperties([], true); // Skip count update
break;
case 'markers':
self.renderMarkers(data.markers);
MobileSheet.renderProperties(data.markers);
break;
}
// Always load property list via AJAX (sorted by distance from center)
MobileSheet.resetPropertyList();
}
},
error: function() {
MobileSheet.renderProperties([]);
MobileSheet.resetPropertyList();
}
});
},
@@ -712,9 +1111,12 @@
* Accepts either a number or formatted string like "$375,000"
*/
formatPrice: function(price) {
// If price is a string, strip non-numeric characters
// If price is a string, handle it carefully
if (typeof price === 'string') {
price = parseInt(price.replace(/[^0-9]/g, ''), 10);
// Remove currency symbol and commas first
price = price.replace(/[$,]/g, '');
// Parse as float to handle decimals, then round to integer
price = Math.round(parseFloat(price));
}
price = Number(price);
if (isNaN(price)) return '$0';
@@ -731,6 +1133,127 @@
*/
formatNumber: function(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
},
/**
* Get state from URL hash
*/
getStateFromHash: function() {
var hash = window.location.hash.replace('#', '');
if (!hash) return null;
var raw = {};
hash.split('&').forEach(function(part) {
var kv = part.split('=');
if (kv.length === 2) {
raw[kv[0]] = decodeURIComponent(kv[1]);
}
});
return {
// Filters
property_type: raw.property_type || '',
city: raw.city || '',
min_price: raw.min_price || '',
max_price: raw.max_price || '',
beds: raw.beds || '',
// Map state
lat: raw.lat ? parseFloat(raw.lat) : null,
lng: raw.lng ? parseFloat(raw.lng) : null,
zoom: raw.zoom ? parseInt(raw.zoom) : null,
// Sheet state
sheet: raw.sheet || 'collapsed',
scroll: raw.scroll ? parseInt(raw.scroll) : 0
};
},
/**
* Update URL hash with current map state
*/
updateUrlHash: function() {
if (!this.map) return;
var hashParts = [];
// Add non-empty filters
if (this.currentFilters.property_type) {
hashParts.push('property_type=' + encodeURIComponent(this.currentFilters.property_type));
}
if (this.currentFilters.city) {
hashParts.push('city=' + encodeURIComponent(this.currentFilters.city));
}
if (this.currentFilters.min_beds) {
hashParts.push('beds=' + encodeURIComponent(this.currentFilters.min_beds));
}
if (this.currentFilters.min_price) {
hashParts.push('min_price=' + encodeURIComponent(this.currentFilters.min_price));
}
if (this.currentFilters.max_price) {
hashParts.push('max_price=' + encodeURIComponent(this.currentFilters.max_price));
}
// Add map state
var center = this.map.getCenter();
var zoom = this.map.getZoom();
hashParts.push('lat=' + center.lat.toFixed(6));
hashParts.push('lng=' + center.lng.toFixed(6));
hashParts.push('zoom=' + zoom);
// Add sheet state
hashParts.push('sheet=' + MobileSheet.currentState);
// Add scroll position if expanded and scrolled
if (MobileSheet.currentState === 'expanded' && MobileSheet.scrollPosition > 0) {
hashParts.push('scroll=' + Math.round(MobileSheet.scrollPosition));
}
var newHash = hashParts.length ? '#' + hashParts.join('&') : '';
history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
},
/**
* Handle browser back/forward (hash change)
*/
onHashChange: function() {
var hashState = this.getStateFromHash();
if (!hashState) return;
// Update filters from hash
this.currentFilters = this.getFiltersFromUrl(hashState);
// Update filter UI in sheet
if (hashState.property_type !== undefined) {
$('#mobile-filter-type').val(hashState.property_type);
}
if (hashState.city !== undefined) {
$('#mobile-filter-city').val(hashState.city);
}
if (hashState.beds !== undefined) {
$('#mobile-filter-beds').val(hashState.beds);
}
if (hashState.min_price !== undefined) {
$('#mobile-filter-min-price').val(hashState.min_price);
}
if (hashState.max_price !== undefined) {
$('#mobile-filter-max-price').val(hashState.max_price);
}
// Restore map position if available
if (hashState.lat !== null && hashState.lng !== null && hashState.zoom !== null) {
this.isRestoringFromHash = true;
this.map.setView([hashState.lat, hashState.lng], hashState.zoom);
this.isRestoringFromHash = false;
}
// Restore sheet state
if (hashState.sheet && (hashState.sheet === 'expanded' || hashState.sheet === 'collapsed')) {
MobileSheet.setState(hashState.sheet, true);
}
// Restore scroll position after a short delay (let content load)
if (hashState.scroll > 0) {
MobileSheet.restoreScrollPosition(hashState.scroll);
}
}
};
+101 -89
View File
@@ -228,107 +228,119 @@
padding: 2rem;
color: var(--color-text-muted);
font-size: 0.875rem;
}
// Mobile property cards (compact horizontal layout)
.sheet-property-card {
display: flex;
gap: 0.75rem;
padding: 0.75rem;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
margin-bottom: 0.75rem;
text-decoration: none;
color: inherit;
&:last-child {
margin-bottom: 0;
}
&:hover,
&.is-highlighted {
border-color: var(--color-accent);
}
&.is-highlighted {
background-color: rgba(159, 55, 48, 0.1);
// Inline loading indicator for infinite scroll
&.sheet-loading-more {
padding: 1rem;
flex-direction: row;
}
}
.sheet-card-image {
flex-shrink: 0;
width: 100px;
height: 80px;
background-color: var(--color-bg-card);
background-size: cover;
background-position: center;
border-radius: 0.375rem;
position: relative;
overflow: hidden;
&.is-loading {
// Mobile property cards - override standard .property-card when in sheet
.sheet-property-list {
.property-card {
display: flex;
align-items: center;
flex-direction: row;
gap: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
border-radius: 0.375rem;
position: relative;
cursor: pointer;
// Keep overlay link for clickability
.property-card-link-overlay {
position: absolute;
inset: 0;
z-index: 1;
}
&:last-child {
margin-bottom: 0;
}
}
.property-card-image {
flex-shrink: 0;
width: 24vw;
height: 24vw;
border-radius: 0.25rem;
.property-card-badge {
padding: 1px 3px;
font-size: 0.5rem;
top: 2px;
left: 2px;
}
.property-card-spinner {
.spinner {
width: 16px;
height: 16px;
}
}
}
.property-card-excerpt {
display: none;
}
.property-card-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.sheet-card-badge {
position: absolute;
top: 0.25rem;
left: 0.25rem;
padding: 0.125rem 0.375rem;
font-size: 0.625rem;
font-weight: 600;
text-transform: uppercase;
border-radius: 0.25rem;
background-color: var(--color-active);
color: white;
&.badge-pending {
background-color: var(--color-pending);
gap: 2px;
padding: 0;
}
&.badge-sold {
background-color: var(--color-sold);
.property-card-price {
font-size: 0.875rem;
line-height: 1.2;
margin: 0;
}
}
.sheet-card-content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.property-card-title {
font-size: 0.75rem;
font-weight: 400;
color: var(--color-text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
margin: 0;
}
.sheet-card-price {
font-size: 1rem;
font-weight: 700;
color: var(--color-text);
margin-bottom: 0.125rem;
}
.sheet-card-address {
font-size: 0.8125rem;
color: var(--color-text-muted);
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.sheet-card-specs {
display: flex;
gap: 0.75rem;
font-size: 0.75rem;
color: var(--color-text-muted);
span {
.property-card-specs {
display: flex;
align-items: center;
gap: 0.25rem;
gap: 0.5rem;
font-size: 0.6875rem;
margin: 0;
padding: 0;
.spec-item {
display: flex;
align-items: center;
gap: 0.125rem;
padding: 0;
margin: 0;
svg {
width: 10px;
height: 10px;
display: none; // Hide icons for compact view
}
span {
font-size: 0.6875rem;
}
}
}
// Hide the "View Details" link in compact mode
.property-card-link {
display: none;
}
}
@@ -2,7 +2,7 @@
/**
* Property Agent Card Template Part
*
* Displays either the assigned agent (from Agent CPT) or a generic contact card
* Business card style display for the listing agent
* Disabled agents show name/photo but use office contact info
*
* @package HomeProz
@@ -15,10 +15,15 @@ if (!defined('ABSPATH')) {
$agent_id = isset($args['agent']) ? $args['agent'] : null;
$property_id = isset($args['property_id']) ? $args['property_id'] : get_the_ID();
$property_title = get_the_title($property_id);
$property_title = isset($args['property_title']) ? $args['property_title'] : ($property_id ? get_the_title($property_id) : '');
$listing_key = isset($args['listing_key']) ? $args['listing_key'] : '';
// Build contact page URL with property parameter
$contact_url = add_query_arg('property', urlencode($property_title), home_url('/contact/'));
// Build inquiry URL - use property-inquiry page with listing key for MLS properties
if ($listing_key) {
$contact_url = add_query_arg('listing', $listing_key, home_url('/property-inquiry/'));
} else {
$contact_url = add_query_arg('property', urlencode($property_title), home_url('/contact/'));
}
// Get company contact info
$company_phone = homeproz_get_option('phone');
@@ -32,6 +37,7 @@ $agent_phone = '';
$agent_email = '';
$agent_title = '';
$agent_photo_url = '';
$agent_url = '';
if ($agent_id && get_post_type($agent_id) === 'agent') {
$has_agent = true;
@@ -42,132 +48,115 @@ if ($agent_id && get_post_type($agent_id) === 'agent') {
// Get agent featured image
$agent_photo_id = get_post_thumbnail_id($agent_id);
if ($agent_photo_id) {
$agent_photo_url = wp_get_attachment_image_url($agent_photo_id, 'thumbnail');
$agent_photo_url = wp_get_attachment_image_url($agent_photo_id, 'medium');
}
// Only get agent contact info if NOT disabled
// Only get agent contact info and profile link if NOT disabled
if (!$agent_disabled) {
$agent_phone = get_field('agent_phone', $agent_id);
$agent_email = get_field('agent_email', $agent_id);
$agent_url = get_permalink($agent_id);
}
}
?>
<?php if ($has_agent) : ?>
<?php
// Agent profile URL - only link if not disabled
$agent_profile_url = !$agent_disabled ? get_permalink($agent_id) : null;
// Use company phone/email for disabled agents
$display_phone = $agent_disabled ? $company_phone : $agent_phone;
?>
<!-- Agent Card -->
<div class="sidebar-widget agent-card<?php echo $agent_disabled ? ' agent-disabled' : ''; ?>">
<!-- Agent Business Card -->
<div class="sidebar-widget agent-business-card<?php echo $agent_disabled ? ' agent-disabled' : ''; ?>">
<h3 class="widget-title">Listing Agent</h3>
<?php if ($agent_profile_url) : ?>
<a href="<?php echo esc_url($agent_profile_url); ?>" class="agent-info-link">
<?php else : ?>
<div class="agent-info-block">
<?php endif; ?>
<div class="agent-info">
<div class="agent-avatar">
<?php if ($agent_photo_url) : ?>
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent_name); ?>">
<div class="agent-card-inner">
<div class="agent-card-photo">
<?php if ($agent_photo_url) : ?>
<?php if ($agent_url) : ?>
<a href="<?php echo esc_url($agent_url); ?>" class="agent-photo-link">
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent_name); ?>">
</a>
<?php else : ?>
<div class="agent-avatar-placeholder">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent_name); ?>">
<?php endif; ?>
</div>
<div class="agent-details">
<p class="agent-name"><?php echo esc_html($agent_name); ?></p>
<p class="agent-role"><?php echo esc_html($agent_title ?: 'Real Estate Agent'); ?></p>
</div>
<?php else : ?>
<div class="agent-photo-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
</div>
<?php endif; ?>
</div>
<div class="agent-card-details">
<p class="agent-name">
<?php if ($agent_url) : ?>
<a href="<?php echo esc_url($agent_url); ?>"><?php echo esc_html($agent_name); ?></a>
<?php else : ?>
<?php echo esc_html($agent_name); ?>
<?php endif; ?>
</p>
<p class="agent-title"><?php echo esc_html($agent_title ?: 'Real Estate Agent'); ?></p>
<?php if ($agent_phone && !$agent_disabled) : ?>
<p class="agent-phone">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9+]/', '', $agent_phone)); ?>"><?php echo esc_html($agent_phone); ?></a>
</p>
<?php endif; ?>
</div>
<?php if ($agent_profile_url) : ?>
</a>
<?php else : ?>
</div>
<?php endif; ?>
<div class="agent-contact">
<?php if ($display_phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $display_phone)); ?>" class="btn btn-primary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<?php echo esc_html($display_phone); ?>
</a>
<?php endif; ?>
<?php if ($agent_profile_url) : ?>
<a href="<?php echo esc_url($agent_profile_url); ?>" class="btn btn-secondary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
Agent Profile
</a>
<?php endif; ?>
<?php if (!$agent_disabled && $agent_email) : ?>
<a href="mailto:<?php echo esc_attr($agent_email); ?>?subject=<?php echo esc_attr('Inquiry about ' . $property_title); ?>" class="btn btn-secondary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
Email Agent
</a>
<?php endif; ?>
<?php if ($agent_disabled) : ?>
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-secondary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Contact Us
</a>
<?php endif; ?>
<div class="agent-card-actions">
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-primary btn-block">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Inquire About This Property
</a>
</div>
</div>
<!-- Property Inquiry Card (always shown) -->
<div class="sidebar-widget property-inquiry-card">
<h3 class="widget-title">Request Information</h3>
<p class="inquiry-note">Have questions about this property? We're here to help.</p>
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-secondary btn-block">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Contact Us About This Property
</a>
</div>
<?php else : ?>
<!-- Generic Contact Card (no agent assigned) -->
<div class="sidebar-widget generic-contact-card">
<div class="sidebar-widget agent-business-card generic-contact">
<h3 class="widget-title">Interested in This Property?</h3>
<p class="contact-note">Contact us for more information on this listing. Our team is ready to answer your questions and schedule a showing.</p>
<div class="generic-contact-actions">
<?php if ($company_phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $company_phone)); ?>" class="btn btn-primary btn-block">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
<div class="agent-card-inner generic">
<div class="agent-card-photo">
<div class="agent-photo-placeholder office">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path d="M3 21h18"/>
<path d="M5 21V7l8-4v18"/>
<path d="M19 21V11l-6-4"/>
<path d="M9 9v.01"/>
<path d="M9 12v.01"/>
<path d="M9 15v.01"/>
<path d="M9 18v.01"/>
</svg>
Call <?php echo esc_html($company_phone); ?>
</a>
<?php endif; ?>
</div>
</div>
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-secondary btn-block">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<div class="agent-card-details">
<p class="agent-name">HomeProz Real Estate</p>
<p class="agent-title">Full Service Brokerage</p>
<?php if ($company_phone) : ?>
<p class="agent-phone">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9+]/', '', $company_phone)); ?>"><?php echo esc_html($company_phone); ?></a>
</p>
<?php endif; ?>
</div>
</div>
<div class="agent-card-actions">
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-primary btn-block">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Send Us a Message
Inquire About This Property
</a>
</div>
</div>
@@ -1,8 +1,11 @@
<?php
/**
* Property Card Template Part
* Property Card for Agent Pages
*
* Displays a property in card format for archive views
* MLS property card using the same markup as the original property-card.php
* for consistent styling on agent profile pages.
*
* Expects MLS property object via set_query_var('agent_listing', $property)
*
* @package HomeProz
*/
@@ -12,23 +15,31 @@ if (!defined('ABSPATH')) {
exit;
}
// Get property data
$property_id = get_the_ID();
$price = get_field('property_price', $property_id);
$street_address = get_field('street_address', $property_id);
$city = get_field('city', $property_id);
$state = get_field('state', $property_id);
$bedrooms = get_field('bedrooms', $property_id);
$bathrooms = get_field('bathrooms', $property_id);
$square_feet = get_field('square_feet', $property_id);
$short_description = get_field('short_description', $property_id);
$property = get_query_var('agent_listing');
// Get status from taxonomy
$status_terms = get_the_terms($property_id, 'property_status');
$status = $status_terms && !is_wp_error($status_terms) ? $status_terms[0]->name : 'Active';
$status_class = homeproz_get_status_class($status);
if (!$property) {
return;
}
// Extract property data from MLS object
$listing_key = $property->listing_key;
$listing_id = $property->listing_id;
$status = $property->standard_status;
// Use close_price for Closed properties if available
$price = ($status === 'Closed' && !empty($property->close_price)) ? $property->close_price : $property->list_price;
$bedrooms = $property->bedrooms_total;
$bathrooms = $property->bathrooms_total;
$square_feet = $property->living_area;
// Build address
$street_address = trim(implode(' ', array_filter([
$property->street_number,
$property->street_name,
$property->street_suffix,
])));
$city = $property->city;
$state = $property->state_or_province;
// Format address
$full_address = $street_address;
if ($city) {
$full_address .= ', ' . $city;
@@ -36,13 +47,27 @@ if ($city) {
if ($state) {
$full_address .= ', ' . $state;
}
// Status class
$status_class = 'badge-success';
if (strtolower($status) === 'pending') {
$status_class = 'badge-warning';
} elseif (strtolower($status) === 'sold' || strtolower($status) === 'closed') {
$status_class = 'badge-muted';
}
// Property URL
$property_url = home_url('/properties/?listing=' . urlencode($listing_key));
// Get primary photo from MLS
$primary_photo = function_exists('mls_get_property_image') ? mls_get_property_image($listing_key, true) : '';
?>
<article id="property-<?php echo esc_attr($property_id); ?>" data-property-id="<?php echo esc_attr($property_id); ?>" <?php post_class('property-card card'); ?>>
<a href="<?php the_permalink(); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<article class="property-card card">
<a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="property-card-image">
<?php if (has_post_thumbnail()) : ?>
<?php the_post_thumbnail('property-card', array('loading' => 'lazy')); ?>
<?php if ($primary_photo) : ?>
<img src="<?php echo esc_url($primary_photo); ?>" alt="<?php echo esc_attr($full_address); ?>" loading="lazy">
<?php else : ?>
<div class="property-card-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
@@ -65,7 +90,7 @@ if ($state) {
</div>
<h3 class="property-card-title">
<?php echo esc_html($full_address ?: get_the_title()); ?>
<?php echo esc_html($full_address); ?>
</h3>
<?php if ($bedrooms || $bathrooms || $square_feet) : ?>
@@ -100,12 +125,6 @@ if ($state) {
</ul>
<?php endif; ?>
<?php if ($short_description) : ?>
<p class="property-card-excerpt">
<?php echo esc_html(wp_trim_words($short_description, 15, '...')); ?>
</p>
<?php endif; ?>
<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">
@@ -20,30 +20,17 @@ if (!$property) {
}
// Extract property data
$price = $property->list_price;
$status = $property->standard_status;
// Use close_price for Closed properties if available
$price = ($status === 'Closed' && !empty($property->close_price)) ? $property->close_price : $property->list_price;
$bedrooms = $property->bedrooms_total;
$bathrooms = $property->bathrooms_total;
$square_feet = $property->living_area;
$status = $property->standard_status;
$listing_id = $property->listing_id;
$listing_key = $property->listing_key;
// Check for MLS override (custom featured photo)
$override = function_exists('homeproz_get_mls_override') ? homeproz_get_mls_override($listing_id) : null;
// Get primary photo - use override if available
$primary_photo = '';
if ($override && !empty($override['featured_photo'])) {
// Use the override featured photo
$primary_photo = isset($override['featured_photo']['sizes']['medium_large'])
? $override['featured_photo']['sizes']['medium_large']
: $override['featured_photo']['url'];
} elseif (!empty($property->photos)) {
$photos = is_string($property->photos) ? json_decode($property->photos, true) : $property->photos;
if (!empty($photos) && is_array($photos)) {
$primary_photo = $photos[0];
}
}
// Get primary photo from MLS
$primary_photo = function_exists('mls_get_image_url') ? mls_get_image_url($listing_key, 1, 'thumb') : '';
// Format address
$address_parts = array();
@@ -79,11 +66,11 @@ if ($status === 'Pending') {
?>
<div class="property-card-minimal">
<a href="<?php echo esc_url($property_url); ?>" class="property-card-minimal-link">
<div class="property-card-minimal-image">
<?php if ($primary_photo) : ?>
<img src="<?php echo esc_url($primary_photo); ?>" alt="<?php echo esc_attr($street_address); ?>" loading="lazy">
<?php else : ?>
<a href="<?php echo esc_url($property_url); ?>" class="property-card-minimal-link" target="_blank" rel="noopener">
<?php if ($primary_photo) : ?>
<div class="property-card-minimal-image" style="background-image: url('<?php echo esc_url($primary_photo); ?>');" role="img" aria-label="<?php echo esc_attr($street_address); ?>"></div>
<?php else : ?>
<div class="property-card-minimal-image">
<div class="property-card-minimal-placeholder">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
@@ -91,9 +78,8 @@ if ($status === 'Pending') {
<polyline points="21 15 16 10 5 21"/>
</svg>
</div>
<?php endif; ?>
<span class="badge <?php echo esc_attr($status_class); ?>"><?php echo esc_html($status); ?></span>
</div>
</div>
<?php endif; ?>
<div class="property-card-minimal-content">
<div class="property-card-minimal-price">$<?php echo esc_html(number_format($price)); ?></div>
<div class="property-card-minimal-address">
@@ -113,7 +99,6 @@ if ($status === 'Pending') {
<?php endif; ?>
</div>
<?php endif; ?>
<div class="property-card-minimal-mls">MLS# <?php echo esc_html($listing_id); ?></div>
</div>
</a>
</div>
@@ -21,11 +21,12 @@ if (!$property) {
// Extract property data
$listing_key = $property->listing_key;
$price = $property->list_price;
$status = $property->standard_status;
// Use close_price for Closed properties if available
$price = ($status === 'Closed' && !empty($property->close_price)) ? $property->close_price : $property->list_price;
$bedrooms = $property->bedrooms_total;
$bathrooms = $property->bathrooms_total;
$square_feet = $property->living_area;
$status = $property->standard_status;
$public_remarks = $property->public_remarks;
// Format address
@@ -41,12 +42,17 @@ if ($property->street_suffix) {
}
$street = implode(' ', $address_parts);
$full_address = $street;
if ($property->city) {
$full_address .= ', ' . $property->city;
}
if ($property->state_or_province) {
$full_address .= ', ' . $property->state_or_province;
// For manual properties, use full_address if street parts are missing
if (empty($street) && !empty($property->full_address)) {
$full_address = $property->full_address;
} else {
$full_address = $street;
if ($property->city) {
$full_address .= ', ' . $property->city;
}
if ($property->state_or_province) {
$full_address .= ', ' . $property->state_or_province;
}
}
// Property URL (will be updated when single property view is implemented)
@@ -60,24 +66,12 @@ if ($status === 'Pending') {
$status_class = 'badge-sold';
}
// Check for MLS override (custom featured photo)
$listing_id = $property->listing_id;
$override = function_exists('homeproz_get_mls_override') ? homeproz_get_mls_override($listing_id) : null;
// Get thumbnail image URL - use override if available, otherwise MLS image
if ($override && !empty($override['featured_photo'])) {
// Use the override featured photo (medium size for cards)
$image_url = isset($override['featured_photo']['sizes']['medium_large'])
? $override['featured_photo']['sizes']['medium_large']
: $override['featured_photo']['url'];
} else {
// Default to MLS image (index 1 = first image)
$image_url = function_exists('mls_get_image_url') ? mls_get_image_url($listing_key, 1, 'thumb') : '';
}
// Get thumbnail image URL from MLS (index 1 = first image)
$image_url = function_exists('mls_get_image_url') ? mls_get_image_url($listing_key, 1, 'thumb') : '';
$has_image = !empty($image_url);
?>
<article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>"<?php if ($property->latitude && $property->longitude) : ?> data-lat="<?php echo esc_attr($property->latitude); ?>" data-lng="<?php echo esc_attr($property->longitude); ?>"<?php endif; ?> class="property-card card mls-property">
<article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>"<?php if ($property->latitude && $property->longitude) : ?> data-lat="<?php echo esc_attr($property->latitude); ?>" data-lng="<?php echo esc_attr($property->longitude); ?>"<?php endif; ?> data-price="<?php echo esc_attr($price); ?>" class="property-card card mls-property">
<a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="property-card-image<?php echo $has_image ? ' has-image is-loading' : ''; ?>"<?php if ($has_image) : ?> data-bg="<?php echo esc_url($image_url); ?>"<?php endif; ?>>
<?php if ($has_image) : ?>
@@ -661,16 +661,16 @@
if (prop.lat && prop.lng) {
// Check if this is the previously selected marker
var isSelected = (prop.id === previousSelectedId);
var iconColor = isSelected ? 'amber' : 'red';
var marker = L.marker([prop.lat, prop.lng], {
icon: self.createIcon(iconColor),
icon: self.createPriceIcon(prop.price, isSelected),
zIndexOffset: isSelected ? 10000 : (self.baseZIndex + index)
});
// Store property ID on marker
marker.propertyId = prop.id;
marker.defaultZIndex = self.baseZIndex + index;
marker.propertyPrice = prop.price;
// Store marker data for later use (e.g., pin click sorting)
self.markerData[prop.id] = {
@@ -680,10 +680,9 @@
address: prop.address
};
// Bind popup
// Bind popup with address and link
marker.bindPopup(
'<div class="map-popup">' +
'<strong>' + prop.price + '</strong><br>' +
'<span>' + prop.address + '</span><br>' +
'<a href="' + prop.url + '">View Details</a>' +
'</div>'
@@ -748,6 +747,42 @@
});
},
/**
* Create a price label marker icon
*/
createPriceIcon: function(price, isSelected) {
var priceLabel = this.formatPrice(price);
var className = 'price-marker' + (isSelected ? ' is-selected' : '');
return L.divIcon({
className: className,
html: '<div class="price-marker-inner">' + priceLabel + '</div>',
iconSize: [70, 28],
iconAnchor: [35, 28],
popupAnchor: [0, -28]
});
},
/**
* Format price for marker label display
*/
formatPrice: function(price) {
// If price is a string, handle it carefully
if (typeof price === 'string') {
// Remove currency symbol and commas first
price = price.replace(/[$,]/g, '');
// Parse as float to handle decimals, then round to integer
price = Math.round(parseFloat(price));
}
price = Number(price);
if (isNaN(price)) return '$0';
if (price >= 1000000) {
return '$' + (price / 1000000).toFixed(1) + 'M';
} else if (price >= 1000) {
return '$' + Math.round(price / 1000) + 'k';
}
return '$' + price;
},
/**
* Handle marker click - scroll to card if exists, otherwise pan map
*/
@@ -826,12 +861,13 @@
},
/**
* Set marker color
* Set marker selected state
*/
setMarkerColor: function(propertyId, color) {
var marker = this.markers[propertyId];
if (marker) {
marker.setIcon(this.createIcon(color));
if (marker && marker.propertyPrice !== undefined) {
var isSelected = (color === 'amber');
marker.setIcon(this.createPriceIcon(marker.propertyPrice, isSelected));
}
},
@@ -897,6 +933,7 @@
if (needsTemporaryMarker) {
var lat = $card.data('lat');
var lng = $card.data('lng');
var price = $card.data('price');
if (lat && lng && self.map) {
// Remove any existing temporary marker
@@ -904,9 +941,9 @@
self.map.removeLayer(self.temporaryHoverMarker);
}
// Create temporary marker with blue color (highlighted)
// Create temporary marker with price label (hovered state)
self.temporaryHoverMarker = L.marker([lat, lng], {
icon: self.createIcon('blue'),
icon: self.createPriceIcon(price, true),
zIndexOffset: 15000 // Above everything else
});
@@ -270,6 +270,48 @@
}
}
// Price Label Markers
.price-marker {
background: none;
border: none;
}
.price-marker-inner {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.5rem;
background-color: var(--color-bg-card);
border: 2px solid var(--color-accent);
border-radius: 0.25rem;
color: var(--color-text);
font-size: 0.75rem;
font-weight: 700;
white-space: nowrap;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
position: relative;
cursor: pointer;
transition: transform 0.15s ease, border-color 0.15s ease;
&::after {
content: '';
position: absolute;
bottom: -6px;
left: 50%;
transform: translateX(-50%);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid var(--color-accent);
}
}
.price-marker.is-selected .price-marker-inner,
.price-marker:hover .price-marker-inner {
background-color: var(--color-accent);
color: white;
transform: scale(1.1);
}
// Marker Cluster Styling
.marker-cluster {
background-clip: padding-box;
@@ -43,6 +43,20 @@
swipeThreshold: 50,
isSwiping: false,
// Activity tracking for smart autoplay
lastActivityTimestamp: 0,
activityTimeout: 20000, // 20 seconds
isPausedForInactivity: false,
// Lazy loading tracking
loadedImagePages: {},
// Image retry tracking
failedImages: {}, // { index: { retries: N, seq: N } }
maxRetries: 5,
retryInterval: 20000, // 20 seconds
retryTimer: null,
/**
* Initialize
*/
@@ -97,16 +111,33 @@
this.bindEvents();
this.bindSwipeEvents();
this.bindActivityTracking();
this.updateThumbnailNavigation();
// Setup thumbnail loading states and preload first two pages
this.setupThumbnailLoading();
this.setupMainImageErrorHandling();
this.preloadThumbnailPages(0, 2);
// Mark first two pages as loaded
this.loadedImagePages[0] = true;
this.loadedImagePages[1] = true;
// Initialize activity timestamp
this.lastActivityTimestamp = Date.now();
// Start autoplay only if more than 1 image
if (this.images.length > 1) {
this.startAutoplay();
}
// Start retry timer for failed images
this.startRetryTimer();
// Preload the next image (image 1) since image 0 is already displayed
if (this.images.length > 1) {
this.preloadNextMainImage(0);
}
},
/**
@@ -235,6 +266,65 @@
}, { passive: true });
},
/**
* Bind activity tracking events (mouse move, scroll, touch)
*/
bindActivityTracking: function() {
var self = this;
var throttleDelay = 250; // Throttle to avoid excessive updates
var lastUpdate = 0;
var updateActivity = function() {
var now = Date.now();
if (now - lastUpdate > throttleDelay) {
lastUpdate = now;
self.lastActivityTimestamp = now;
// Resume autoplay if it was paused for inactivity
if (self.isPausedForInactivity && self.isPlaying) {
self.isPausedForInactivity = false;
// Advance to next image now that user is active again
self.advanceImage();
}
}
};
// Track mouse movement
$(document).on('mousemove', updateActivity);
// Track scroll
$(window).on('scroll', updateActivity);
// Track touch (for mobile)
$(document).on('touchstart touchmove', updateActivity);
// Track keyboard activity
$(document).on('keydown', updateActivity);
},
/**
* Check if user has been active within the timeout period
*/
isUserActive: function() {
return (Date.now() - this.lastActivityTimestamp) < this.activityTimeout;
},
/**
* Check if current index is at a page boundary (last image on a thumbnail page)
*/
isAtPageBoundary: function(index) {
// Last image on a page is when (index + 1) % thumbnailsPerPage === 0
// e.g., with 5 per page: index 4, 9, 14, 19...
return (index + 1) % this.thumbnailsPerPage === 0;
},
/**
* Get the page number for a given image index
*/
getPageForIndex: function(index) {
return Math.floor(index / this.thumbnailsPerPage);
},
/**
* Handle swipe start
*/
@@ -335,6 +425,24 @@
if (newIndex >= this.images.length) {
newIndex = 0;
}
// Check if we're crossing a page boundary
var currentPage = this.getPageForIndex(this.currentIndex);
var newPage = this.getPageForIndex(newIndex);
if (newPage !== currentPage) {
// We're about to cross to a new page
// Check if user has been active
if (!this.isUserActive()) {
// User inactive - pause at page boundary until activity resumes
this.isPausedForInactivity = true;
return;
}
// User is active - preload the page after the new page
this.ensurePageLoaded(newPage + 1);
}
this.setMainImage(newIndex, true); // true = use fade
},
@@ -358,6 +466,11 @@
if (newIndex >= this.images.length) newIndex = 0;
}
// Ensure the page for this image and the next page are loaded
var newPage = this.getPageForIndex(newIndex);
this.ensurePageLoaded(newPage);
this.ensurePageLoaded(newPage + 1);
var image = this.images[newIndex];
// Validate image data
@@ -431,6 +544,9 @@
// Ensure the current thumbnail is visible
this.scrollToThumbnail(newIndex);
// Preload the next image for smooth slideshow
this.preloadNextMainImage(newIndex);
},
/**
@@ -449,6 +565,11 @@
return;
}
// Ensure the page for this image is loaded
var page = this.getPageForIndex(index);
this.ensurePageLoaded(page);
this.ensurePageLoaded(page + 1);
var image = this.images[index];
// Validate image data
@@ -513,6 +634,9 @@
// Ensure the current thumbnail is visible
this.scrollToThumbnail(index);
// Preload the next image for smooth slideshow
this.preloadNextMainImage(index);
},
/**
@@ -566,7 +690,11 @@
if (this.thumbnailPage > 0) {
this.thumbnailPage--;
this.scrollThumbnails();
this.preloadPrevThumbnailPage();
// Ensure current and previous page are loaded
this.ensurePageLoaded(this.thumbnailPage);
if (this.thumbnailPage > 0) {
this.ensurePageLoaded(this.thumbnailPage - 1);
}
}
},
@@ -578,7 +706,9 @@
if (this.thumbnailPage < totalPages - 1) {
this.thumbnailPage++;
this.scrollThumbnails();
this.preloadNextThumbnailPage();
// Ensure current and next page are loaded
this.ensurePageLoaded(this.thumbnailPage);
this.ensurePageLoaded(this.thumbnailPage + 1);
}
},
@@ -723,9 +853,12 @@
* Adds loading class and spinner to each thumbnail
*/
setupThumbnailLoading: function() {
var self = this;
this.$thumbnails.each(function() {
var $thumb = $(this);
var $img = $thumb.find('img');
var index = parseInt($thumb.data('index'));
// Add loading state
$thumb.addClass('is-loading');
@@ -736,14 +869,19 @@
}
// Handle image load
if ($img[0].complete) {
if ($img[0].complete && $img[0].naturalWidth > 0) {
$thumb.removeClass('is-loading');
} else if ($img[0].complete) {
// Image complete but no naturalWidth = failed
self.markImageFailed(index, 'thumbnail');
} else {
$img.on('load', function() {
$thumb.removeClass('is-loading');
self.markImageSuccess(index, 'thumbnail');
});
$img.on('error', function() {
$thumb.removeClass('is-loading');
// Keep spinner visible, mark as failed for retry
self.markImageFailed(index, 'thumbnail');
});
}
});
@@ -797,6 +935,243 @@
if (prevPage >= 0) {
this.preloadThumbnailPages(prevPage, 1);
}
},
/**
* Ensure a specific page of images is loaded
* @param {number} page - Page number to load (0-indexed)
*/
ensurePageLoaded: function(page) {
var totalPages = Math.ceil(this.images.length / this.thumbnailsPerPage);
// Don't load if page is out of bounds or already loaded
if (page < 0 || page >= totalPages || this.loadedImagePages[page]) {
return;
}
// Mark as loaded
this.loadedImagePages[page] = true;
// Preload the thumbnail images for this page
this.preloadThumbnailPages(page, 1);
// Also preload the main images for this page
var startIndex = page * this.thumbnailsPerPage;
var endIndex = Math.min(startIndex + this.thumbnailsPerPage, this.images.length);
for (var i = startIndex; i < endIndex; i++) {
this.preloadMainImage(i);
}
},
/**
* Preload a main image by index
* @param {number} index - Image index to preload
*/
preloadMainImage: function(index) {
if (index < 0 || index >= this.images.length) {
return;
}
var image = this.images[index];
if (image && image.url) {
var preloader = new Image();
preloader.src = image.url;
}
},
/**
* Preload the next main image after the given index
* @param {number} currentIndex - Current image index
*/
preloadNextMainImage: function(currentIndex) {
if (this.images.length <= 1) {
return;
}
// Calculate next index with wrap-around
var nextIndex = currentIndex + 1;
if (nextIndex >= this.images.length) {
nextIndex = 0;
}
this.preloadMainImage(nextIndex);
},
/**
* Mark an image as failed and needing retry
* @param {number} index - Image index
* @param {string} type - 'thumbnail' or 'main'
*/
markImageFailed: function(index, type) {
var key = type + '_' + index;
if (!this.failedImages[key]) {
this.failedImages[key] = { retries: 0, seq: 1, type: type, index: index };
}
},
/**
* Mark an image as successfully loaded
* @param {number} index - Image index
* @param {string} type - 'thumbnail' or 'main'
*/
markImageSuccess: function(index, type) {
var key = type + '_' + index;
delete this.failedImages[key];
},
/**
* Start the retry timer for failed images
*/
startRetryTimer: function() {
var self = this;
this.retryTimer = setInterval(function() {
self.retryFailedImages();
}, this.retryInterval);
},
/**
* Retry loading failed images that are visible on current page
*/
retryFailedImages: function() {
var self = this;
var currentPage = this.thumbnailPage;
Object.keys(this.failedImages).forEach(function(key) {
var failed = self.failedImages[key];
// Skip if max retries exceeded
if (failed.retries >= self.maxRetries) {
return;
}
// Check if image is on current page
var imagePage = self.getPageForIndex(failed.index);
if (imagePage !== currentPage && imagePage !== currentPage + 1) {
// Not on current or next page, skip
return;
}
// Retry the image
failed.retries++;
failed.seq++;
if (failed.type === 'thumbnail') {
self.retryThumbnailImage(failed.index, failed.seq);
} else if (failed.type === 'main') {
self.retryMainImage(failed.index, failed.seq);
}
});
},
/**
* Retry loading a thumbnail image
* @param {number} index - Image index
* @param {number} seq - Sequence number for cache busting
*/
retryThumbnailImage: function(index, seq) {
var self = this;
var $thumb = this.$thumbnails.filter('[data-index="' + index + '"]');
var $img = $thumb.find('img');
if (!$img.length) return;
var originalSrc = $img.attr('src');
var newSrc = this.addSeqParam(originalSrc, seq);
// Create a test image to check if it loads
var testImg = new Image();
testImg.onload = function() {
// Success - update the actual image
$img.attr('src', newSrc);
$thumb.removeClass('is-loading');
self.markImageSuccess(index, 'thumbnail');
};
testImg.onerror = function() {
// Still failing - will retry on next interval
};
testImg.src = newSrc;
},
/**
* Retry loading the main gallery image
* @param {number} index - Image index
* @param {number} seq - Sequence number for cache busting
*/
retryMainImage: function(index, seq) {
var self = this;
// Only retry if this is the current image
if (index !== this.currentIndex) {
return;
}
var image = this.images[index];
if (!image || !image.url) return;
var newUrl = this.addSeqParam(image.url, seq);
// Create a test image to check if it loads
var testImg = new Image();
testImg.onload = function() {
// Success - update the main image
self.$mainImage.attr('src', newUrl);
self.$mainImageContainer.removeClass('is-loading');
self.markImageSuccess(index, 'main');
};
testImg.onerror = function() {
// Still failing - will retry on next interval
};
testImg.src = newUrl;
},
/**
* Add or update seq parameter in URL
* @param {string} url - Original URL
* @param {number} seq - Sequence number
* @return {string} URL with seq parameter
*/
addSeqParam: function(url, seq) {
if (!url) return url;
// Remove existing seq param if present
url = url.replace(/([?&])seq=\d+(&|$)/, function(match, p1, p2) {
return p2 ? p1 : '';
});
// Add new seq param
var separator = url.indexOf('?') !== -1 ? '&' : '?';
return url + separator + 'seq=' + seq;
},
/**
* Setup main image error handling
*/
setupMainImageErrorHandling: function() {
var self = this;
// Add spinner container to main image if not present
if (!this.$mainImageContainer.find('.gallery-spinner').length) {
this.$mainImageContainer.append(
'<div class="gallery-spinner" style="display:none;">' +
'<div class="spinner"></div>' +
'</div>'
);
}
// Handle main image errors
this.$mainImage.on('error', function() {
self.$mainImageContainer.addClass('is-loading');
self.$mainImageContainer.find('.gallery-spinner').show();
self.markImageFailed(self.currentIndex, 'main');
});
this.$mainImage.on('load', function() {
self.$mainImageContainer.removeClass('is-loading');
self.$mainImageContainer.find('.gallery-spinner').hide();
self.markImageSuccess(self.currentIndex, 'main');
});
}
};
@@ -319,3 +319,34 @@
color: white;
font-size: 0.875rem;
}
// Main gallery spinner (for loading/retry states)
.gallery-main-image {
position: relative;
&.is-loading {
.gallery-spinner {
display: flex;
}
}
}
.gallery-spinner {
position: absolute;
inset: 0;
display: none;
align-items: center;
justify-content: center;
background: var(--color-bg-card);
border-radius: 0.5rem;
z-index: 5;
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: thumbnail-spin 0.8s linear infinite;
}
}
@@ -21,8 +21,11 @@ if (!$listing_key || !function_exists('mls_get_property')) {
exit;
}
// Check for A/B testing - view=org skips manual override
$skip_manual_override = isset($_GET['view']) && $_GET['view'] === 'org';
// Fetch property from MLS database
$property = mls_get_property($listing_key);
$property = mls_get_property($listing_key, $skip_manual_override);
if (!$property) {
// Property not found - show 404
@@ -33,8 +36,13 @@ if (!$property) {
exit;
}
// Update listing_key to the actual property's key (may differ if manual property overrides MLS)
$listing_key = $property->listing_key;
// Extract property data
$price = $property->list_price;
$status = $property->standard_status;
// Use close_price for Closed properties if available
$price = ($status === 'Closed' && !empty($property->close_price)) ? $property->close_price : $property->list_price;
$bedrooms = $property->bedrooms_total;
$bathrooms = $property->bathrooms_total;
$square_feet = $property->living_area;
@@ -42,7 +50,6 @@ $lot_size = $property->lot_size_area;
$lot_units = $property->lot_size_units;
$year_built = $property->year_built;
$garage = $property->garage_spaces;
$status = $property->standard_status;
$property_type = $property->property_type;
$property_subtype = $property->property_sub_type;
$public_remarks = $property->public_remarks;
@@ -50,6 +57,9 @@ $directions = $property->directions;
$days_on_market = $property->days_on_market;
$listing_id = $property->listing_id;
// Check if this is a manual property
$is_manual_property = strpos($listing_key, 'MANUAL-') === 0;
// Format address
$address_parts = array();
if ($property->street_number) {
@@ -66,15 +76,29 @@ if ($property->unit_number) {
}
$street_address = implode(' ', $address_parts);
$full_address = $street_address;
if ($property->city) {
$full_address .= ', ' . $property->city;
// For manual properties, use full_address if street parts are missing
if (empty($street_address) && !empty($property->full_address)) {
$full_address = $property->full_address;
// Extract street for display (before first comma)
$street_address = explode(',', $property->full_address)[0];
} else {
$full_address = $street_address;
if ($property->city) {
$full_address .= ', ' . $property->city;
}
if ($property->state_or_province) {
$full_address .= ', ' . $property->state_or_province;
}
if ($property->postal_code) {
$full_address .= ' ' . $property->postal_code;
}
}
if ($property->state_or_province) {
$full_address .= ', ' . $property->state_or_province;
}
if ($property->postal_code) {
$full_address .= ' ' . $property->postal_code;
// Display title: use WordPress post title for manual properties, address for MLS
if ($is_manual_property && !empty($property->wp_post_id)) {
$display_title = get_the_title($property->wp_post_id);
} else {
$display_title = $full_address;
}
// Status class
@@ -88,9 +112,11 @@ if ($status === 'Pending') {
// Get all images for this property
$media = function_exists('mls_get_property_media') ? mls_get_property_media($listing_key) : array();
// Check for MLS override (custom featured photo)
$override = function_exists('homeproz_get_mls_override') ? homeproz_get_mls_override($listing_id) : null;
$override_photo = ($override && !empty($override['featured_photo'])) ? $override['featured_photo'] : null;
// Check if request is from a spider/bot - serve placeholder images to reduce API load
$is_spider = function_exists('homeproz_is_spider') && homeproz_is_spider();
$spider_placeholder = $is_spider && function_exists('homeproz_get_spider_placeholder_image')
? homeproz_get_spider_placeholder_image()
: '';
// Format lot size
$lot_display = '';
@@ -103,14 +129,42 @@ if ($lot_size) {
// Agent info
$agent_name = $property->list_agent_name;
$agent_mls_id = $property->list_agent_mls_id;
$office_name = $property->list_office_name;
// Check if this is a HomeProz listing
$is_homeproz_listing = false;
if ($office_name) {
$is_homeproz_listing = !empty($property->is_homeproz);
if (!$is_homeproz_listing && $office_name) {
$is_homeproz_listing = (stripos($office_name, 'HomeProz') !== false || stripos($office_name, 'Home Proz') !== false);
}
// For HomeProz listings, look up the agent from Agent CPT
$homeproz_agent_id = null;
// Manual properties: agent is directly linked by post ID
if (!empty($property->list_agent_post_id)) {
$homeproz_agent_id = (int) $property->list_agent_post_id;
}
// MLS properties: look up agent by MLS ID
elseif ($is_homeproz_listing && $agent_mls_id) {
$agent_query = new WP_Query(array(
'post_type' => 'agent',
'posts_per_page' => 1,
'post_status' => 'publish',
'meta_query' => array(
array(
'key' => 'agent_mls_id',
'value' => $agent_mls_id,
'compare' => '=',
),
),
));
if ($agent_query->have_posts()) {
$homeproz_agent_id = $agent_query->posts[0]->ID;
}
wp_reset_postdata();
}
// Set page title to property address
$mls_page_title = $full_address;
if ($price) {
@@ -124,6 +178,19 @@ add_filter('pre_get_document_title', function() use ($mls_page_title) {
return $mls_page_title . ' - HomeProz';
}, 99);
// Set social sharing meta (use REAL image, not spider placeholder)
$social_image_url = '';
if (!empty($media) && function_exists('mls_get_image_url')) {
$social_image_url = mls_get_image_url($listing_key, $media[0]->media_order, 'full');
}
if (function_exists('homeproz_set_mls_property_social_meta')) {
homeproz_set_mls_property_social_meta($property, $social_image_url, $full_address);
}
// Explicitly set the canonical URL for this property
if (function_exists('homeproz_set_social_meta')) {
homeproz_set_social_meta('url', home_url('/properties/?listing=' . $listing_key));
}
get_header();
?>
@@ -131,7 +198,7 @@ get_header();
<div class="container">
<!-- Property Address Header -->
<header class="property-address-header">
<h1 class="property-address-title"><?php echo esc_html($full_address); ?></h1>
<h1 class="property-address-title"><?php echo esc_html($display_title); ?></h1>
</header>
<div class="single-property-layout">
@@ -140,29 +207,28 @@ get_header();
<!-- Gallery -->
<?php
$image_count = count($media);
$has_override_photo = !empty($override_photo);
if ($image_count > 0 || $has_override_photo) :
if ($image_count > 0) :
// Build images array for JS
$gallery_images = array();
// Prepend override photo if available
if ($has_override_photo) {
$gallery_images[] = array(
'url' => $override_photo['url'],
'alt' => $street_address . ' - Featured Photo',
);
}
// Add MLS images
// Add MLS images (or placeholder for bots)
foreach ($media as $item) {
$full_url = function_exists('mls_get_image_url')
? mls_get_image_url($listing_key, $item->media_order, 'full')
: '';
if ($full_url) {
if ($is_spider && $spider_placeholder) {
// Serve placeholder to bots
$gallery_images[] = array(
'url' => $full_url,
'url' => $spider_placeholder,
'alt' => $street_address . ' - Photo ' . $item->media_order,
);
} else {
$full_url = function_exists('mls_get_image_url')
? mls_get_image_url($listing_key, $item->media_order, 'full')
: '';
if ($full_url) {
$gallery_images[] = array(
'url' => $full_url,
'alt' => $street_address . ' - Photo ' . $item->media_order,
);
}
}
}
$image_count = count($gallery_images);
@@ -200,20 +266,18 @@ get_header();
// Build thumbnails array (same order as gallery_images)
$thumbnail_images = array();
// Prepend override photo thumbnail if available
if ($has_override_photo) {
$thumbnail_images[] = isset($override_photo['sizes']['thumbnail'])
? $override_photo['sizes']['thumbnail']
: (isset($override_photo['sizes']['medium']) ? $override_photo['sizes']['medium'] : $override_photo['url']);
}
// Add MLS thumbnails
// Add MLS thumbnails (or placeholder for bots)
foreach ($media as $item) {
$thumb_url = function_exists('mls_get_image_url')
? mls_get_image_url($listing_key, $item->media_order, 'thumb')
: '';
if ($thumb_url) {
$thumbnail_images[] = $thumb_url;
if ($is_spider && $spider_placeholder) {
// Serve placeholder to bots
$thumbnail_images[] = $spider_placeholder;
} else {
$thumb_url = function_exists('mls_get_image_url')
? mls_get_image_url($listing_key, $item->media_order, 'thumb')
: '';
if ($thumb_url) {
$thumbnail_images[] = $thumb_url;
}
}
}
?>
@@ -304,44 +368,44 @@ get_header();
<ul class="property-specs-grid">
<?php if ($bedrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bedrooms</span>
<span class="spec-value"><?php echo esc_html($bedrooms); ?></span>
<span class="spec-label"><?php echo $bedrooms == 1 ? 'Bed' : 'Beds'; ?></span>
</li>
<?php endif; ?>
<?php if ($bathrooms) : ?>
<li class="spec-item">
<span class="spec-label">Bathrooms</span>
<span class="spec-value"><?php echo esc_html($bathrooms); ?></span>
<span class="spec-label"><?php echo $bathrooms == 1 ? 'Bath' : 'Baths'; ?></span>
</li>
<?php endif; ?>
<?php if ($square_feet) : ?>
<li class="spec-item">
<span class="spec-label">Square Feet</span>
<span class="spec-value"><?php echo esc_html(number_format($square_feet)); ?></span>
<span class="spec-label">Sq Ft</span>
</li>
<?php endif; ?>
<?php if ($lot_display) : ?>
<li class="spec-item">
<span class="spec-label">Lot Size</span>
<span class="spec-value"><?php echo esc_html($lot_display); ?></span>
<span class="spec-label">Lot</span>
</li>
<?php endif; ?>
<?php if ($year_built) : ?>
<li class="spec-item">
<span class="spec-label">Year Built</span>
<span class="spec-label">Built</span>
<span class="spec-value"><?php echo esc_html($year_built); ?></span>
</li>
<?php endif; ?>
<?php if ($garage) : ?>
<li class="spec-item">
<span class="spec-label">Garage</span>
<span class="spec-value"><?php echo esc_html($garage); ?> Car</span>
<span class="spec-value"><?php echo esc_html($garage); ?></span>
<span class="spec-label">Car Garage</span>
</li>
<?php endif; ?>
<?php if ($days_on_market !== null) : ?>
<li class="spec-item">
<span class="spec-label">Days on Market</span>
<span class="spec-value"><?php echo esc_html($days_on_market); ?></span>
<span class="spec-label">Days Listed</span>
</li>
<?php endif; ?>
</ul>
@@ -353,7 +417,7 @@ get_header();
<section class="property-description">
<h2 class="section-title">Description</h2>
<div class="property-full-desc">
<?php echo wpautop(esc_html($public_remarks)); ?>
<?php echo homeproz_format_property_description($public_remarks); ?>
</div>
</section>
<?php endif; ?>
@@ -367,6 +431,10 @@ get_header();
<p class="property-address-link">
<a href="<?php echo esc_url($google_maps_url); ?>" target="_blank" rel="noopener noreferrer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
<?php echo esc_html($full_address); ?>
</a>
</p>
@@ -379,7 +447,7 @@ get_header();
<iframe
src="https://www.google.com/maps?q=<?php echo urlencode($full_address); ?>&z=15&output=embed"
width="100%"
height="300"
height="500"
style="border:0;"
allowfullscreen=""
loading="lazy"
@@ -412,38 +480,57 @@ get_header();
</div>
<!-- Contact Agent -->
<div class="sidebar-widget property-agent-widget">
<?php if ($is_homeproz_listing) : ?>
<h3 class="widget-title">Your HomeProz Agent</h3>
<?php if ($agent_name) : ?>
<p class="agent-name"><?php echo esc_html($agent_name); ?></p>
<?php endif; ?>
<?php else : ?>
<h3 class="widget-title">Interested in This Property?</h3>
<p class="agent-intro">Our team can help you learn more about this listing and schedule a showing.</p>
<?php endif; ?>
<?php if ($is_homeproz_listing && $homeproz_agent_id) : ?>
<?php
$inquiry_url = add_query_arg('listing', $listing_key, home_url('/property-inquiry/'));
$button_text = $is_homeproz_listing ? 'Schedule a Showing' : 'Inquire About This Property';
// Use the agent card template for HomeProz listings with a matched agent
get_template_part('template-parts/property/property-agent', null, array(
'agent' => $homeproz_agent_id,
'property_id' => 0, // No WP post ID for MLS properties
'property_title' => $full_address,
'listing_key' => $listing_key,
));
?>
<a href="<?php echo esc_url($inquiry_url); ?>" class="btn btn-primary btn-block">
<?php echo esc_html($button_text); ?>
</a>
</div>
<?php else : ?>
<div class="sidebar-widget property-agent-widget">
<?php if ($is_homeproz_listing) : ?>
<h3 class="widget-title">Your HomeProz Agent</h3>
<?php if ($agent_name) : ?>
<p class="agent-name"><?php echo esc_html($agent_name); ?></p>
<?php endif; ?>
<?php else : ?>
<h3 class="widget-title">Interested in This Property?</h3>
<p class="agent-intro">Our team can help you learn more about this listing and schedule a showing.</p>
<?php endif; ?>
<?php
$inquiry_url = add_query_arg('listing', $listing_key, home_url('/property-inquiry/'));
$button_text = $is_homeproz_listing ? 'Schedule a Showing' : 'Inquire About This Property';
?>
<a href="<?php echo esc_url($inquiry_url); ?>" class="btn btn-primary btn-block">
<?php echo esc_html($button_text); ?>
</a>
</div>
<?php endif; ?>
<!-- Share -->
<div class="sidebar-widget property-share-widget">
<h3 class="widget-title">Share This Property</h3>
<div class="share-buttons">
<a href="mailto:?subject=<?php echo rawurlencode($full_address); ?>&body=<?php echo rawurlencode(get_permalink()); ?>"
<?php $share_url = home_url('/properties/?listing=' . $listing_key); ?>
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo rawurlencode($share_url); ?>"
class="share-btn share-facebook" aria-label="Share on Facebook" target="_blank" rel="noopener noreferrer">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/>
</svg>
</a>
<a href="mailto:?subject=<?php echo rawurlencode($full_address); ?>&body=<?php echo rawurlencode($share_url); ?>"
class="share-btn share-email" aria-label="Share via Email">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</a>
<button class="share-btn share-copy" data-url="<?php echo esc_url(home_url('/properties/?listing=' . $listing_key)); ?>" aria-label="Copy Link">
<button class="share-btn share-copy" data-url="<?php echo esc_url($share_url); ?>" aria-label="Copy Link">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
@@ -0,0 +1,399 @@
/**
* Single Property MLS Styles
*
* @package HomeProz
*/
.Single_Property_MLS {
padding: 2rem 0 4rem;
// Address Header
.property-address-header {
margin-bottom: 2rem;
}
.property-address-title {
font-size: 2rem;
margin-bottom: 0;
@media (max-width: 768px) {
font-size: 1.5rem;
}
}
// Two Column Layout
.single-property-layout {
display: grid;
grid-template-columns: 1fr 380px;
gap: 2rem;
align-items: start;
@media (max-width: 1024px) {
grid-template-columns: 1fr;
}
}
// Main Content
.single-property-content {
min-width: 0;
}
// Sidebar
.single-property-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
@media (max-width: 1024px) {
order: -1;
}
}
// Sidebar Widgets
.sidebar-widget {
background-color: var(--color-bg-card);
border-radius: 0.5rem;
padding: 1.5rem;
}
.widget-title {
font-size: 1.125rem;
margin-bottom: 1rem;
}
// Property Header Widget (Price, Status, MLS#)
.property-header-widget {
.property-header-top {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1rem;
}
.property-title {
font-family: var(--font-display);
font-size: 2.25rem;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.property-mls {
font-size: 0.875rem;
color: var(--color-text-muted);
margin: 0;
}
}
// Agent Business Card - Side by side layout with edge-to-edge photo
.agent-business-card {
.agent-card-inner {
display: flex;
gap: 0;
align-items: stretch;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
margin-bottom: 1rem;
overflow: hidden;
}
.agent-card-photo {
flex-shrink: 0;
width: 126px;
img {
width: 100%;
height: 100%;
min-height: 126px;
object-fit: cover;
display: block;
image-rendering: -webkit-optimize-contrast; // Safari
image-rendering: smooth; // Modern browsers
-ms-interpolation-mode: bicubic; // Legacy IE
}
.agent-photo-link {
display: block;
height: 100%;
&:hover img {
opacity: 0.9;
}
}
}
.agent-photo-placeholder {
width: 100%;
height: 100%;
min-height: 126px;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-card);
color: var(--color-text-muted);
&.office {
background-color: rgba(159, 55, 48, 0.15);
color: var(--color-accent);
}
}
.agent-card-details {
flex: 1;
min-width: 0;
padding: 1rem 1.25rem;
display: flex;
flex-direction: column;
justify-content: center;
.agent-name {
font-weight: 600;
font-size: 1.0625rem;
color: var(--color-text);
margin: 0 0 0.25rem;
line-height: 1.3;
a {
color: inherit;
&:hover {
color: var(--color-accent-light);
}
}
}
.agent-title {
font-size: 0.8125rem;
color: var(--color-text-muted);
margin: 0 0 0.625rem;
}
.agent-phone {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.875rem;
color: var(--color-text-muted);
margin: 0;
svg {
flex-shrink: 0;
color: var(--color-accent);
}
a {
color: var(--color-text);
&:hover {
color: var(--color-accent-light);
}
}
}
}
.agent-card-actions {
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
svg {
flex-shrink: 0;
}
}
}
// Disabled agent styling
&.agent-disabled {
.agent-card-inner {
opacity: 0.85;
}
.agent-card-photo img {
filter: grayscale(30%);
}
}
}
// Legacy Property Agent Widget (keep for backwards compatibility)
.property-agent-widget {
.agent-name {
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.5rem;
}
.agent-intro {
font-size: 0.875rem;
margin-bottom: 1rem;
}
.btn-block {
display: block;
width: 100%;
text-align: center;
}
}
// Share Widget
.property-share-widget {
.share-buttons {
display: flex;
gap: 0.75rem;
}
.share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
color: var(--color-text-muted);
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background-color: var(--color-accent);
border-color: var(--color-accent);
color: white;
transform: translateY(-2px);
}
&:active {
transform: translateY(0);
}
&.copied {
background-color: var(--color-success);
border-color: var(--color-success);
color: white;
}
// Facebook brand styling
&.share-facebook {
background-color: #1877F2;
border-color: #1877F2;
color: white;
&:hover {
background-color: #0d65d9;
border-color: #0d65d9;
}
}
}
}
// Section Titles
.section-title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
// Property Specs Section - Horizontal Stat Bar
.property-specs-section {
margin-bottom: 2rem;
}
.property-specs-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0;
list-style: none;
padding: 1rem 1.25rem;
margin: 0;
background-color: var(--color-bg-card);
border-radius: 0.5rem;
}
.spec-item {
display: flex;
align-items: baseline;
gap: 0.375rem;
padding: 0.25rem 0;
// Separator dot between items
&:not(:last-child)::after {
content: '';
display: inline-block;
width: 4px;
height: 4px;
background-color: var(--color-text-muted);
border-radius: 50%;
margin-left: 1rem;
margin-right: 1rem;
vertical-align: middle;
position: relative;
top: -2px;
}
}
.spec-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
}
.spec-label {
font-size: 0.875rem;
color: var(--color-text-muted);
}
// Description Section
.property-description {
margin-bottom: 2rem;
.property-full-desc {
line-height: 1.7;
p {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}
}
// Location Section
.property-location-section {
margin-bottom: 2rem;
}
.property-address-link {
margin-bottom: 1rem;
a {
display: inline-flex;
align-items: center;
gap: 0.5rem;
&:hover {
text-decoration: underline;
}
}
}
.property-directions-text {
font-size: 0.9375rem;
color: var(--color-text-muted);
margin-bottom: 1rem;
}
.property-location-map {
border-radius: 0.5rem;
overflow: hidden;
iframe {
display: block;
}
}
}
@@ -1,710 +0,0 @@
/**
* Single Property Styles
*
* @package HomeProz
*/
// Breadcrumbs
.breadcrumbs {
background-color: var(--color-bg-card);
padding: 1rem 0;
border-bottom: 1px solid var(--color-border);
}
.breadcrumb-list {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
list-style: none;
margin: 0;
padding: 0;
font-size: 0.875rem;
li {
display: flex;
align-items: center;
&:not(:last-child)::after {
content: '/';
margin-left: 0.5rem;
color: var(--color-text-muted);
}
a {
color: var(--color-text-muted);
text-decoration: none;
&:hover {
color: var(--color-accent-light);
}
}
&:last-child {
color: var(--color-text);
}
}
}
// Main Layout
.single-property-main {
padding-bottom: 4rem;
}
// Property Address Header
.property-address-header {
padding-top: 2rem;
padding-bottom: 1.5rem;
}
.property-address-title {
font-size: 2rem;
margin-bottom: 0;
color: var(--color-text);
@media (max-width: 768px) {
font-size: 1.5rem;
}
}
.single-property-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
@media (min-width: 1024px) {
grid-template-columns: 1fr 350px;
gap: 3rem;
}
}
// Section Title
.section-title {
font-size: 1.25rem;
margin-bottom: 1.25rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
// Property Specs Grid
.property-specs-section {
margin-bottom: 2rem;
@media (max-width: 1023px) {
background-color: var(--color-bg-card);
padding: 1.5rem;
border-radius: 0.5rem;
}
}
.property-specs-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem 1.5rem;
list-style: none;
margin: 0;
padding: 0;
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
}
.property-specs-grid .spec-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
border-bottom: 1px solid var(--color-border);
@media (min-width: 1024px) {
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: 0.25rem;
padding: 1rem;
background-color: var(--color-bg-card);
border-radius: 0.25rem;
border-bottom: none;
}
}
.spec-label {
font-size: 0.875rem;
color: var(--color-text-muted);
@media (min-width: 1024px) {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
.spec-value {
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
@media (min-width: 1024px) {
font-size: 1.25rem;
}
}
// Documents
.property-documents {
margin-bottom: 2rem;
}
.documents-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
list-style: none;
margin: 0;
padding: 0;
}
.document-item {
margin: 0;
}
.document-link {
display: inline-flex;
align-items: center;
gap: 0.5rem;
svg {
flex-shrink: 0;
}
}
.document-ext {
font-size: 0.75rem;
opacity: 0.7;
text-transform: uppercase;
}
// Description
.property-description {
margin-bottom: 2rem;
}
.property-short-desc {
font-size: 1.125rem;
color: var(--color-text);
margin-bottom: 1rem;
}
.property-full-desc {
color: var(--color-text-muted);
line-height: 1.7;
}
// Features List
.property-features {
margin-bottom: 2rem;
}
.features-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
list-style: none;
margin: 0;
padding: 0;
@media (min-width: 640px) {
grid-template-columns: repeat(3, 1fr);
}
}
.feature-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9375rem;
color: var(--color-text-muted);
svg {
flex-shrink: 0;
color: var(--color-success);
}
}
// Sidebar (no sticky - stays in document flow)
.single-property-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
@media (min-width: 1024px) {
align-self: start;
}
}
// Sidebar Widgets
.sidebar-widget {
background-color: var(--color-bg-card);
border-radius: 0.5rem;
padding: 1.5rem;
}
.widget-title {
font-family: 'Times New Roman', Times, serif;
font-size: 18px;
font-weight: 700;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid var(--color-border);
}
// Property Header Widget (in sidebar)
.property-header-widget {
.property-header-top {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.75rem;
}
.property-title {
font-family: var(--font-display);
font-size: 1.75rem;
color: var(--color-text);
margin-bottom: 0.5rem;
line-height: 1.2;
}
.property-mls {
font-size: 0.8125rem;
color: var(--color-sold);
margin-bottom: 0;
}
}
// Property type badges
.badge-type-residential {
background-color: #93C5FD;
color: #1E3A5F;
}
.badge-type-commercial {
background-color: var(--color-accent);
color: white;
}
.badge-type-land {
background-color: #86EFAC;
color: #14532D;
}
.badge-type-multi-family {
background-color: #C4B5FD;
color: #3B1D66;
}
// Property Documents Widget
.property-documents-widget {
.sidebar-documents-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.document-btn {
display: flex;
align-items: center;
gap: 0.625rem;
width: 100%;
padding: 0.75rem 1rem;
background-color: var(--color-accent);
border: none;
border-radius: 0.25rem;
color: white;
text-decoration: none;
font-weight: 600;
font-size: 0.875rem;
cursor: pointer;
&:hover {
background-color: var(--color-accent-hover);
color: white;
}
svg {
flex-shrink: 0;
stroke: white;
}
}
.document-btn-label {
flex: 1;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.document-btn-ext {
flex-shrink: 0;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
background-color: rgba(0, 0, 0, 0.2);
color: white;
padding: 0.1875rem 0.375rem;
border-radius: 0.1875rem;
letter-spacing: 0.03em;
}
}
// Agent Card
.agent-card {
padding: 1.5rem;
}
// Agent Info Link (clickable block)
.agent-info-link {
display: block;
text-decoration: none;
color: inherit;
border-radius: 0.375rem;
margin: -0.5rem -0.5rem 1rem -0.5rem;
padding: 0.5rem;
&:hover {
background-color: var(--color-bg-dark);
.agent-name {
color: var(--color-accent-light);
}
}
.agent-info {
margin-bottom: 0;
}
}
.agent-card-header {
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--color-border);
}
.agent-card-title {
font-size: 1.125rem;
margin-bottom: 0;
}
.agent-info {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.agent-avatar {
flex-shrink: 0;
background-color: #000;
border-radius: 50%;
img {
width: 64px;
height: 64px;
border-radius: 50%;
object-fit: cover;
}
}
.agent-avatar-placeholder {
width: 64px;
height: 64px;
display: flex;
align-items: center;
justify-content: center;
background-color: #000;
border-radius: 50%;
color: var(--color-text-muted);
}
.agent-name {
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.agent-role {
font-size: 0.8125rem;
color: var(--color-text-muted);
margin-bottom: 0;
}
.agent-contact {
display: flex;
flex-direction: column;
gap: 0.75rem;
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: 100%;
}
}
.agent-card-footer {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border);
}
.agent-card-note {
font-size: 0.8125rem;
color: var(--color-text-muted);
text-align: center;
margin-bottom: 0;
}
// Property Inquiry Card
.property-inquiry-card {
.inquiry-note {
font-size: 0.9375rem;
color: var(--color-text-muted);
margin-bottom: 1rem;
line-height: 1.5;
}
.btn-block {
display: flex;
width: 100%;
text-align: center;
justify-content: center;
}
}
// Generic Contact Card (no agent)
.generic-contact-card {
.contact-note {
font-size: 0.9375rem;
color: var(--color-text-muted);
margin-bottom: 1.25rem;
line-height: 1.5;
}
.generic-contact-actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.btn-block {
display: flex;
width: 100%;
text-align: center;
justify-content: center;
}
}
// Body class for lightbox
body.lightbox-open {
overflow: hidden;
}
// MLS Single Property Styles
.Single_Property_MLS {
// Breadcrumb
.property-breadcrumb {
padding: 1rem 0;
font-size: 0.875rem;
color: var(--color-text-muted);
a {
color: var(--color-text-muted);
text-decoration: none;
&:hover {
color: var(--color-accent-light);
}
}
.separator {
margin: 0 0.5rem;
}
.current {
color: var(--color-text);
}
}
// Gallery Section
.property-gallery-section {
margin-bottom: 2rem;
}
.property-gallery-main {
border-radius: 0.5rem;
overflow: hidden;
background-color: var(--color-bg-card);
margin-bottom: 1rem;
.gallery-main-image {
width: 100%;
height: auto;
max-height: 500px;
object-fit: cover;
display: block;
}
.gallery-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 300px;
color: var(--color-text-muted);
}
}
.property-gallery-thumbs {
display: flex;
gap: 0.5rem;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.gallery-thumb {
flex-shrink: 0;
width: 80px;
height: 60px;
border-radius: 0.25rem;
overflow: hidden;
border: 2px solid transparent;
cursor: pointer;
padding: 0;
background: none;
&.is-active {
border-color: var(--color-accent);
}
&:hover {
border-color: var(--color-accent-light);
}
img {
width: 100%;
height: 100%;
object-fit: cover;
}
&.gallery-more {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-card);
color: var(--color-text);
font-weight: 600;
font-size: 0.875rem;
}
}
// Location and Directions Section
.property-location-section {
margin-bottom: 2rem;
}
.property-address-link {
margin-bottom: 0.75rem;
a {
color: #ef4444;
text-decoration: none;
font-weight: 700;
&:hover {
text-decoration: underline;
}
}
}
.property-directions-text {
color: var(--color-text-muted);
line-height: 1.6;
margin-bottom: 1rem;
}
.property-location-map {
border-radius: 0.5rem;
overflow: hidden;
iframe {
display: block;
}
}
// Agent Widget
.property-agent-widget {
.agent-name {
font-weight: 600;
margin-bottom: 0.25rem;
}
.office-name {
font-size: 0.875rem;
color: var(--color-text-muted);
margin-bottom: 1rem;
}
.agent-intro {
font-size: 0.875rem;
color: var(--color-text-muted);
margin-bottom: 1rem;
line-height: 1.5;
}
}
// Share Widget
.property-share-widget {
.share-buttons {
display: flex;
gap: 0.75rem;
}
.share-btn {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 0.375rem;
background-color: var(--color-bg-dark);
color: var(--color-text-muted);
border: none;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s, color 0.2s;
&:hover {
background-color: var(--color-accent);
color: white;
}
&.copied {
background-color: var(--color-success);
color: white;
}
}
}
// Badge type generic
.badge-type {
background-color: var(--color-bg-dark);
color: var(--color-text);
}
}