UI/UX improvements: gallery autoplay, clickable cards, footer legal section

Property details page:
- Move address to header above gallery
- Add property type badges (blue residential, red commercial)
- Gallery autoplay with play/pause button, 5-second interval
- Fade transitions for autoplay, slide transitions for swipe
- Thumbnail navigation with sync
- Swipe support in gallery and lightbox
- Widget titles: 18px Times New Roman bold
- Remove breadcrumbs

Layout and styling:
- Container width: 1400px
- Contact page map 50% taller (375px)
- Contact info labels: Times New Roman 16px
- Agent photo backgrounds solid black
- CTA accent button hover: black text

Clickable components:
- Service cards fully clickable with stretched links
- Resource cards fully clickable with stretched links
- Addresses link to Google Maps (contact page, footer)

Footer updates:
- Add Send Us a Message link with paper airplane icon
- Replace credentials with legal section
- Privacy Policy, Fair Housing, MLS Disclaimer, Brokerage Disclosure links
- Credits: Web Design by HansonXyz

Other:
- Install Classic Editor plugin

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-01 00:24:34 -06:00
parent 837b3219fb
commit cbeca7c6d8
42 changed files with 4593 additions and 668 deletions
+8 -8
View File
@@ -44,13 +44,13 @@ $agent_count = count($agents_data);
?>
<main id="primary" class="site-main Agents_Archive">
<?php
// Hero Section - title only, subtitle below in content area
get_template_part('template-parts/components/hero-section', null, array(
'title' => 'Our Team',
'size' => 'small',
));
?>
<!-- Archive Hero -->
<section class="archive-hero">
<div class="container">
<h1 class="archive-hero-title">Our Team</h1>
<p class="archive-hero-subtitle">Meet the dedicated professionals ready to help you with all your real estate needs.</p>
</div>
</section>
<section class="agents-section">
<div class="container">
@@ -122,7 +122,7 @@ $agent_count = count($agents_data);
<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>View Profile</span>
<span>Profile</span>
</a>
</div>
</article>
+112 -149
View File
@@ -14,11 +14,12 @@ if (!defined('ABSPATH')) {
get_header();
// Check if map view is enabled
$show_map = isset($_GET['view']) && $_GET['view'] === 'map';
// Map view is default on desktop, grid view requires ?view=grid
$show_map = !isset($_GET['view']) || $_GET['view'] !== 'grid';
$view_class = $show_map ? 'is-map-view' : 'is-grid-view';
?>
<main id="primary" class="site-main property-archive-main">
<main id="primary" class="site-main property-archive-main <?php echo esc_attr($view_class); ?>">
<?php
// Get hero content from Theme Options
$hero_title = get_field('properties_hero_title', 'option') ?: 'Find Your Perfect Property';
@@ -42,166 +43,128 @@ $show_map = isset($_GET['view']) && $_GET['view'] === 'map';
<!-- Filters -->
<?php get_template_part('template-parts/property/property-filters'); ?>
<!-- View Toggle -->
<div class="view-toggle">
<a href="<?php echo esc_url(remove_query_arg('view')); ?>" class="view-toggle-btn <?php echo !$show_map ? 'active' : ''; ?>">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
</svg>
<span>Grid</span>
</a>
<a href="<?php echo esc_url(add_query_arg('view', 'map')); ?>" class="view-toggle-btn <?php echo $show_map ? 'active' : ''; ?>">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
<span>Map</span>
</a>
<!-- Map + List View (hidden below 1024px via CSS) -->
<div class="property-map-layout">
<div class="property-map-container">
<!-- View Toggle (inside map column) -->
<div class="view-toggle">
<a href="<?php echo esc_url(add_query_arg('view', 'grid')); ?>" class="view-toggle-btn view-toggle-grid">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
</svg>
<span>Grid</span>
</a>
<a href="<?php echo esc_url(remove_query_arg('view')); ?>" class="view-toggle-btn view-toggle-map active">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
<span>Map</span>
</a>
</div>
<div id="property-map" class="property-map">
<!-- Leaflet map will be initialized here -->
</div>
</div>
<div class="property-list-container">
<div id="property-results" class="property-results-map">
<?php get_template_part('template-parts/property/property-results'); ?>
</div>
</div>
</div>
<?php if ($show_map) : ?>
<!-- Map + List View -->
<div class="property-map-layout">
<div class="property-map-container">
<div id="property-map" class="property-map">
<!-- Leaflet map will be initialized here -->
</div>
</div>
<div class="property-list-container">
<div id="property-results">
<?php get_template_part('template-parts/property/property-results'); ?>
</div>
</div>
<!-- Grid View (shown below 1024px, or when grid selected above 1024px) -->
<div class="grid-view-container">
<div class="view-toggle">
<a href="<?php echo esc_url(add_query_arg('view', 'grid')); ?>" class="view-toggle-btn view-toggle-grid active">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="7" height="7"/>
<rect x="14" y="3" width="7" height="7"/>
<rect x="3" y="14" width="7" height="7"/>
<rect x="14" y="14" width="7" height="7"/>
</svg>
<span>Grid</span>
</a>
<a href="<?php echo esc_url(remove_query_arg('view')); ?>" class="view-toggle-btn view-toggle-map">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
<span>Map</span>
</a>
</div>
<?php else : ?>
<!-- Grid View -->
<div id="property-results">
<div id="property-results-grid" class="property-results-grid">
<?php get_template_part('template-parts/property/property-results'); ?>
</div>
<?php endif; ?>
</div>
</div>
</main>
<?php if ($show_map) : ?>
<?php
// Always load map data for responsive switching
$map_properties = new WP_Query(array(
'post_type' => 'property',
'posts_per_page' => -1,
));
$markers = array();
$city_coords = array(
'Albert Lea' => array(43.6480, -93.3685),
'Austin' => array(43.6666, -92.9746),
'Glenville' => array(43.5733, -93.2779),
'Emmons' => array(43.5013, -93.4896),
'Clarks Grove' => array(43.7627, -93.3196),
'Alden' => array(43.6719, -93.5768),
'Hartland' => array(43.8030, -93.4846),
'Geneva' => array(43.8255, -93.2682),
'Owatonna' => array(44.0838, -93.2260),
'Faribault' => array(44.2949, -93.2688),
'Rochester' => array(44.0234, -92.4699),
'Mankato' => array(44.1636, -93.9994),
);
if ($map_properties->have_posts()) :
while ($map_properties->have_posts()) :
$map_properties->the_post();
$city = get_field('city');
$price = get_field('property_price');
$address = get_field('street_address');
// Get coords for city, default to Albert Lea
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea'];
// Add small random offset (seeded by post ID for consistency)
srand(get_the_ID());
$lat = $coords[0] + (rand(-50, 50) / 10000);
$lng = $coords[1] + (rand(-50, 50) / 10000);
$markers[] = array(
'id' => get_the_ID(),
'lat' => $lat,
'lng' => $lng,
'title' => get_the_title(),
'price' => '$' . number_format($price),
'address' => $address . ', ' . $city,
'url' => get_permalink(),
);
endwhile;
wp_reset_postdata();
endif;
?>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
(function($) {
// Wait for DOM
$(document).ready(function() {
// Initialize map centered on Albert Lea area
var map = L.map('property-map').setView([43.6480, -93.3685], 10);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(map);
// Property data from PHP
var properties = [
<?php
// Get all properties for map markers
$map_properties = new WP_Query(array(
'post_type' => 'property',
'posts_per_page' => -1,
'tax_query' => array(
array(
'taxonomy' => 'property_status',
'field' => 'slug',
'terms' => array('active', 'pending'),
),
),
));
$markers = array();
if ($map_properties->have_posts()) :
while ($map_properties->have_posts()) :
$map_properties->the_post();
$city = get_field('city');
$price = get_field('property_price');
$address = get_field('street_address');
// Approximate coordinates based on city (can be enhanced with geocoding)
$city_coords = array(
'Albert Lea' => array(43.6480, -93.3685),
'Austin' => array(43.6666, -92.9746),
'Glenville' => array(43.5733, -93.2779),
'Emmons' => array(43.5013, -93.4896),
'Clarks Grove' => array(43.7627, -93.3196),
'Alden' => array(43.6719, -93.5768),
'Hartland' => array(43.8030, -93.4846),
'Geneva' => array(43.8255, -93.2682),
'Owatonna' => array(44.0838, -93.2260),
'Faribault' => array(44.2949, -93.2688),
'Rochester' => array(44.0234, -92.4699),
'Mankato' => array(44.1636, -93.9994),
);
// Get coords for city, default to Albert Lea
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea'];
// Add small random offset to prevent overlapping markers
$lat = $coords[0] + (rand(-50, 50) / 10000);
$lng = $coords[1] + (rand(-50, 50) / 10000);
$markers[] = array(
'id' => get_the_ID(),
'lat' => $lat,
'lng' => $lng,
'title' => get_the_title(),
'price' => '$' . number_format($price),
'address' => $address . ', ' . $city,
'url' => get_permalink(),
);
endwhile;
wp_reset_postdata();
endif;
// Output JSON
foreach ($markers as $i => $marker) {
echo json_encode($marker);
if ($i < count($markers) - 1) echo ',';
}
?>
];
// Custom marker icon
var propertyIcon = L.divIcon({
className: 'property-marker',
html: '<div class="marker-pin"></div>',
iconSize: [30, 40],
iconAnchor: [15, 40],
popupAnchor: [0, -40]
});
// Add markers for each property
properties.forEach(function(prop) {
if (prop.lat && prop.lng) {
var marker = L.marker([prop.lat, prop.lng], {icon: propertyIcon}).addTo(map);
marker.bindPopup(
'<div class="map-popup">' +
'<strong>' + prop.price + '</strong><br>' +
'<span>' + prop.address + '</span><br>' +
'<a href="' + prop.url + '">View Details</a>' +
'</div>'
);
}
});
// Fit bounds if we have properties
if (properties.length > 0) {
var bounds = L.latLngBounds(properties.map(function(p) { return [p.lat, p.lng]; }));
map.fitBounds(bounds, { padding: [50, 50] });
}
});
})(jQuery);
var homeprozMapData = {
properties: <?php echo json_encode($markers); ?>,
isMapView: <?php echo $show_map ? 'true' : 'false'; ?>
};
</script>
<?php endif; ?>
<?php
get_footer();
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -94,6 +94,7 @@ $featured_commercial = new WP_Query(array(
'show_location_search' => true,
'primary_cta_text' => 'View All Properties',
'primary_cta_url' => home_url('/properties/'),
'primary_cta_icon' => 'map',
'secondary_cta_text' => 'Contact Us',
'secondary_cta_url' => home_url('/contact/'),
'background_image' => $hero_image_split ?: $fallback_image,
@@ -115,6 +116,7 @@ $featured_commercial = new WP_Query(array(
'show_location_search' => true,
'primary_cta_text' => 'View All Properties',
'primary_cta_url' => home_url('/properties/'),
'primary_cta_icon' => 'map',
'secondary_cta_text' => 'Contact Us',
'secondary_cta_url' => home_url('/contact/'),
'background_image' => $hero_image_card ?: $fallback_image,
@@ -197,10 +197,51 @@ function homeproz_ajax_filter_properties() {
<?php
$html = ob_get_clean();
// Build markers data for map view
$markers = array();
$city_coords = array(
'Albert Lea' => array(43.6480, -93.3685),
'Austin' => array(43.6666, -92.9746),
'Glenville' => array(43.5733, -93.2779),
'Emmons' => array(43.5013, -93.4896),
'Clarks Grove' => array(43.7627, -93.3196),
'Alden' => array(43.6719, -93.5768),
'Hartland' => array(43.8030, -93.4846),
'Geneva' => array(43.8255, -93.2682),
'Owatonna' => array(44.0838, -93.2260),
'Faribault' => array(44.2949, -93.2688),
'Rochester' => array(44.0234, -92.4699),
'Mankato' => array(44.1636, -93.9994),
);
foreach ($sorted_properties as $prop) {
$city = get_field('city', $prop->ID);
$price = get_field('property_price', $prop->ID);
$address = get_field('street_address', $prop->ID);
// Get coords for city, default to Albert Lea
$coords = isset($city_coords[$city]) ? $city_coords[$city] : $city_coords['Albert Lea'];
// Add small random offset to prevent overlapping markers (seeded by post ID for consistency)
srand($prop->ID);
$lat = $coords[0] + (rand(-50, 50) / 10000);
$lng = $coords[1] + (rand(-50, 50) / 10000);
$markers[] = array(
'id' => $prop->ID,
'lat' => $lat,
'lng' => $lng,
'title' => $prop->post_title,
'price' => '$' . number_format($price),
'address' => $address . ', ' . $city,
'url' => get_permalink($prop->ID),
);
}
wp_send_json_success(array(
'html' => $html,
'found_posts' => $total,
'max_pages' => $max_pages,
'markers' => $markers,
));
}
add_action('wp_ajax_homeproz_filter_properties', 'homeproz_ajax_filter_properties');
@@ -153,8 +153,6 @@ function homeproz_footer_fallback_menu() {
echo '<li class="menu-item"><a href="' . esc_url(home_url('/properties/')) . '">Properties</a></li>';
echo '<li class="menu-item"><a href="' . esc_url(get_post_type_archive_link('agent')) . '">Agents</a></li>';
echo '<li class="menu-item"><a href="' . esc_url(home_url('/about/')) . '">About</a></li>';
echo '<li class="menu-item"><a href="' . esc_url(home_url('/blog/')) . '">Blog</a></li>';
echo '<li class="menu-item"><a href="' . esc_url(home_url('/contact/')) . '">Contact</a></li>';
echo '</ul>';
}
+121 -77
View File
@@ -40,95 +40,139 @@ get_header();
<!-- Company Story Section -->
<section class="about-story-section">
<div class="container">
<div class="about-story-content">
<?php
while (have_posts()) :
the_post();
the_content();
endwhile;
?>
</div>
</div>
</section>
<!-- Our Values Section -->
<section class="about-values-section">
<div class="container">
<header class="about-values-header">
<h2 class="about-values-title">Our Values</h2>
</header>
<div class="features-grid">
<?php
get_template_part('template-parts/components/feature-block', null, array(
'icon' => 'handshake',
'title' => 'Integrity',
'text' => 'We believe in honest, transparent communication throughout every transaction. Your trust is our priority.',
));
get_template_part('template-parts/components/feature-block', null, array(
'icon' => 'personalized',
'title' => 'Client-Focused',
'text' => 'Your goals are our goals. We listen carefully and work tirelessly to exceed your expectations.',
));
get_template_part('template-parts/components/feature-block', null, array(
'icon' => 'local-expertise',
'title' => 'Community',
'text' => 'We live and work in the communities we serve. Local knowledge means better results for you.',
));
?>
<div class="about-story-layout">
<div class="about-story-image">
<img src="<?php echo esc_url(get_template_directory_uri() . '/assets/images/about-us.webp'); ?>" alt="HomeProz Real Estate Team">
</div>
<div class="about-story-content">
<?php
while (have_posts()) :
the_post();
the_content();
endwhile;
?>
</div>
</div>
</div>
</section>
<!-- Meet the Team Section -->
<section class="about-team-section">
<section class="about-team-section Agents_Archive">
<div class="container">
<header class="about-team-header">
<h2 class="about-team-title">Meet Our Team</h2>
<p class="about-team-subtitle">A dedicated group of real estate professionals committed to your success</p>
</header>
<div class="about-team-grid">
<?php
// Team member data with bios
$team_members = array(
array(
'name' => 'Heidi Johnson',
'title' => 'REALTOR',
'phone' => '507-402-2310',
'email' => 'heidi@homeprozrealestate.com',
'bio' => 'With deep roots in the Albert Lea community, Heidi brings years of local knowledge and dedication to every client relationship.',
),
array(
'name' => 'Kyra Johnson',
'title' => 'REALTOR',
'phone' => '507-516-4870',
'email' => 'kyra@homeprozrealestate.com',
'bio' => 'Kyra combines her passion for real estate with a commitment to exceptional service, guiding buyers and sellers to successful outcomes.',
),
array(
'name' => 'John Hill',
'title' => 'REALTOR',
'phone' => '507-383-1738',
'email' => 'john@homeprozrealestate.com',
'bio' => 'John\'s expertise in the local market and attention to detail ensure a smooth experience whether you\'re buying or selling.',
),
array(
'name' => 'Mindy Moreno',
'title' => 'REALTOR',
'phone' => '507-438-5900',
'email' => 'mindy@homeprozrealestate.com',
'bio' => 'Mindy\'s bilingual skills and warm approach help her connect with a diverse range of clients in our community.',
),
);
<?php
// Query all published agents, then filter disabled and sort by order
$all_agents = get_posts([
'post_type' => 'agent',
'posts_per_page' => -1,
'post_status' => 'publish',
]);
foreach ($team_members as $member) :
get_template_part('template-parts/components/agent-card', null, $member);
endforeach;
?>
</div>
// Filter out disabled agents and add sorting data
$agents_data = [];
foreach ($all_agents as $agent) {
$disabled = get_field('agent_disabled', $agent->ID);
if ($disabled) continue;
$order = get_field('agent_order', $agent->ID);
$agents_data[] = [
'post' => $agent,
'order' => $order ? (int) $order : 10,
];
}
// Sort by order, then by title
usort($agents_data, function($a, $b) {
if ($a['order'] !== $b['order']) {
return $a['order'] - $b['order'];
}
return strcmp($a['post']->post_title, $b['post']->post_title);
});
?>
<?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>
+4 -2
View File
@@ -132,7 +132,9 @@ if ($property_inquiry) {
</div>
<div class="contact-info-content">
<h4 class="contact-info-label">Office Address</h4>
<p class="contact-info-value"><?php echo esc_html($address); ?></p>
<p class="contact-info-value">
<a href="https://www.google.com/maps/search/?api=1&query=<?php echo urlencode($address); ?>" target="_blank" rel="noopener"><?php echo esc_html($address); ?></a>
</p>
</div>
</div>
<?php endif; ?>
@@ -196,7 +198,7 @@ if ($property_inquiry) {
<iframe
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2871.8244599999997!2d-93.36827!3d43.64829!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x87f5024a6c0d9e7d%3A0x0!2s111%20E%20Clark%20St%2C%20Albert%20Lea%2C%20MN%2056007!5e0!3m2!1sen!2sus!4v1234567890"
width="100%"
height="250"
height="375"
style="border:0;"
allowfullscreen=""
loading="lazy"
@@ -49,6 +49,7 @@ $resource_pages = get_pages(array(
<div class="resources-featured-grid">
<!-- Buyer's Guide -->
<div class="resource-featured-card">
<a href="<?php echo esc_url(home_url('/resources/buyers-guide/')); ?>" class="resource-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="resource-featured-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
@@ -59,13 +60,14 @@ $resource_pages = get_pages(array(
<p class="resource-featured-description">
Ready to buy your first home or next property? Our comprehensive guide walks you through every step of the home buying process, from getting pre-approved to closing day.
</p>
<a href="<?php echo esc_url(home_url('/resources/buyers-guide/')); ?>" class="btn btn-primary">
<span class="btn btn-primary">
Read the Buyer's Guide
</a>
</span>
</div>
<!-- Seller's Guide -->
<div class="resource-featured-card">
<a href="<?php echo esc_url(home_url('/resources/sellers-guide/')); ?>" class="resource-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="resource-featured-icon">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
@@ -78,9 +80,9 @@ $resource_pages = get_pages(array(
<p class="resource-featured-description">
Thinking about selling? Learn how to prepare your home for the market, price it right, and navigate the selling process to get the best possible return on your investment.
</p>
<a href="<?php echo esc_url(home_url('/resources/sellers-guide/')); ?>" class="btn btn-primary">
<span class="btn btn-primary">
Read the Seller's Guide
</a>
</span>
</div>
</div>
</div>
@@ -96,6 +98,7 @@ $resource_pages = get_pages(array(
<div class="resources-additional-grid">
<div class="resource-card">
<a href="<?php echo esc_url(home_url('/resources/moving-checklist/')); ?>" class="resource-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="resource-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" 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"/>
@@ -110,6 +113,7 @@ $resource_pages = get_pages(array(
</div>
<div class="resource-card">
<a href="<?php echo esc_url(home_url('/resources/mortgage-calculator/')); ?>" class="resource-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="resource-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
@@ -121,6 +125,7 @@ $resource_pages = get_pages(array(
</div>
<div class="resource-card">
<a href="<?php echo esc_url(home_url('/resources/faq/')); ?>" class="resource-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="resource-card-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
<circle cx="12" cy="12" r="10"/>
+14 -24
View File
@@ -52,18 +52,12 @@ while (have_posts()) :
?>
<main id="primary" class="site-main single-property-main">
<!-- Breadcrumbs -->
<nav class="breadcrumbs" aria-label="Breadcrumb">
<div class="container">
<ol class="breadcrumb-list">
<li><a href="<?php echo esc_url(home_url('/')); ?>">Home</a></li>
<li><a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>">Properties</a></li>
<li aria-current="page"><?php echo esc_html($street_address ?: get_the_title()); ?></li>
</ol>
</div>
</nav>
<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">
@@ -199,23 +193,19 @@ while (have_posts()) :
<!-- 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; ?>
<?php if ($type) : ?>
<span class="property-type"><?php echo esc_html($type); ?></span>
<?php endif; ?>
</div>
<h1 class="property-title"><?php echo esc_html(homeproz_format_price($price)); ?></h1>
<p class="property-address">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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); ?>
</p>
<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>
@@ -224,7 +214,7 @@ while (have_posts()) :
<!-- Property Documents -->
<div class="sidebar-widget property-documents-widget">
<h3 class="widget-title">Property Documents</h3>
<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) :
+6 -2
View File
@@ -34,7 +34,6 @@
@import '../template-parts/components/hero-section-card.scss';
@import '../template-parts/components/cta-section.scss';
@import '../template-parts/components/testimonial.scss';
@import '../template-parts/components/agent-card.scss';
@import '../template-parts/components/feature-block.scss';
@import '../template-parts/components/service-cards.scss';
@@ -61,7 +60,7 @@
--font-body: 'Inter', 'Droid Sans', Arial, sans-serif;
// Spacing
--container-max: 1200px;
--container-max: 1400px;
--container-padding: 1.5rem;
// Transitions (minimal, only functional)
@@ -144,6 +143,7 @@ a {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
font-family: var(--font-body);
font-weight: 600;
@@ -154,6 +154,10 @@ a {
border: none;
cursor: pointer;
svg {
flex-shrink: 0;
}
&-primary {
background-color: var(--color-accent);
color: white;
@@ -121,25 +121,31 @@
align-items: center;
justify-content: center;
gap: 0.375rem;
width: 40px;
height: 40px;
background-color: var(--color-bg-dark);
padding: 0 0.75rem;
background-color: transparent;
border: 2px solid var(--color-accent);
border-radius: 0.25rem;
color: var(--color-text-muted);
color: var(--color-accent);
text-decoration: none;
font-size: 0.8125rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
&:hover {
background-color: var(--color-accent);
color: white;
}
// Icon-only buttons (phone, email)
&:not(.agent-action-profile) {
width: 40px;
padding: 0;
}
&.agent-action-profile {
flex: 1;
width: auto;
font-size: 0.8125rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
}
@@ -31,6 +31,7 @@
// Agent Photo
.agent-photo-wrapper {
flex-shrink: 0;
background-color: #000;
}
.agent-photo {
@@ -1,82 +0,0 @@
<?php
/**
* Agent Card Component
*
* @package HomeProz
*
* Args:
* - name (string): Agent name
* - title (string): Agent title (e.g., "REALTOR")
* - phone (string): Agent phone number
* - email (string): Agent email
* - photo (string): Photo URL or attachment ID
* - bio (string): Short bio text
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Get args with defaults
$name = isset($args['name']) ? $args['name'] : '';
$title = isset($args['title']) ? $args['title'] : 'REALTOR';
$phone = isset($args['phone']) ? $args['phone'] : '';
$email = isset($args['email']) ? $args['email'] : '';
$photo = isset($args['photo']) ? $args['photo'] : '';
$bio = isset($args['bio']) ? $args['bio'] : '';
if (!$name) {
return;
}
// If photo is an ID, get the URL
if (is_numeric($photo)) {
$photo = wp_get_attachment_image_url($photo, 'medium');
}
?>
<div class="agent-card-component card">
<div class="agent-card-photo">
<?php if ($photo) : ?>
<img src="<?php echo esc_url($photo); ?>" alt="<?php echo esc_attr($name); ?>" loading="lazy">
<?php else : ?>
<div class="agent-card-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-body">
<h3 class="agent-card-name"><?php echo esc_html($name); ?></h3>
<p class="agent-card-title"><?php echo esc_html($title); ?></p>
<?php if ($bio) : ?>
<p class="agent-card-bio"><?php echo esc_html($bio); ?></p>
<?php endif; ?>
<div class="agent-card-contact">
<?php if ($phone) : ?>
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $phone)); ?>" class="agent-card-phone">
<svg width="16" height="16" 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($phone); ?>
</a>
<?php endif; ?>
<?php if ($email) : ?>
<a href="mailto:<?php echo esc_attr($email); ?>" class="agent-card-email">
<svg width="16" height="16" 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
</a>
<?php endif; ?>
</div>
</div>
</div>
@@ -1,133 +0,0 @@
/**
* Agent Card Component Styles
*
* @package HomeProz
*/
.agent-card-component {
display: flex;
flex-direction: column;
text-align: center;
padding: 1.5rem;
}
.agent-card-photo {
margin-bottom: 1rem;
img {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
margin: 0 auto;
}
}
.agent-card-photo-placeholder {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: var(--color-bg-dark);
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: var(--color-text-muted);
}
.agent-card-body {
flex: 1;
}
.agent-card-name {
font-family: var(--font-display);
font-size: 1.25rem;
color: var(--color-text);
margin-bottom: 0.25rem;
}
.agent-card-title {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-accent-light);
margin-bottom: 0.75rem;
}
.agent-card-bio {
font-size: 0.875rem;
color: var(--color-text-muted);
line-height: 1.6;
margin-bottom: 1rem;
}
.agent-card-contact {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
}
.agent-card-phone,
.agent-card-email {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--color-text-muted);
&:hover {
color: var(--color-accent-light);
}
svg {
flex-shrink: 0;
}
}
// Team Section (for homepage/about page)
.team-section {
padding: 4rem 0;
background-color: var(--color-bg-card);
@media (max-width: 768px) {
padding: 3rem 0;
}
}
.team-section-header {
text-align: center;
margin-bottom: 3rem;
}
.team-section-title {
font-family: var(--font-display);
font-size: 2.25rem;
color: var(--color-text);
margin-bottom: 0.75rem;
@media (max-width: 768px) {
font-size: 1.875rem;
}
}
.team-section-subtitle {
font-size: 1.125rem;
color: var(--color-text-muted);
max-width: 600px;
margin: 0 auto;
}
.team-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
@@ -31,8 +31,10 @@
background-color: #fff;
color: var(--color-accent);
&:hover {
&:hover,
&:active {
background-color: var(--color-text);
color: #000;
}
}
}
@@ -16,6 +16,7 @@ $title = isset($args['title']) ? $args['title'] : '';
$subtitle = isset($args['subtitle']) ? $args['subtitle'] : '';
$primary_cta_text = isset($args['primary_cta_text']) ? $args['primary_cta_text'] : '';
$primary_cta_url = isset($args['primary_cta_url']) ? $args['primary_cta_url'] : '';
$primary_cta_icon = isset($args['primary_cta_icon']) ? $args['primary_cta_icon'] : '';
$secondary_cta_text = isset($args['secondary_cta_text']) ? $args['secondary_cta_text'] : '';
$secondary_cta_url = isset($args['secondary_cta_url']) ? $args['secondary_cta_url'] : '';
$background_image = isset($args['background_image']) ? $args['background_image'] : '';
@@ -78,6 +79,13 @@ if ($show_location_search) {
<div class="hero-section-actions">
<?php if ($primary_cta_text && $primary_cta_url) : ?>
<a href="<?php echo esc_url($primary_cta_url); ?>" class="btn btn-primary">
<?php if ($primary_cta_icon === 'map') : ?>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
<?php endif; ?>
<?php echo esc_html($primary_cta_text); ?>
</a>
<?php endif; ?>
@@ -70,13 +70,13 @@
left: 32vw;
}
@media (min-width: 1400px) and (max-width: 1649px) {
// 1400-1650px: left edge 150px from left of page
@media (min-width: 1500px) and (max-width: 1649px) {
// 1500-1650px: left edge 150px from left of page
// Left edge = 150px, center = 150px + 260px = 410px
left: 410px;
}
// Below 1400px: stays centered at 50vw (default)
// Below 1500px: stays centered at 50vw (default)
@media (max-width: 768px) {
position: relative;
@@ -111,7 +111,7 @@
line-height: 1.1;
@media (max-width: 1450px) {
font-size: 32px;
font-size: 42px;
}
@media (max-width: 768px) {
@@ -10,6 +10,7 @@
* - subtitle (string): Hero subheadline
* - primary_cta_text (string): Primary button text
* - primary_cta_url (string): Primary button URL
* - primary_cta_icon (string): Primary button icon ('map', 'search', 'arrow', or custom SVG)
* - secondary_cta_text (string): Secondary button text
* - secondary_cta_url (string): Secondary button URL
* - background_image (string): Background image URL
@@ -29,6 +30,7 @@ $title = isset($args['title']) ? $args['title'] : '';
$subtitle = isset($args['subtitle']) ? $args['subtitle'] : '';
$primary_cta_text = isset($args['primary_cta_text']) ? $args['primary_cta_text'] : '';
$primary_cta_url = isset($args['primary_cta_url']) ? $args['primary_cta_url'] : '';
$primary_cta_icon = isset($args['primary_cta_icon']) ? $args['primary_cta_icon'] : '';
$secondary_cta_text = isset($args['secondary_cta_text']) ? $args['secondary_cta_text'] : '';
$secondary_cta_url = isset($args['secondary_cta_url']) ? $args['secondary_cta_url'] : '';
$background_image = isset($args['background_image']) ? $args['background_image'] : '';
@@ -95,6 +97,13 @@ if ($show_location_search) {
<div class="hero-section-actions">
<?php if ($primary_cta_text && $primary_cta_url) : ?>
<a href="<?php echo esc_url($primary_cta_url); ?>" class="btn btn-primary">
<?php if ($primary_cta_icon === 'map') : ?>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
<line x1="8" y1="2" x2="8" y2="18"/>
<line x1="16" y1="6" x2="16" y2="22"/>
</svg>
<?php endif; ?>
<?php echo esc_html($primary_cta_text); ?>
</a>
<?php endif; ?>
@@ -41,13 +41,14 @@ $services = array(
<section class="service-cards-section">
<div class="container">
<header class="service-cards-header">
<h2 class="service-cards-title">How Can We Help?</h2>
<h2 class="service-cards-title">Go With The Proz</h2>
<p class="service-cards-subtitle">Whether you're buying, renting, or selling, we're here to guide you every step of the way.</p>
</header>
<div class="service-cards-grid">
<?php foreach ($services as $service) : ?>
<div class="service-card">
<a href="<?php echo esc_url($service['button_url']); ?>" class="service-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="service-card-icon">
<?php if ($service['icon'] === 'home') : ?>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
@@ -69,12 +70,12 @@ $services = array(
</div>
<h3 class="service-card-title"><?php echo esc_html($service['title']); ?></h3>
<p class="service-card-description"><?php echo esc_html($service['description']); ?></p>
<a href="<?php echo esc_url($service['button_url']); ?>" class="btn btn-outline service-card-btn">
<span class="btn btn-outline service-card-btn">
<?php echo esc_html($service['button_text']); ?>
<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>
</span>
</div>
<?php endforeach; ?>
</div>
@@ -48,19 +48,33 @@
}
.service-card {
position: relative;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 2rem;
text-align: center;
transition: border-color 0.2s ease, transform 0.2s ease;
cursor: pointer;
&:hover {
border-color: var(--color-accent);
transform: translateY(-4px);
.service-card-btn {
background-color: var(--color-accent);
color: var(--color-text);
}
}
}
.service-card-link-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.service-card-icon {
display: flex;
align-items: center;
@@ -19,14 +19,41 @@
}
}
.about-story-content {
max-width: 800px;
margin: 0 auto;
.about-story-layout {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 3rem;
align-items: center;
@media (max-width: 1300px) {
grid-template-columns: 1fr;
gap: 2rem;
}
}
.about-story-image {
img {
width: 100%;
height: auto;
border-radius: 0.5rem;
display: block;
}
@media (max-width: 1300px) {
max-width: 500px;
margin: 0 auto;
}
}
.about-story-content {
h2, h3 {
font-family: var(--font-display);
color: var(--color-text);
margin-top: 2rem;
&:first-child {
margin-top: 0;
}
}
p {
@@ -95,20 +122,6 @@
margin: 0 auto;
}
.about-team-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1.5rem;
@media (max-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: 640px) {
grid-template-columns: 1fr;
}
}
// Broker Section
.about-broker-section {
padding: 3rem 0;
@@ -181,7 +181,8 @@
}
.contact-info-label {
font-size: 0.875rem;
font-family: 'Times New Roman', Times, serif;
font-size: 1rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: 0.25rem;
@@ -25,18 +25,32 @@
}
.resource-featured-card {
position: relative;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 2.5rem;
text-align: center;
transition: border-color 0.2s ease;
cursor: pointer;
&:hover {
border-color: var(--color-accent);
.btn {
background-color: var(--color-accent-hover);
}
}
}
.resource-card-link-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.resource-featured-icon {
display: flex;
align-items: center;
@@ -100,11 +114,12 @@
}
.resource-card {
position: relative;
background-color: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 0.5rem;
padding: 1.5rem;
transition: border-color 0.2s ease;
cursor: pointer;
&:hover {
border-color: var(--color-accent);
@@ -79,6 +79,13 @@ $license_info = 'MN License #40229984';
<div class="footer-column footer-contact">
<h4 class="footer-heading">Contact Us</h4>
<ul class="contact-list">
<li class="contact-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M22 2L11 13"/>
<path d="M22 2L15 22L11 13L2 9L22 2Z"/>
</svg>
<a href="<?php echo esc_url(home_url('/contact/')); ?>">Send Us a Message</a>
</li>
<?php if ($phone) : ?>
<li class="contact-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
@@ -102,7 +109,7 @@ $license_info = 'MN License #40229984';
<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>
<span><?php echo esc_html($address); ?></span>
<a href="https://www.google.com/maps/search/?api=1&query=<?php echo urlencode($address); ?>" target="_blank" rel="noopener"><?php echo esc_html($address); ?></a>
</li>
<?php endif; ?>
</ul>
@@ -122,31 +129,51 @@ $license_info = 'MN License #40229984';
</div>
</div>
<!-- Professional Associations & License Info -->
<div class="footer-credentials">
<div class="footer-logos">
<!-- REALTOR Logo -->
<div class="credential-logo" title="REALTOR">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor" aria-label="REALTOR">
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-size="6" font-weight="bold">R</text>
<circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="1"/>
<!-- Legal & Compliance Section -->
<div class="footer-legal">
<div class="footer-legal-inner">
<!-- Privacy Policy -->
<a href="<?php echo esc_url(home_url('/privacy-policy/')); ?>" class="legal-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
</div>
<!-- Equal Housing Logo -->
<div class="credential-logo" title="Equal Housing Opportunity">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-label="Equal Housing Opportunity">
<span>Privacy Policy</span>
</a>
<!-- Fair Housing -->
<a href="<?php echo esc_url(home_url('/fair-housing-statement/')); ?>" class="legal-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<line x1="8" y1="14" x2="16" y2="14"/>
<line x1="8" y1="17" x2="16" y2="17"/>
</svg>
</div>
</div>
<p class="footer-license"><?php echo esc_html($license_info); ?></p>
</div>
<span>Fair Housing</span>
</a>
<!-- Temporary Alt Page Link -->
<div class="footer-temp-link">
<a href="<?php echo esc_url(home_url('/home-page-alt/')); ?>">HOME PAGE ALT</a>
<!-- MLS Disclaimer -->
<a href="<?php echo esc_url(home_url('/mls-disclaimer/')); ?>" class="legal-item">
<svg width="20" height="20" 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="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
<span>MLS Disclaimer</span>
</a>
<!-- Brokerage Disclosure -->
<a href="<?php echo esc_url(home_url('/brokerage-disclosure/')); ?>" class="legal-item">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
<line x1="12" y1="11" x2="12" y2="17"/>
<line x1="9" y1="14" x2="15" y2="14"/>
</svg>
<span>Brokerage Disclosure</span>
</a>
</div>
<p class="footer-license"><?php echo esc_html($license_info); ?></p>
</div>
<!-- Footer Bottom -->
@@ -155,7 +182,7 @@ $license_info = 'MN License #40229984';
&copy; <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. All rights reserved.
</p>
<p class="footer-credits">
Powered by <a href="https://developer.wordpress.org/" target="_blank" rel="noopener noreferrer">WordPress</a>
Web Design by <a href="https://hanson.xyz" target="_blank" rel="noopener">HansonXyz</a>
</p>
</div>
</div>
@@ -192,33 +192,59 @@
}
}
// Credentials Section
.footer-credentials {
// Legal Section
.footer-legal {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
gap: 1.25rem;
padding-top: 2rem;
margin-top: 2rem;
border-top: 1px solid var(--color-border);
}
.footer-logos {
.footer-legal-inner {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: center;
gap: 1.5rem;
@media (max-width: 640px) {
gap: 1rem;
}
}
.credential-logo {
.legal-item {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background-color: var(--color-bg-dark);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
color: var(--color-text-muted);
opacity: 0.7;
font-size: 0.8125rem;
text-decoration: none;
&:hover {
border-color: var(--color-accent);
color: var(--color-accent-light);
svg {
color: var(--color-accent);
}
}
svg {
width: 40px;
height: 40px;
flex-shrink: 0;
color: var(--color-text-muted);
}
@media (max-width: 640px) {
width: calc(50% - 0.5rem);
justify-content: center;
padding: 0.625rem 0.75rem;
font-size: 0.75rem;
}
}
@@ -38,8 +38,9 @@ if ($state) {
}
?>
<article id="property-<?php echo esc_attr($property_id); ?>" <?php post_class('property-card card'); ?>>
<a href="<?php the_permalink(); ?>" class="property-card-image">
<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>
<div class="property-card-image">
<?php if (has_post_thumbnail()) : ?>
<?php the_post_thumbnail('property-card', array('loading' => 'lazy')); ?>
<?php else : ?>
@@ -56,7 +57,7 @@ if ($state) {
<?php echo esc_html($status); ?>
</span>
<?php endif; ?>
</a>
</div>
<div class="property-card-content">
<div class="property-card-price">
@@ -64,9 +65,7 @@ if ($state) {
</div>
<h3 class="property-card-title">
<a href="<?php the_permalink(); ?>">
<?php echo esc_html($full_address ?: get_the_title()); ?>
</a>
<?php echo esc_html($full_address ?: get_the_title()); ?>
</h3>
<?php if ($bedrooms || $bathrooms || $square_feet) : ?>
@@ -107,11 +106,11 @@ if ($state) {
</p>
<?php endif; ?>
<a href="<?php the_permalink(); ?>" class="property-card-link">
<span class="property-card-link">
View Details
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</a>
</span>
</div>
</article>
@@ -12,6 +12,26 @@
border: 1px solid var(--color-accent);
border-radius: 0.5rem;
overflow: hidden;
position: relative;
cursor: pointer;
&:hover {
border-color: var(--color-accent-hover);
.property-card-link {
color: var(--color-accent-hover);
}
}
}
// Stretched link overlay - makes entire card clickable
.property-card-link-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
}
.property-card-image {
@@ -64,15 +84,7 @@
font-weight: 500;
line-height: 1.4;
margin-bottom: 0.75rem;
a {
color: var(--color-text-muted);
text-decoration: none;
&:hover {
color: var(--color-accent-light);
}
}
color: var(--color-text-muted);
}
// Property specs (beds, baths, sqft)
@@ -9,6 +9,266 @@
(function($) {
'use strict';
/**
* Property Map Manager
*/
var PropertyMap = {
map: null,
markers: {}, // Object keyed by property ID
markerLayer: null,
selectedPropertyId: null,
hoveredPropertyId: null,
baseZIndex: 400,
/**
* Initialize the map
*/
init: function(initialProperties) {
var $mapContainer = $('#property-map');
if (!$mapContainer.length || typeof L === 'undefined') {
return;
}
// Initialize map centered on Albert Lea area
this.map = L.map('property-map').setView([43.6480, -93.3685], 10);
// Add OpenStreetMap tiles
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map);
// Create a layer group for markers
this.markerLayer = L.layerGroup().addTo(this.map);
// Add initial markers
if (initialProperties && initialProperties.length > 0) {
this.updateMarkers(initialProperties);
}
// Bind card hover events
this.bindCardHoverEvents();
},
/**
* Create marker icon with specified color
*/
createIcon: function(color) {
color = color || 'red';
return L.divIcon({
className: 'property-marker property-marker-' + color,
html: '<div class="marker-pin"></div>',
iconSize: [30, 40],
iconAnchor: [15, 40],
popupAnchor: [0, -40]
});
},
/**
* Update map markers with new property data
*/
updateMarkers: function(properties) {
if (!this.map || !this.markerLayer) {
return;
}
// Clear existing markers and reset state
this.markerLayer.clearLayers();
this.markers = {};
this.selectedPropertyId = null;
this.hoveredPropertyId = null;
// Reset any highlighted cards
$('.property-card').removeClass('property-card-highlighted');
var self = this;
properties.forEach(function(prop, index) {
if (prop.lat && prop.lng) {
var marker = L.marker([prop.lat, prop.lng], {
icon: self.createIcon('red'),
zIndexOffset: self.baseZIndex + index
});
// Store property ID on marker
marker.propertyId = prop.id;
marker.defaultZIndex = self.baseZIndex + index;
// Bind popup
marker.bindPopup(
'<div class="map-popup">' +
'<strong>' + prop.price + '</strong><br>' +
'<span>' + prop.address + '</span><br>' +
'<a href="' + prop.url + '">View Details</a>' +
'</div>'
);
// Handle marker click
marker.on('click', function(e) {
self.onMarkerClick(prop.id);
});
self.markerLayer.addLayer(marker);
self.markers[prop.id] = marker;
}
});
// Fit bounds to show all markers
this.fitBounds(properties);
},
/**
* Handle marker click - scroll to card and highlight
*/
onMarkerClick: function(propertyId) {
var self = this;
// If clicking the same pin, do nothing
if (this.selectedPropertyId === propertyId) {
return;
}
// Reset previous selection
if (this.selectedPropertyId) {
this.setMarkerColor(this.selectedPropertyId, 'red');
this.resetMarkerZIndex(this.selectedPropertyId);
$('#property-' + this.selectedPropertyId).removeClass('property-card-highlighted');
}
// Set new selection
this.selectedPropertyId = propertyId;
this.setMarkerColor(propertyId, 'amber');
this.setMarkerZIndex(propertyId, 10000); // Amber on top
// Find the card
var $card = $('#property-' + propertyId);
if (!$card.length) {
return;
}
// Check if card is in view
var cardTop = $card.offset().top;
var cardBottom = cardTop + $card.outerHeight();
var viewportTop = $(window).scrollTop();
var viewportBottom = viewportTop + $(window).height();
var isInView = cardTop >= viewportTop && cardBottom <= viewportBottom;
if (isInView) {
// Already in view, flash immediately
self.flashCard($card);
} else {
// Scroll then flash
$('html, body').animate({
scrollTop: cardTop - 120
}, 400, function() {
self.flashCard($card);
});
}
},
/**
* Flash card border amber animation
*/
flashCard: function($card) {
var self = this;
$card.removeClass('property-card-highlighted');
// Flash sequence: amber -> red -> amber -> stay amber
setTimeout(function() {
$card.addClass('property-card-highlighted');
setTimeout(function() {
$card.removeClass('property-card-highlighted');
setTimeout(function() {
$card.addClass('property-card-highlighted');
// Leave it highlighted
}, 150);
}, 150);
}, 50);
},
/**
* Set marker color
*/
setMarkerColor: function(propertyId, color) {
var marker = this.markers[propertyId];
if (marker) {
marker.setIcon(this.createIcon(color));
}
},
/**
* Set marker z-index
*/
setMarkerZIndex: function(propertyId, zIndex) {
var marker = this.markers[propertyId];
if (marker) {
marker.setZIndexOffset(zIndex);
}
},
/**
* Reset marker to default z-index
*/
resetMarkerZIndex: function(propertyId) {
var marker = this.markers[propertyId];
if (marker) {
marker.setZIndexOffset(marker.defaultZIndex);
}
},
/**
* Bind card hover events (delegated for AJAX-loaded content)
*/
bindCardHoverEvents: function() {
var self = this;
$(document).on('mouseenter', '.property-card[data-property-id]', function() {
var propertyId = $(this).data('property-id');
// Don't change if this is the selected (amber) card
if (propertyId === self.selectedPropertyId) {
return;
}
self.hoveredPropertyId = propertyId;
self.setMarkerColor(propertyId, 'blue');
self.setMarkerZIndex(propertyId, 9000); // Blue below amber but above red
});
$(document).on('mouseleave', '.property-card[data-property-id]', function() {
var propertyId = $(this).data('property-id');
// Don't change if this is the selected (amber) card
if (propertyId === self.selectedPropertyId) {
return;
}
if (self.hoveredPropertyId === propertyId) {
self.hoveredPropertyId = null;
}
self.setMarkerColor(propertyId, 'red');
self.resetMarkerZIndex(propertyId);
});
},
/**
* Fit map bounds to show all properties
*/
fitBounds: function(properties) {
if (!this.map || !properties || properties.length === 0) {
// Reset to default view if no properties
if (this.map) {
this.map.setView([43.6480, -93.3685], 10);
}
return;
}
var bounds = L.latLngBounds(properties.map(function(p) {
return [p.lat, p.lng];
}));
this.map.fitBounds(bounds, { padding: [50, 50] });
}
};
var PropertyFilters = {
// Elements
$form: null,
@@ -140,6 +400,11 @@
self.$results.html(response.data.html);
self.isFirstLoad = false;
// Update map markers if map is active
if (response.data.markers) {
PropertyMap.updateMarkers(response.data.markers);
}
// Update URL
if (updateHistory) {
self.updateUrl(formData, page);
@@ -231,9 +496,80 @@
}
};
/**
* Responsive View Manager
* Handles switching between map and grid views based on viewport width
*/
var ResponsiveView = {
breakpoint: 1024,
isMapView: true, // Track if user selected map view
isAboveBreakpoint: true,
mapInitialized: false,
init: function() {
var self = this;
// Get initial view state from PHP
if (typeof homeprozMapData !== 'undefined') {
this.isMapView = homeprozMapData.isMapView !== false;
}
// Check initial viewport state
this.isAboveBreakpoint = window.innerWidth >= this.breakpoint;
// Initialize map if above breakpoint, map view selected, and we have data
if (this.isAboveBreakpoint && this.isMapView && typeof homeprozMapData !== 'undefined' && homeprozMapData.properties) {
PropertyMap.init(homeprozMapData.properties);
this.mapInitialized = true;
}
// Handle resize
var resizeTimeout;
$(window).on('resize', function() {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function() {
self.handleResize();
}, 150);
});
},
handleResize: function() {
var wasAbove = this.isAboveBreakpoint;
this.isAboveBreakpoint = window.innerWidth >= this.breakpoint;
var $main = $('.property-archive-main');
// Crossing from below to above breakpoint
if (!wasAbove && this.isAboveBreakpoint) {
// Restore the user's view preference
if (this.isMapView) {
$main.removeClass('is-grid-view').addClass('is-map-view');
// Initialize map if not already done
if (!this.mapInitialized && typeof homeprozMapData !== 'undefined') {
PropertyMap.init(homeprozMapData.properties);
this.mapInitialized = true;
} else if (PropertyMap.map) {
// Invalidate size to fix map rendering after show
setTimeout(function() {
PropertyMap.map.invalidateSize();
}, 100);
}
} else {
$main.removeClass('is-map-view').addClass('is-grid-view');
}
}
// No need to do anything when crossing below - CSS handles hiding map
},
setMapView: function(isMap) {
this.isMapView = isMap;
}
};
// Initialize on document ready
$(function() {
PropertyFilters.init();
ResponsiveView.init();
});
})(jQuery);
@@ -102,15 +102,42 @@
}
}
// Map + List Layout
// Responsive View Containers
// Below 1024px: always show grid, hide map layout and view toggle
// Above 1024px: show based on view selection
.property-map-layout {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
display: none; // Hidden by default (mobile-first)
@media (min-width: 1024px) {
grid-template-columns: 1fr 1fr;
gap: 2rem;
// Show map layout when in map view above breakpoint
.is-map-view & {
display: grid;
grid-template-columns: 38% 1fr;
gap: 2rem;
}
}
}
.grid-view-container {
display: block; // Visible by default (mobile)
// Hide view toggle below 1024px
.view-toggle {
display: none;
@media (min-width: 1024px) {
display: flex;
}
}
@media (min-width: 1024px) {
// Above breakpoint, only show if grid view selected
display: none;
.is-grid-view & {
display: block;
}
}
}
@@ -122,6 +149,11 @@
top: 100px;
height: calc(50vh - 75px);
}
// View toggle inside map container
.view-toggle {
margin-bottom: 1.4rem;
}
}
.property-map {
@@ -133,17 +165,26 @@
border: 1px solid var(--color-border);
@media (min-width: 1024px) {
height: 100%;
height: calc(100% - 44px); // Account for view toggle height + margin
}
}
.property-list-container {
.property-grid {
.properties-meta {
padding-top: 0;
padding-bottom: 0;
}
.properties-grid {
grid-template-columns: 1fr;
@media (min-width: 640px) {
grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 1024px) {
grid-template-columns: repeat(2, 1fr);
}
}
}
@@ -173,6 +214,22 @@
border-radius: 50%;
}
}
// Amber marker (selected)
&.property-marker-amber .marker-pin {
background: #F59E0B;
}
// Blue marker (hovered)
&.property-marker-blue .marker-pin {
background: #3B82F6;
}
}
// Highlighted property card (amber border)
.property-card-highlighted {
border-color: #F59E0B !important;
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.3);
}
// Map Popup
@@ -1,7 +1,7 @@
/**
* Property Gallery JavaScript
*
* Lightbox and thumbnail navigation
* Lightbox, thumbnail navigation, autoplay with fade transitions, and swipe support
*
* @package HomeProz
*/
@@ -14,13 +14,34 @@
$gallery: null,
$lightbox: null,
$mainImage: null,
$mainImageContainer: null,
$thumbnails: null,
$thumbnailsContainer: null,
$thumbnailsViewport: null,
$playbackBtn: null,
$prevBtn: null,
$nextBtn: null,
$lightboxImage: null,
$lightboxImageContainer: null,
$lightboxCounter: null,
// State
images: [],
currentIndex: 0,
isPlaying: true,
isTransitioning: false,
autoplayInterval: null,
autoplayDelay: 5000,
fadeDuration: 1000,
slideDuration: 300,
thumbnailsPerPage: 5,
thumbnailPage: 0,
// Swipe state
swipeStartX: 0,
swipeStartY: 0,
swipeThreshold: 50,
isSwiping: false,
/**
* Initialize
@@ -33,9 +54,16 @@
return;
}
this.$mainImage = this.$gallery.find('.gallery-main-image img');
this.$mainImageContainer = this.$gallery.find('.gallery-main-image');
this.$mainImage = this.$mainImageContainer.find('img');
this.$thumbnailsContainer = this.$gallery.find('.gallery-thumbnails-container');
this.$thumbnailsViewport = this.$gallery.find('.gallery-thumbnails-viewport');
this.$thumbnails = this.$gallery.find('.gallery-thumbnail');
this.$playbackBtn = this.$gallery.find('.gallery-playback-btn');
this.$prevBtn = this.$gallery.find('.gallery-thumbnails-prev');
this.$nextBtn = this.$gallery.find('.gallery-thumbnails-next');
this.$lightboxImage = this.$lightbox.find('.lightbox-image');
this.$lightboxImageContainer = this.$lightbox.find('.lightbox-image-container');
this.$lightboxCounter = this.$lightbox.find('.lightbox-current');
// Load images data
@@ -48,7 +76,28 @@
return;
}
// Calculate thumbnails per page based on viewport
this.calculateThumbnailsPerPage();
this.bindEvents();
this.bindSwipeEvents();
this.updateThumbnailNavigation();
// Start autoplay only if more than 1 image
if (this.images.length > 1) {
this.startAutoplay();
}
},
/**
* Calculate thumbnails per page based on viewport width
*/
calculateThumbnailsPerPage: function() {
if ($(window).width() <= 640) {
this.thumbnailsPerPage = 4;
} else {
this.thumbnailsPerPage = 5;
}
},
/**
@@ -57,14 +106,42 @@
bindEvents: function() {
var self = this;
// Thumbnail clicks
this.$thumbnails.on('click', function() {
// Thumbnail clicks - stop autoplay, instant transition
this.$thumbnails.on('click', function(e) {
e.stopPropagation();
var index = parseInt($(this).data('index'));
self.setMainImage(index);
self.stopAutoplay();
self.setMainImage(index, false); // false = no fade
});
// Open lightbox
this.$gallery.find('[data-lightbox-trigger]').on('click', function() {
// Play/pause button
this.$playbackBtn.on('click', function(e) {
e.stopPropagation();
e.preventDefault();
if (self.isPlaying) {
self.stopAutoplay();
} else {
self.startAutoplay();
}
});
// Thumbnail navigation buttons
this.$prevBtn.on('click', function() {
self.prevThumbnailPage();
});
this.$nextBtn.on('click', function() {
self.nextThumbnailPage();
});
// Open lightbox - stop autoplay
this.$gallery.find('[data-lightbox-trigger]').on('click', function(e) {
// Don't open if we just finished a swipe
if (self.isSwiping) {
self.isSwiping = false;
return;
}
self.stopAutoplay();
self.openLightbox(self.currentIndex);
});
@@ -73,13 +150,13 @@
self.closeLightbox();
});
// Navigation
// Lightbox navigation
this.$lightbox.find('.lightbox-prev').on('click', function() {
self.prevImage();
self.slideLightboxImage('prev');
});
this.$lightbox.find('.lightbox-next').on('click', function() {
self.nextImage();
self.slideLightboxImage('next');
});
// Keyboard navigation
@@ -93,33 +170,376 @@
self.closeLightbox();
break;
case 'ArrowLeft':
self.prevImage();
self.slideLightboxImage('prev');
break;
case 'ArrowRight':
self.nextImage();
self.slideLightboxImage('next');
break;
}
});
// Recalculate on resize
$(window).on('resize', function() {
self.calculateThumbnailsPerPage();
self.updateThumbnailNavigation();
});
},
/**
* Set main gallery image
* Bind swipe events for touch devices
*/
setMainImage: function(index) {
bindSwipeEvents: function() {
var self = this;
// Main gallery swipe
this.$mainImageContainer[0].addEventListener('touchstart', function(e) {
self.handleSwipeStart(e);
}, { passive: true });
this.$mainImageContainer[0].addEventListener('touchend', function(e) {
self.handleMainGallerySwipeEnd(e);
}, { passive: true });
// Lightbox swipe
this.$lightboxImageContainer[0].addEventListener('touchstart', function(e) {
self.handleSwipeStart(e);
}, { passive: true });
this.$lightboxImageContainer[0].addEventListener('touchend', function(e) {
self.handleLightboxSwipeEnd(e);
}, { passive: true });
},
/**
* Handle swipe start
*/
handleSwipeStart: function(e) {
if (e.touches.length === 1) {
this.swipeStartX = e.touches[0].clientX;
this.swipeStartY = e.touches[0].clientY;
}
},
/**
* Handle main gallery swipe end
*/
handleMainGallerySwipeEnd: function(e) {
if (e.changedTouches.length !== 1) return;
var deltaX = e.changedTouches[0].clientX - this.swipeStartX;
var deltaY = e.changedTouches[0].clientY - this.swipeStartY;
// Only handle horizontal swipes (not vertical scrolling)
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > this.swipeThreshold) {
this.isSwiping = true;
this.stopAutoplay();
if (deltaX > 0) {
// Swipe right - previous image
this.slideMainImage('prev');
} else {
// Swipe left - next image
this.slideMainImage('next');
}
}
},
/**
* Handle lightbox swipe end
*/
handleLightboxSwipeEnd: function(e) {
if (e.changedTouches.length !== 1) return;
var deltaX = e.changedTouches[0].clientX - this.swipeStartX;
var deltaY = e.changedTouches[0].clientY - this.swipeStartY;
// Only handle horizontal swipes
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > this.swipeThreshold) {
if (deltaX > 0) {
// Swipe right - previous image
this.slideLightboxImage('prev');
} else {
// Swipe left - next image
this.slideLightboxImage('next');
}
}
},
/**
* Start autoplay
*/
startAutoplay: function() {
var self = this;
if (this.images.length <= 1) {
return;
}
this.isPlaying = true;
this.$playbackBtn.addClass('is-playing');
this.$playbackBtn.attr('aria-label', 'Pause slideshow');
this.autoplayInterval = setInterval(function() {
self.advanceImage();
}, this.autoplayDelay);
},
/**
* Stop autoplay
*/
stopAutoplay: function() {
this.isPlaying = false;
this.$playbackBtn.removeClass('is-playing');
this.$playbackBtn.attr('aria-label', 'Play slideshow');
if (this.autoplayInterval) {
clearInterval(this.autoplayInterval);
this.autoplayInterval = null;
}
},
/**
* Advance to next image (for autoplay) - with fade
*/
advanceImage: function() {
if (this.isTransitioning) {
return;
}
var newIndex = this.currentIndex + 1;
if (newIndex >= this.images.length) {
newIndex = 0;
}
this.setMainImage(newIndex, true); // true = use fade
},
/**
* Slide main image with slide transition
* @param {string} direction - 'prev' or 'next'
*/
slideMainImage: function(direction) {
var self = this;
if (this.isTransitioning || this.images.length <= 1) {
return;
}
var newIndex;
if (direction === 'prev') {
newIndex = this.currentIndex - 1;
if (newIndex < 0) newIndex = this.images.length - 1;
} else {
newIndex = this.currentIndex + 1;
if (newIndex >= this.images.length) newIndex = 0;
}
this.isTransitioning = true;
var image = this.images[newIndex];
var slideFrom = direction === 'next' ? '100%' : '-100%';
var slideTo = direction === 'next' ? '-100%' : '100%';
// Create new image for slide in
var $newImage = $('<img class="gallery-slide-image" />');
$newImage.attr('src', image.url);
$newImage.attr('alt', image.alt || 'Property photo');
$newImage.css({
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'object-fit': 'cover',
'transform': 'translateX(' + slideFrom + ')',
'z-index': 2,
'border-radius': '0.5rem'
});
// Ensure container has relative positioning and overflow hidden
this.$mainImageContainer.css({
'position': 'relative',
'overflow': 'hidden'
});
// Append new image
this.$mainImageContainer.append($newImage);
// Animate current image out and new image in
this.$mainImage.css({
'transition': 'transform ' + this.slideDuration + 'ms ease-out'
});
$newImage.css({
'transition': 'transform ' + this.slideDuration + 'ms ease-out'
});
// Trigger reflow
$newImage[0].offsetHeight;
// Animate
this.$mainImage.css('transform', 'translateX(' + slideTo + ')');
$newImage.css('transform', 'translateX(0)');
// After transition
setTimeout(function() {
self.$mainImage.attr('src', image.url);
self.$mainImage.attr('alt', image.alt || 'Property photo');
self.$mainImage.css({
'transition': '',
'transform': ''
});
$newImage.remove();
self.isTransitioning = false;
}, this.slideDuration);
this.currentIndex = newIndex;
// Update active thumbnail
this.$thumbnails.removeClass('is-active');
this.$thumbnails.filter('[data-index="' + newIndex + '"]').addClass('is-active');
// Ensure the current thumbnail is visible
this.scrollToThumbnail(newIndex);
},
/**
* Set main gallery image
* @param {number} index - Image index
* @param {boolean} useFade - Whether to use fade transition
*/
setMainImage: function(index, useFade) {
var self = this;
if (index < 0 || index >= this.images.length) {
return;
}
this.currentIndex = index;
if (this.isTransitioning) {
return;
}
// Update main image
var image = this.images[index];
this.$mainImage.attr('src', image.url);
this.$mainImage.attr('alt', image.alt || 'Property photo');
if (useFade) {
// Fade transition for autoplay
this.isTransitioning = true;
// Create overlay image for crossfade
var $overlay = $('<img class="gallery-fade-overlay" />');
$overlay.attr('src', image.url);
$overlay.attr('alt', image.alt || 'Property photo');
$overlay.css({
'position': 'absolute',
'top': 0,
'left': 0,
'width': '100%',
'height': '100%',
'object-fit': 'cover',
'opacity': 0,
'transform': 'scale(1.02)',
'transition': 'opacity ' + this.fadeDuration + 'ms ease-in-out, transform ' + this.fadeDuration + 'ms ease-in-out',
'z-index': 2,
'border-radius': '0.5rem'
});
// Ensure container has relative positioning
this.$mainImageContainer.css('position', 'relative');
// Append overlay
this.$mainImageContainer.append($overlay);
// Trigger reflow then animate
$overlay[0].offsetHeight; // Force reflow
$overlay.css({
'opacity': 1,
'transform': 'scale(1)'
});
// After transition, update main image and remove overlay
setTimeout(function() {
self.$mainImage.attr('src', image.url);
self.$mainImage.attr('alt', image.alt || 'Property photo');
$overlay.remove();
self.isTransitioning = false;
}, this.fadeDuration);
} else {
// Instant transition for manual clicks
this.$mainImage.attr('src', image.url);
this.$mainImage.attr('alt', image.alt || 'Property photo');
}
this.currentIndex = index;
// Update active thumbnail
this.$thumbnails.removeClass('is-active');
this.$thumbnails.filter('[data-index="' + index + '"]').addClass('is-active');
// Ensure the current thumbnail is visible
this.scrollToThumbnail(index);
},
/**
* Scroll to make thumbnail visible
*/
scrollToThumbnail: function(index) {
var pageForIndex = Math.floor(index / this.thumbnailsPerPage);
if (pageForIndex !== this.thumbnailPage) {
this.thumbnailPage = pageForIndex;
this.scrollThumbnails();
}
},
/**
* Scroll thumbnails to current page
*/
scrollThumbnails: function() {
var $thumbnailsInner = this.$gallery.find('.gallery-thumbnails');
var thumbnailWidth = this.$thumbnails.first().outerWidth(true);
var scrollAmount = this.thumbnailPage * this.thumbnailsPerPage * thumbnailWidth;
$thumbnailsInner.css('transform', 'translateX(-' + scrollAmount + 'px)');
this.updateThumbnailNavigation();
},
/**
* Update thumbnail navigation button states
*/
updateThumbnailNavigation: function() {
var totalPages = Math.ceil(this.images.length / this.thumbnailsPerPage);
this.$prevBtn.prop('disabled', this.thumbnailPage === 0);
this.$nextBtn.prop('disabled', this.thumbnailPage >= totalPages - 1);
// Hide nav buttons if all thumbnails fit
if (totalPages <= 1) {
this.$prevBtn.hide();
this.$nextBtn.hide();
} else {
this.$prevBtn.show();
this.$nextBtn.show();
}
},
/**
* Previous thumbnail page
*/
prevThumbnailPage: function() {
if (this.thumbnailPage > 0) {
this.thumbnailPage--;
this.scrollThumbnails();
}
},
/**
* Next thumbnail page
*/
nextThumbnailPage: function() {
var totalPages = Math.ceil(this.images.length / this.thumbnailsPerPage);
if (this.thumbnailPage < totalPages - 1) {
this.thumbnailPage++;
this.scrollThumbnails();
}
},
/**
@@ -142,31 +562,103 @@
},
/**
* Previous image
* Slide lightbox image with animation
* @param {string} direction - 'prev' or 'next'
*/
slideLightboxImage: function(direction) {
var self = this;
if (this.isTransitioning || this.images.length <= 1) {
return;
}
var newIndex;
if (direction === 'prev') {
newIndex = this.currentIndex - 1;
if (newIndex < 0) newIndex = this.images.length - 1;
} else {
newIndex = this.currentIndex + 1;
if (newIndex >= this.images.length) newIndex = 0;
}
this.isTransitioning = true;
var image = this.images[newIndex];
var slideFrom = direction === 'next' ? '100%' : '-100%';
var slideTo = direction === 'next' ? '-100%' : '100%';
// Create new image for slide in
var $newImage = $('<img class="lightbox-slide-image" />');
$newImage.attr('src', image.url);
$newImage.attr('alt', image.alt || 'Property photo');
$newImage.css({
'position': 'absolute',
'max-width': '100%',
'max-height': 'calc(100vh - 8rem)',
'object-fit': 'contain',
'transform': 'translateX(' + slideFrom + ')',
'left': '50%',
'top': '50%',
'margin-left': '-45vw',
'margin-top': 'calc(-50vh + 4rem)'
});
// Ensure container has relative positioning
this.$lightboxImageContainer.css({
'position': 'relative',
'overflow': 'hidden'
});
// Append new image
this.$lightboxImageContainer.append($newImage);
// Animate current image out and new image in
this.$lightboxImage.css({
'transition': 'transform ' + this.slideDuration + 'ms ease-out'
});
$newImage.css({
'transition': 'transform ' + this.slideDuration + 'ms ease-out'
});
// Trigger reflow
$newImage[0].offsetHeight;
// Animate
this.$lightboxImage.css('transform', 'translateX(' + slideTo + ')');
$newImage.css('transform', 'translateX(0)');
// After transition
setTimeout(function() {
self.$lightboxImage.attr('src', image.url);
self.$lightboxImage.attr('alt', image.alt || 'Property photo');
self.$lightboxImage.css({
'transition': '',
'transform': ''
});
$newImage.remove();
self.isTransitioning = false;
self.$lightboxCounter.text(newIndex + 1);
}, this.slideDuration);
this.currentIndex = newIndex;
},
/**
* Previous image (legacy - now uses slide)
*/
prevImage: function() {
var newIndex = this.currentIndex - 1;
if (newIndex < 0) {
newIndex = this.images.length - 1;
}
this.currentIndex = newIndex;
this.updateLightboxImage();
this.slideLightboxImage('prev');
},
/**
* Next image
* Next image (legacy - now uses slide)
*/
nextImage: function() {
var newIndex = this.currentIndex + 1;
if (newIndex >= this.images.length) {
newIndex = 0;
}
this.currentIndex = newIndex;
this.updateLightboxImage();
this.slideLightboxImage('next');
},
/**
* Update lightbox image
* Update lightbox image (instant, no animation)
*/
updateLightboxImage: function() {
var image = this.images[this.currentIndex];
@@ -53,6 +53,16 @@ $image_count = count($images);
<button class="gallery-main-image" type="button" data-lightbox-trigger aria-label="Open gallery">
<img src="<?php echo esc_url($images[0]['url']); ?>" alt="<?php echo esc_attr($images[0]['alt'] ?: 'Property photo'); ?>" />
<?php if ($image_count > 1) : ?>
<!-- Play/Pause Button -->
<button class="gallery-playback-btn is-playing" type="button" aria-label="Pause slideshow">
<svg class="icon-pause" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="6" y="4" width="4" height="16"/>
<rect x="14" y="4" width="4" height="16"/>
</svg>
<svg class="icon-play" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<polygon points="5,3 19,12 5,21"/>
</svg>
</button>
<span class="gallery-count">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
@@ -67,20 +77,31 @@ $image_count = count($images);
<?php if ($image_count > 1) : ?>
<!-- Thumbnails -->
<div class="gallery-thumbnails">
<?php foreach (array_slice($images, 0, 5) as $index => $image) : ?>
<button
class="gallery-thumbnail <?php echo $index === 0 ? 'is-active' : ''; ?>"
type="button"
data-index="<?php echo esc_attr($index); ?>"
aria-label="View image <?php echo esc_attr($index + 1); ?>"
>
<img src="<?php echo esc_url(wp_get_attachment_image_url($image['id'], 'thumbnail')); ?>" alt="" loading="lazy" />
<?php if ($index === 4 && $image_count > 5) : ?>
<span class="thumbnail-more">+<?php echo esc_html($image_count - 5); ?></span>
<?php endif; ?>
</button>
<?php endforeach; ?>
<div class="gallery-thumbnails-container">
<button class="gallery-thumbnails-nav gallery-thumbnails-prev" type="button" aria-label="Previous thumbnails" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"/>
</svg>
</button>
<div class="gallery-thumbnails-viewport">
<div class="gallery-thumbnails">
<?php foreach ($images as $index => $image) : ?>
<button
class="gallery-thumbnail <?php echo $index === 0 ? 'is-active' : ''; ?>"
type="button"
data-index="<?php echo esc_attr($index); ?>"
aria-label="View image <?php echo esc_attr($index + 1); ?>"
>
<img src="<?php echo esc_url(wp_get_attachment_image_url($image['id'], 'thumbnail')); ?>" alt="" loading="lazy" />
</button>
<?php endforeach; ?>
</div>
</div>
<button class="gallery-thumbnails-nav gallery-thumbnails-next" type="button" aria-label="Next thumbnails">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"/>
</svg>
</button>
</div>
<?php endif; ?>
@@ -48,19 +48,68 @@
font-weight: 500;
}
// Thumbnails
.gallery-thumbnails {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0.5rem;
// Play/Pause Button
.gallery-playback-btn {
position: absolute;
bottom: 1rem;
left: 1rem;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 0;
background-color: rgba(0, 0, 0, 0.75);
border: none;
border-radius: 0.25rem;
color: white;
cursor: pointer;
z-index: 2;
@media (max-width: 640px) {
grid-template-columns: repeat(4, 1fr);
.icon-play {
display: none;
}
.icon-pause {
display: block;
}
&:not(.is-playing) {
.icon-play {
display: block;
}
.icon-pause {
display: none;
}
}
&:hover {
background-color: rgba(0, 0, 0, 0.9);
}
}
// Thumbnails Container
.gallery-thumbnails-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.gallery-thumbnails-viewport {
flex: 1;
overflow: hidden;
}
.gallery-thumbnails {
display: flex;
gap: 0.5rem;
}
.gallery-thumbnail {
position: relative;
flex-shrink: 0;
width: calc((100% - 2rem) / 5);
padding: 0;
border: 2px solid transparent;
background: none;
@@ -68,6 +117,10 @@
border-radius: 0.25rem;
overflow: hidden;
@media (max-width: 640px) {
width: calc((100% - 1.5rem) / 4);
}
&.is-active {
border-color: var(--color-accent);
}
@@ -80,16 +133,31 @@
}
}
.thumbnail-more {
position: absolute;
inset: 0;
// Thumbnail Navigation Arrows
.gallery-thumbnails-nav {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.7);
color: white;
font-size: 1rem;
font-weight: 600;
width: 32px;
height: 32px;
padding: 0;
background-color: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 0.25rem;
color: var(--color-text);
cursor: pointer;
&:hover:not(:disabled) {
background-color: var(--color-accent);
border-color: var(--color-accent);
color: white;
}
&:disabled {
opacity: 0.3;
cursor: not-allowed;
}
}
// Gallery Placeholder
@@ -51,11 +51,26 @@
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;
padding-top: 2rem;
@media (min-width: 1024px) {
grid-template-columns: 1fr 350px;
@@ -213,8 +228,9 @@
}
.widget-title {
font-size: 1rem;
font-weight: 600;
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);
@@ -230,33 +246,12 @@
margin-bottom: 0.75rem;
}
.property-type {
font-size: 0.75rem;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.property-title {
font-family: var(--font-display);
font-size: 1.75rem;
margin-bottom: 0.75rem;
line-height: 1.2;
}
.property-address {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.9375rem;
color: var(--color-text-muted);
color: var(--color-text);
margin-bottom: 0.5rem;
line-height: 1.4;
svg {
flex-shrink: 0;
margin-top: 0.125rem;
color: var(--color-accent);
}
line-height: 1.2;
}
.property-mls {
@@ -266,6 +261,27 @@
}
}
// 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 {
@@ -364,6 +380,8 @@
.agent-avatar {
flex-shrink: 0;
background-color: #000;
border-radius: 50%;
img {
width: 64px;
@@ -379,7 +397,7 @@
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-bg-dark);
background-color: #000;
border-radius: 50%;
color: var(--color-text-muted);
}