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:
@@ -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">
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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) : '—';
|
||||
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 '—';
|
||||
}
|
||||
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');
|
||||
|
||||
Regular → Executable
@@ -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
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Regular → Executable
@@ -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
|
||||
|
||||
|
||||
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
Generated
Vendored
Regular → Executable
wp-content/themes/homeproz/node_modules/@rollup/rollup-linux-arm64-musl/rollup.linux-arm64-musl.node
Generated
Vendored
Regular → Executable
@@ -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
|
||||
|
||||
Regular → Executable
Regular → Executable
@@ -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);
|
||||
|
||||
Executable
+126
@@ -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();
|
||||
Executable
+181
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
Regular → Executable
+562
-39
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Regular → Executable
+101
-89
@@ -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>
|
||||
|
||||
+47
-28
@@ -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"/>
|
||||
|
||||
+399
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user