wip
@@ -13,21 +13,29 @@ if (!defined('ABSPATH')) {
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Get 404 page content from Theme Options
|
||||
$error_title = homeproz_get_acf_option('404_title', '404');
|
||||
$error_subtitle = homeproz_get_acf_option('404_subtitle', 'Page Not Found');
|
||||
$error_message = homeproz_get_acf_option('404_message', "The page you're looking for doesn't exist or has been moved.");
|
||||
$primary_button = homeproz_get_acf_option('404_primary_button', 'Go Home');
|
||||
$secondary_button = homeproz_get_acf_option('404_secondary_button', 'View Properties');
|
||||
?>
|
||||
<main id="primary" class="site-main">
|
||||
<div class="container">
|
||||
<section class="error-404">
|
||||
<header class="error-header">
|
||||
<h1 class="error-title">404</h1>
|
||||
<p class="error-subtitle">Page Not Found</p>
|
||||
<h1 class="error-title"><?php echo esc_html($error_title); ?></h1>
|
||||
<p class="error-subtitle"><?php echo esc_html($error_subtitle); ?></p>
|
||||
</header>
|
||||
|
||||
<div class="error-content">
|
||||
<p>The page you're looking for doesn't exist or has been moved.</p>
|
||||
<p><?php echo esc_html($error_message); ?></p>
|
||||
<p>Let's get you back on track:</p>
|
||||
|
||||
<div class="error-actions">
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn btn-primary">Go Home</a>
|
||||
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-secondary">View Properties</a>
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn btn-primary"><?php echo esc_html($primary_button); ?></a>
|
||||
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-secondary"><?php echo esc_html($secondary_button); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# HomeProz Theme - Hardcoded Content Audit
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This audit identifies all hardcoded content in the HomeProz WordPress theme and tracks what has been made editable. **Phase 1 and 2 are complete**, with Theme Options expansion and Homepage Content now fully editable via ACF.
|
||||
|
||||
---
|
||||
|
||||
## COMPLETED - Now Editable
|
||||
|
||||
### Theme Options (via ACF Options Page)
|
||||
|
||||
**Contact Information Tab:**
|
||||
- Phone number (`theme_phone`)
|
||||
- Email address (`theme_email`)
|
||||
- Office address (`theme_address`)
|
||||
|
||||
**Social Media Tab:**
|
||||
- Facebook URL (`theme_facebook`)
|
||||
- TikTok URL (`theme_tiktok`)
|
||||
- Instagram URL (`theme_instagram`)
|
||||
- YouTube URL (`theme_youtube`)
|
||||
|
||||
**Footer & Branding Tab (NEW):**
|
||||
- Footer tagline (`theme_footer_tagline`)
|
||||
- Broker information (`theme_broker_info`)
|
||||
- Organization description (`theme_org_description`)
|
||||
|
||||
**Office Hours Tab (NEW):**
|
||||
- Office hours repeater (`theme_office_hours`) - Day/Time pairs
|
||||
|
||||
**Location Tab (NEW):**
|
||||
- Latitude (`theme_latitude`)
|
||||
- Longitude (`theme_longitude`)
|
||||
- Google Maps embed code (`theme_map_embed`)
|
||||
|
||||
### Homepage Content (ACF Page Fields - NEW)
|
||||
|
||||
**Hero Section Tab:**
|
||||
- Hero title (`home_hero_title`)
|
||||
- Hero subtitle (`home_hero_subtitle`)
|
||||
- Primary CTA text (`home_hero_primary_cta_text`)
|
||||
- Primary CTA URL (`home_hero_primary_cta_url`)
|
||||
- Secondary CTA text (`home_hero_secondary_cta_text`)
|
||||
- Secondary CTA URL (`home_hero_secondary_cta_url`)
|
||||
- Hero background image (`home_hero_background`)
|
||||
|
||||
**Service Cards Tab:**
|
||||
- Section title (`home_services_title`)
|
||||
- Section subtitle (`home_services_subtitle`)
|
||||
- Service cards repeater (`home_service_cards`) - Icon, Title, Description, Button Text, Button URL
|
||||
|
||||
**Featured Sections Tab:**
|
||||
- Featured Homes title (`home_featured_homes_title`)
|
||||
- Featured Homes subtitle (`home_featured_homes_subtitle`)
|
||||
- Commercial section title (`home_commercial_title`)
|
||||
- Commercial section subtitle (`home_commercial_subtitle`)
|
||||
|
||||
**CTA Section Tab:**
|
||||
- CTA title (`home_cta_title`)
|
||||
- CTA text (`home_cta_text`)
|
||||
- CTA button text (`home_cta_button_text`)
|
||||
- CTA button URL (`home_cta_button_url`)
|
||||
|
||||
**Property Types Tab:**
|
||||
- Section title (`home_property_types_title`)
|
||||
- Section subtitle (`home_property_types_subtitle`)
|
||||
|
||||
### Page Hero Fields (all pages except homepage)
|
||||
- `hero_title` - Override page title
|
||||
- `hero_subtitle` - Short description
|
||||
- `hero_background` - Background image
|
||||
|
||||
### Property CPT (comprehensive)
|
||||
- All pricing, address, details, features, gallery, documents, agent fields
|
||||
|
||||
### Agent CPT (comprehensive)
|
||||
- All contact, bio, gallery, social, settings fields
|
||||
|
||||
---
|
||||
|
||||
## Data Consistency Fixed
|
||||
|
||||
The schema markup (`schema-markup.php`) now pulls from Theme Options, eliminating these previous inconsistencies:
|
||||
|
||||
| Data Point | Previous Issue | Now |
|
||||
|------------|----------------|-----|
|
||||
| **Phone** | Different in schema vs theme options | Uses `theme_phone` |
|
||||
| **Email** | Different in schema vs theme options | Uses `theme_email` |
|
||||
| **Address** | Different in schema vs theme options | Uses `theme_address` |
|
||||
| **Facebook** | Different in schema vs theme options | Uses `theme_facebook` |
|
||||
| **Coordinates** | Hardcoded in schema | Uses `theme_latitude`/`theme_longitude` |
|
||||
| **Description** | Hardcoded in schema | Uses `theme_org_description` |
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `/inc/acf-fields.php` - Added new field groups:
|
||||
- Theme Options: Footer & Branding, Office Hours, Location tabs
|
||||
- Homepage Content field group with all homepage sections
|
||||
|
||||
2. `/inc/acf-fields.php` - Updated helper functions:
|
||||
- `homeproz_get_acf_option()` - Extended with new fields
|
||||
- `homeproz_get_office_hours()` - New function for office hours
|
||||
|
||||
3. `/template-parts/footer/site-footer.php` - Now uses:
|
||||
- `homeproz_get_acf_option('footer_tagline')`
|
||||
- `homeproz_get_acf_option('broker_info')`
|
||||
- `homeproz_get_office_hours()`
|
||||
|
||||
4. `/front-page.php` - Now uses ACF fields for:
|
||||
- Hero section (title, subtitle, CTAs)
|
||||
- Featured sections (titles, subtitles)
|
||||
- CTA section (title, text, button)
|
||||
|
||||
5. `/template-parts/components/service-cards.php` - Now uses:
|
||||
- ACF section title/subtitle
|
||||
- ACF repeater for service cards (with hardcoded fallback)
|
||||
|
||||
6. `/template-parts/components/property-type-boxes.php` - Now uses:
|
||||
- ACF section title/subtitle
|
||||
|
||||
7. `/inc/schema-markup.php` - Now pulls all data from Theme Options:
|
||||
- Contact info (phone, email, address)
|
||||
- Location (latitude, longitude)
|
||||
- Social links (Facebook, TikTok)
|
||||
- Organization description
|
||||
|
||||
8. `/inc/acf-migration.php` - New migration script:
|
||||
- Populates ACF fields with existing hardcoded values
|
||||
- Run via: `wp eval-file wp-content/themes/homeproz/inc/acf-migration.php`
|
||||
|
||||
---
|
||||
|
||||
## REMAINING - Future Work (Phase 3)
|
||||
|
||||
These items are still hardcoded but lower priority. They can be made editable in a future update:
|
||||
|
||||
### Marketing Pages
|
||||
|
||||
#### About Page (page-about.php)
|
||||
- Team section subtitle
|
||||
- Broker disclosure section content
|
||||
|
||||
#### Contact Page (page-contact.php)
|
||||
- Contact form configuration
|
||||
- Map is now configurable via `theme_map_embed` in Theme Options
|
||||
|
||||
#### Join Page (page-join.php)
|
||||
- Benefits cards content (4 cards with title, description, icon)
|
||||
- Team preview section titles
|
||||
|
||||
#### Resources Page (page-resources.php)
|
||||
- Buyer's Guide card content
|
||||
- Seller's Guide card content
|
||||
- Additional resources section
|
||||
|
||||
#### Mortgage Calculator Page (page-mortgage-calculator.php)
|
||||
- Calculator default values
|
||||
- Guide section content
|
||||
- Notes section content
|
||||
- Quick tips content
|
||||
- Help widget content
|
||||
|
||||
#### Communities Page (page-communities.php)
|
||||
- About section content
|
||||
|
||||
### Block Patterns
|
||||
|
||||
| Item | Location | Status |
|
||||
|------|----------|--------|
|
||||
| Contact info block pattern | patterns/contact-info.php | Could use Theme Options |
|
||||
| Feature grid pattern | patterns/feature-grid.php | Low priority |
|
||||
|
||||
### Other Low Priority Items
|
||||
|
||||
| Item | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Schema service radius | schema-markup.php | Hardcoded 50km |
|
||||
| Design credit | site-footer.php | Keep hardcoded |
|
||||
|
||||
---
|
||||
|
||||
## How to Edit Content
|
||||
|
||||
### Theme Options (Site-Wide Settings)
|
||||
1. Go to WordPress Admin > Theme Options
|
||||
2. Navigate to the appropriate tab:
|
||||
- **Contact Information** - Phone, email, address
|
||||
- **Social Media** - Facebook, TikTok, Instagram, YouTube URLs
|
||||
- **Footer & Branding** - Tagline, broker info, organization description
|
||||
- **Office Hours** - Add/edit day/time pairs
|
||||
- **Location** - Latitude, longitude, Google Maps embed
|
||||
|
||||
### Homepage Content
|
||||
1. Go to WordPress Admin > Pages > Home (front page)
|
||||
2. Scroll to "Homepage Content" section below editor
|
||||
3. Navigate tabs:
|
||||
- **Hero Section** - Main hero title, subtitle, button text/URLs
|
||||
- **Service Cards** - Edit the 3 service cards or add more
|
||||
- **Featured Sections** - Section headings for Featured Homes and Commercial
|
||||
- **CTA Section** - Bottom call-to-action content
|
||||
- **Property Types** - Section heading for property type boxes
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Migration script auto-populated all new fields with existing hardcoded values
|
||||
- All fields have fallback defaults if ACF values are empty
|
||||
- No visual changes to the site unless content is edited
|
||||
- Run migration again safely - it skips fields that already have values
|
||||
@@ -25,6 +25,12 @@ get_header();
|
||||
// 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';
|
||||
|
||||
// Near me mode - show location prompt before loading map
|
||||
$near_me_mode = isset($_GET['near_me']) && $_GET['near_me'] === '1';
|
||||
if ($near_me_mode) {
|
||||
$view_class .= ' is-near-me-mode';
|
||||
}
|
||||
?>
|
||||
|
||||
<main id="primary" class="site-main property-archive-main <?php echo esc_attr($view_class); ?>">
|
||||
@@ -47,38 +53,66 @@ $view_class = $show_map ? 'is-map-view' : 'is-grid-view';
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<?php if ($near_me_mode) : ?>
|
||||
<!-- Near Me Location Prompt -->
|
||||
<div id="near-me-overlay" class="near-me-overlay">
|
||||
<div class="near-me-prompt">
|
||||
<div class="near-me-icon">
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="near-me-title" id="near-me-title">Finding your location...</h2>
|
||||
<p class="near-me-message" id="near-me-message">Click "Allow" in your browser when prompted to share your location.</p>
|
||||
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-primary near-me-fallback">View All Properties</a>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="container">
|
||||
<!-- Filters -->
|
||||
<?php get_template_part('template-parts/property/property-filters'); ?>
|
||||
|
||||
<!-- 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 class="property-sidebar">
|
||||
<div class="property-sidebar-content">
|
||||
<!-- View Toggle -->
|
||||
<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>
|
||||
<a href="<?php echo esc_url(home_url('/properties/?near_me=1')); ?>" class="view-toggle-btn view-toggle-nearme">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
<span>Near Me</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="property-map" class="property-map">
|
||||
<!-- Leaflet map will be initialized here -->
|
||||
</div>
|
||||
<!-- Sticky Filters (below map, visible when main filters scroll out of view) -->
|
||||
<?php get_template_part('template-parts/property/property-filters-sticky'); ?>
|
||||
</div>
|
||||
<div id="property-map" class="property-map">
|
||||
<!-- Leaflet map will be initialized here -->
|
||||
</div>
|
||||
<!-- Sticky Filters (below map, visible when main filters scroll out of view) -->
|
||||
<?php get_template_part('template-parts/property/property-filters-sticky'); ?>
|
||||
</div>
|
||||
<div class="property-list-container">
|
||||
<div id="property-results" class="property-results-map">
|
||||
@@ -107,6 +141,14 @@ $view_class = $show_map ? 'is-map-view' : 'is-grid-view';
|
||||
</svg>
|
||||
<span>Map</span>
|
||||
</a>
|
||||
<a href="<?php echo esc_url(home_url('/properties/?near_me=1')); ?>" class="view-toggle-btn view-toggle-nearme">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
<span>Near Me</span>
|
||||
</a>
|
||||
</div>
|
||||
<div id="property-results-grid" class="property-results-grid">
|
||||
<?php get_template_part('template-parts/property/property-results'); ?>
|
||||
@@ -142,9 +184,11 @@ if (function_exists('mls_get_property_count')) {
|
||||
<script>
|
||||
var homeprozMapData = {
|
||||
isMapView: <?php echo $show_map ? 'true' : 'false'; ?>,
|
||||
isNearMeMode: <?php echo $near_me_mode ? 'true' : 'false'; ?>,
|
||||
clusterEndpoint: '<?php echo admin_url('admin-ajax.php'); ?>',
|
||||
initialFilters: <?php echo json_encode($initial_filters); ?>,
|
||||
totalProperties: <?php echo (int) $total_with_coords; ?>
|
||||
totalProperties: <?php echo (int) $total_with_coords; ?>,
|
||||
propertiesUrl: '<?php echo esc_url(home_url('/properties/')); ?>'
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# HomeProz Theme Images
|
||||
|
||||
Index of image assets used in the HomeProz theme.
|
||||
|
||||
## Root Images
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `hero.webp` | Main homepage hero background image |
|
||||
| `hero.png` | Hero image PNG source |
|
||||
| `hero-original.png` | Original unprocessed hero image |
|
||||
| `about-us.webp` | About page hero/feature image |
|
||||
| `logo.webp` | HomeProz logo (standard) |
|
||||
| `logo-cropped.webp` | HomeProz logo cropped version |
|
||||
| `logo-transparent.webp` | HomeProz logo with transparent background |
|
||||
|
||||
## /hero-gallery/
|
||||
|
||||
Alternative hero images for potential rotation or A/B testing.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `hero-2.png` | Alternative hero image 2 |
|
||||
| `hero-3.png` | Alternative hero image 3 |
|
||||
| `hero-4.jpg` | Alternative hero image 4 |
|
||||
| `hero-5.png` | Alternative hero image 5 |
|
||||
| `hero-6.png` | Alternative hero image 6 |
|
||||
|
||||
## /logos/
|
||||
|
||||
Real estate association and compliance logos. Each logo has PNG source and WebP optimized versions.
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `semr-white.webp` | Southeast Minnesota Realtors logo (white, for dark backgrounds) |
|
||||
| `semr-black.webp` | Southeast Minnesota Realtors logo (black, for light backgrounds) |
|
||||
| `realtor-equal-housing-white.webp` | Combined Equal Housing Opportunity + REALTOR logo (white) |
|
||||
| `realtor-equal-housing-black.webp` | Combined Equal Housing Opportunity + REALTOR logo (black) |
|
||||
|
||||
### Usage
|
||||
|
||||
- White versions are used in the site footer (dark background)
|
||||
- Black versions available for light background contexts if needed
|
||||
- PNG source files retained for future edits
|
||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 17 MiB |
|
Before Width: | Height: | Size: 15 MiB After Width: | Height: | Size: 15 MiB |
|
Before Width: | Height: | Size: 4.9 MiB After Width: | Height: | Size: 4.9 MiB |
|
Before Width: | Height: | Size: 24 MiB After Width: | Height: | Size: 24 MiB |
|
Before Width: | Height: | Size: 14 MiB After Width: | Height: | Size: 14 MiB |
|
Before Width: | Height: | Size: 17 MiB After Width: | Height: | Size: 17 MiB |
|
Before Width: | Height: | Size: 39 MiB After Width: | Height: | Size: 39 MiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 10 KiB |
@@ -43,3 +43,27 @@ require_once HOMEPROZ_DIR . '/inc/image-transform.php';
|
||||
|
||||
// Schema.org structured data
|
||||
require_once HOMEPROZ_DIR . '/inc/schema-markup.php';
|
||||
|
||||
// Contact Form 7 hooks (agent email routing)
|
||||
require_once HOMEPROZ_DIR . '/inc/wpcf7-hooks.php';
|
||||
|
||||
/**
|
||||
* Send no-cache headers for HTML pages
|
||||
* Prevents browser/proxy caching of dynamic content
|
||||
*/
|
||||
function homeproz_nocache_headers() {
|
||||
// Only for frontend HTML pages, not admin, AJAX, or REST
|
||||
if (is_admin() || wp_doing_ajax() || defined('REST_REQUEST')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't interfere with feeds
|
||||
if (is_feed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||
header('Pragma: no-cache');
|
||||
header('Expires: 0');
|
||||
}
|
||||
add_action('send_headers', 'homeproz_nocache_headers');
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
/**
|
||||
* ACF Migration Script
|
||||
*
|
||||
* Populates new ACF fields with existing hardcoded data.
|
||||
* Run via WP-CLI: wp eval-file wp-content/themes/homeproz/inc/acf-migration.php
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access unless running from WP-CLI
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate hardcoded content to ACF fields
|
||||
*/
|
||||
function homeproz_migrate_acf_fields() {
|
||||
if (!function_exists('update_field')) {
|
||||
echo "ACF is not active. Cannot run migration.\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
$migrated = array();
|
||||
$skipped = array();
|
||||
|
||||
// Theme Options - only update if field is empty
|
||||
$theme_options = array(
|
||||
'theme_footer_tagline' => 'Your trusted partner in Minnesota and Iowa real estate. Finding homes, building futures.',
|
||||
'theme_broker_info' => 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC',
|
||||
'theme_org_description' => 'Your trusted partner for buying and selling homes in Albert Lea, Minnesota and surrounding areas.',
|
||||
'theme_latitude' => '43.6477',
|
||||
'theme_longitude' => '-93.3683',
|
||||
);
|
||||
|
||||
foreach ($theme_options as $field_name => $default_value) {
|
||||
$current = get_field($field_name, 'option');
|
||||
if (empty($current)) {
|
||||
update_field($field_name, $default_value, 'option');
|
||||
$migrated[] = $field_name;
|
||||
} else {
|
||||
$skipped[] = $field_name . ' (already has value)';
|
||||
}
|
||||
}
|
||||
|
||||
// Office Hours - only populate if empty
|
||||
$current_hours = get_field('theme_office_hours', 'option');
|
||||
if (empty($current_hours)) {
|
||||
$office_hours = array(
|
||||
array('day' => 'Monday - Friday', 'time' => '9:00 AM - 5:00 PM'),
|
||||
array('day' => 'Saturday - Sunday', 'time' => 'By Appointment'),
|
||||
);
|
||||
update_field('theme_office_hours', $office_hours, 'option');
|
||||
$migrated[] = 'theme_office_hours';
|
||||
} else {
|
||||
$skipped[] = 'theme_office_hours (already has value)';
|
||||
}
|
||||
|
||||
// Homepage fields - get front page ID
|
||||
$front_page_id = get_option('page_on_front');
|
||||
if ($front_page_id) {
|
||||
$homepage_fields = array(
|
||||
'home_hero_title' => 'Find Your Dream Home Today',
|
||||
'home_hero_subtitle' => 'Expert real estate services for buyers and sellers in Albert Lea and the surrounding Minnesota communities.',
|
||||
'home_hero_primary_cta_text' => 'View All Properties',
|
||||
'home_hero_secondary_cta_text' => 'Contact Us',
|
||||
'home_services_title' => 'Go With The Proz',
|
||||
'home_services_subtitle' => 'Whether you\'re buying or selling, we\'re here to guide you every step of the way.',
|
||||
'home_featured_homes_title' => 'Featured Homes',
|
||||
'home_featured_homes_subtitle' => 'Browse our residential properties for sale',
|
||||
'home_commercial_title' => 'Commercial & Land',
|
||||
'home_commercial_subtitle' => 'Explore commercial properties and land for sale',
|
||||
'home_cta_title' => 'Ready to Find Your Dream Home?',
|
||||
'home_cta_text' => 'Whether you\'re buying or selling, our team is here to help you every step of the way.',
|
||||
'home_cta_button_text' => 'Contact Us Today',
|
||||
'home_property_types_title' => 'Browse by Property Type',
|
||||
'home_property_types_subtitle' => 'Find the perfect property for your needs',
|
||||
);
|
||||
|
||||
foreach ($homepage_fields as $field_name => $default_value) {
|
||||
$current = get_field($field_name, $front_page_id);
|
||||
if (empty($current)) {
|
||||
update_field($field_name, $default_value, $front_page_id);
|
||||
$migrated[] = $field_name . ' (homepage)';
|
||||
} else {
|
||||
$skipped[] = $field_name . ' (homepage - already has value)';
|
||||
}
|
||||
}
|
||||
|
||||
// Service Cards - only populate if empty
|
||||
$current_cards = get_field('home_service_cards', $front_page_id);
|
||||
if (empty($current_cards)) {
|
||||
$service_cards = array(
|
||||
array(
|
||||
'icon' => 'home',
|
||||
'title' => 'Find a Home',
|
||||
'description' => 'Browse our selection of homes for sale in Albert Lea and surrounding Minnesota communities. Your dream home is waiting.',
|
||||
'button_text' => 'Search Homes',
|
||||
'button_url' => home_url('/properties/?type=residential'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'dollar-sign',
|
||||
'title' => 'Sell Your Home',
|
||||
'description' => 'Ready to sell? Our experienced agents will help you get the best price for your property in today\'s market.',
|
||||
'button_text' => 'Get Started',
|
||||
'button_url' => home_url('/contact/?subject=selling'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'users',
|
||||
'title' => 'Resources & Guides',
|
||||
'description' => 'First-time buyer? Preparing to sell? Our guides walk you through every step of the real estate process.',
|
||||
'button_text' => 'Learn More',
|
||||
'button_url' => home_url('/resources/'),
|
||||
),
|
||||
);
|
||||
update_field('home_service_cards', $service_cards, $front_page_id);
|
||||
$migrated[] = 'home_service_cards (homepage)';
|
||||
} else {
|
||||
$skipped[] = 'home_service_cards (homepage - already has value)';
|
||||
}
|
||||
} else {
|
||||
echo "Warning: No front page set. Homepage fields not migrated.\n";
|
||||
}
|
||||
|
||||
// Output results
|
||||
echo "\n=== ACF Migration Complete ===\n\n";
|
||||
|
||||
if (!empty($migrated)) {
|
||||
echo "Migrated fields:\n";
|
||||
foreach ($migrated as $field) {
|
||||
echo " - $field\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($skipped)) {
|
||||
echo "\nSkipped fields (already have values):\n";
|
||||
foreach ($skipped as $field) {
|
||||
echo " - $field\n";
|
||||
}
|
||||
}
|
||||
|
||||
echo "\nTotal migrated: " . count($migrated) . "\n";
|
||||
echo "Total skipped: " . count($skipped) . "\n";
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Run migration when this file is executed
|
||||
homeproz_migrate_acf_fields();
|
||||
@@ -30,6 +30,23 @@ function homeproz_scripts() {
|
||||
// jQuery is already included in WordPress, just ensure it's loaded
|
||||
wp_enqueue_script('jquery');
|
||||
|
||||
// Slick Slider for agent gallery
|
||||
if (is_singular('agent')) {
|
||||
wp_enqueue_style(
|
||||
'slick-css',
|
||||
'https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.css',
|
||||
array(),
|
||||
'1.8.1'
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'slick-js',
|
||||
'https://cdn.jsdelivr.net/npm/slick-carousel@1.8.1/slick/slick.min.js',
|
||||
array('jquery'),
|
||||
'1.8.1',
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// Main JavaScript (Vite compiled) with filemtime cache buster
|
||||
if (file_exists($js_file)) {
|
||||
wp_enqueue_script(
|
||||
|
||||
@@ -14,39 +14,71 @@ if (!defined('ABSPATH')) {
|
||||
* Output LocalBusiness schema on all pages
|
||||
*/
|
||||
function homeproz_local_business_schema() {
|
||||
// Get data from Theme Options
|
||||
$phone = homeproz_get_option('phone');
|
||||
$email = homeproz_get_option('email');
|
||||
$address = homeproz_get_option('address');
|
||||
$facebook = homeproz_get_option('facebook');
|
||||
$tiktok = homeproz_get_option('tiktok');
|
||||
$org_description = homeproz_get_acf_option('org_description');
|
||||
$latitude = (float) homeproz_get_acf_option('latitude');
|
||||
$longitude = (float) homeproz_get_acf_option('longitude');
|
||||
|
||||
// Parse address into components (format: "111 E Clark St, Albert Lea, MN 56007")
|
||||
$address_parts = array_map('trim', explode(',', $address));
|
||||
$street = isset($address_parts[0]) ? $address_parts[0] : '';
|
||||
$city = isset($address_parts[1]) ? $address_parts[1] : 'Albert Lea';
|
||||
$state_zip = isset($address_parts[2]) ? $address_parts[2] : 'MN 56007';
|
||||
$state_zip_parts = explode(' ', trim($state_zip));
|
||||
$state = isset($state_zip_parts[0]) ? $state_zip_parts[0] : 'MN';
|
||||
$zip = isset($state_zip_parts[1]) ? $state_zip_parts[1] : '56007';
|
||||
|
||||
// Format phone for schema (E.164 format)
|
||||
$phone_clean = preg_replace('/[^0-9]/', '', $phone);
|
||||
$phone_formatted = '+1-' . substr($phone_clean, 0, 3) . '-' . substr($phone_clean, 3, 3) . '-' . substr($phone_clean, 6);
|
||||
|
||||
// Build sameAs array from social links
|
||||
$same_as = array();
|
||||
if ($facebook) {
|
||||
$same_as[] = $facebook;
|
||||
}
|
||||
if ($tiktok) {
|
||||
$same_as[] = $tiktok;
|
||||
}
|
||||
|
||||
$schema = array(
|
||||
'@context' => 'https://schema.org',
|
||||
'@type' => 'RealEstateAgent',
|
||||
'@id' => home_url('/#organization'),
|
||||
'name' => 'HomeProz Real Estate',
|
||||
'description' => 'Your trusted partner for buying and selling homes in Albert Lea, Minnesota and surrounding areas.',
|
||||
'name' => get_bloginfo('name'),
|
||||
'description' => $org_description,
|
||||
'url' => home_url('/'),
|
||||
'logo' => array(
|
||||
'@type' => 'ImageObject',
|
||||
'url' => HOMEPROZ_URI . '/assets/images/logo.png',
|
||||
),
|
||||
'image' => HOMEPROZ_URI . '/assets/images/logo.png',
|
||||
'telephone' => '+1-507-383-2048',
|
||||
'email' => 'contact@homeprozrealestate.com',
|
||||
'telephone' => $phone_formatted,
|
||||
'email' => $email,
|
||||
'address' => array(
|
||||
'@type' => 'PostalAddress',
|
||||
'streetAddress' => '113 E Clark St',
|
||||
'addressLocality' => 'Albert Lea',
|
||||
'addressRegion' => 'MN',
|
||||
'postalCode' => '56007',
|
||||
'streetAddress' => $street,
|
||||
'addressLocality' => $city,
|
||||
'addressRegion' => $state,
|
||||
'postalCode' => $zip,
|
||||
'addressCountry' => 'US',
|
||||
),
|
||||
'geo' => array(
|
||||
'@type' => 'GeoCoordinates',
|
||||
'latitude' => 43.6477,
|
||||
'longitude' => -93.3683,
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude,
|
||||
),
|
||||
'areaServed' => array(
|
||||
'@type' => 'GeoCircle',
|
||||
'geoMidpoint' => array(
|
||||
'@type' => 'GeoCoordinates',
|
||||
'latitude' => 43.6477,
|
||||
'longitude' => -93.3683,
|
||||
'latitude' => $latitude,
|
||||
'longitude' => $longitude,
|
||||
),
|
||||
'geoRadius' => '50000',
|
||||
),
|
||||
@@ -58,11 +90,13 @@ function homeproz_local_business_schema() {
|
||||
'closes' => '17:00',
|
||||
),
|
||||
),
|
||||
'sameAs' => array(
|
||||
'https://www.facebook.com/homeprozrealestate',
|
||||
),
|
||||
);
|
||||
|
||||
// Add sameAs if we have social links
|
||||
if (!empty($same_as)) {
|
||||
$schema['sameAs'] = $same_as;
|
||||
}
|
||||
|
||||
echo '<script type="application/ld+json">' . wp_json_encode($schema, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . '</script>' . "\n";
|
||||
}
|
||||
add_action('wp_head', 'homeproz_local_business_schema', 5);
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<?php
|
||||
/**
|
||||
* Contact Form 7 Hooks
|
||||
*
|
||||
* Handles programmatic email routing for agent contact forms
|
||||
* and property inquiry forms.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send additional email to specific agent after form submission
|
||||
*
|
||||
* This hook fires after CF7 successfully sends the main email.
|
||||
* It checks for agent-related hidden fields and sends a copy to
|
||||
* the specific agent.
|
||||
*
|
||||
* @param WPCF7_ContactForm $contact_form The contact form object
|
||||
*/
|
||||
function homeproz_wpcf7_send_agent_copy($contact_form) {
|
||||
$submission = WPCF7_Submission::get_instance();
|
||||
|
||||
if (!$submission) {
|
||||
return;
|
||||
}
|
||||
|
||||
$posted_data = $submission->get_posted_data();
|
||||
|
||||
// Check if this is an agent contact form (has agent-email field)
|
||||
if (!empty($posted_data['agent-email'])) {
|
||||
homeproz_send_agent_contact_copy($posted_data, $contact_form);
|
||||
}
|
||||
|
||||
// Check if this is a property inquiry form (has listing-key field)
|
||||
if (!empty($posted_data['listing-key'])) {
|
||||
homeproz_send_property_inquiry_agent_copy($posted_data, $contact_form);
|
||||
}
|
||||
}
|
||||
add_action('wpcf7_mail_sent', 'homeproz_wpcf7_send_agent_copy');
|
||||
|
||||
/**
|
||||
* Send copy of agent contact form to the specific agent
|
||||
*
|
||||
* @param array $posted_data Form submission data
|
||||
* @param WPCF7_ContactForm $contact_form The contact form object
|
||||
*/
|
||||
function homeproz_send_agent_contact_copy($posted_data, $contact_form) {
|
||||
$agent_email = sanitize_email($posted_data['agent-email']);
|
||||
$agent_name = sanitize_text_field($posted_data['agent-name'] ?? 'Agent');
|
||||
|
||||
// Validate email
|
||||
if (!is_email($agent_email)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build email content
|
||||
$sender_name = sanitize_text_field($posted_data['your-name'] ?? $posted_data['name'] ?? 'Website Visitor');
|
||||
$sender_email = sanitize_email($posted_data['your-email'] ?? $posted_data['email'] ?? '');
|
||||
$sender_phone = sanitize_text_field($posted_data['your-phone'] ?? $posted_data['phone'] ?? '');
|
||||
$message = sanitize_textarea_field($posted_data['your-message'] ?? $posted_data['message'] ?? $posted_data['comments'] ?? '');
|
||||
$default_message = sanitize_textarea_field($posted_data['default-message'] ?? '');
|
||||
|
||||
$subject = sprintf('[HomeProz] New Contact Form Message for %s', $agent_name);
|
||||
|
||||
$body = "Hello {$agent_name},\n\n";
|
||||
$body .= "You have received a new message through the HomeProz website contact form.\n\n";
|
||||
$body .= "---\n\n";
|
||||
|
||||
if ($default_message) {
|
||||
$body .= "Introduction:\n{$default_message}\n\n";
|
||||
}
|
||||
|
||||
if ($message) {
|
||||
$body .= "Additional Message:\n{$message}\n\n";
|
||||
}
|
||||
|
||||
$body .= "---\n\n";
|
||||
$body .= "Contact Information:\n";
|
||||
$body .= "Name: {$sender_name}\n";
|
||||
|
||||
if ($sender_email) {
|
||||
$body .= "Email: {$sender_email}\n";
|
||||
}
|
||||
|
||||
if ($sender_phone) {
|
||||
$body .= "Phone: {$sender_phone}\n";
|
||||
}
|
||||
|
||||
$body .= "\n---\n";
|
||||
$body .= "This is an automated copy sent to you as the contacted agent.\n";
|
||||
$body .= "The office has also received a copy of this submission.";
|
||||
|
||||
$headers = array('Content-Type: text/plain; charset=UTF-8');
|
||||
|
||||
if ($sender_email) {
|
||||
$headers[] = 'Reply-To: ' . $sender_name . ' <' . $sender_email . '>';
|
||||
}
|
||||
|
||||
wp_mail($agent_email, $subject, $body, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send copy of property inquiry form to the listing agent
|
||||
*
|
||||
* For HomeProz listings (is_homeproz = 1), looks up the listing agent
|
||||
* and sends them a copy of the inquiry.
|
||||
*
|
||||
* @param array $posted_data Form submission data
|
||||
* @param WPCF7_ContactForm $contact_form The contact form object
|
||||
*/
|
||||
function homeproz_send_property_inquiry_agent_copy($posted_data, $contact_form) {
|
||||
$listing_key = sanitize_text_field($posted_data['listing-key']);
|
||||
|
||||
if (!$listing_key || !function_exists('mls_get_property')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the property from MLS database
|
||||
$property = mls_get_property($listing_key);
|
||||
|
||||
if (!$property) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send agent copy for HomeProz listings
|
||||
if (empty($property->is_homeproz) || !$property->is_homeproz) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to find the listing agent by MLS ID
|
||||
$list_agent_id = $property->list_agent_mls_id ?? '';
|
||||
|
||||
if (!$list_agent_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Query for agent with matching MLS ID
|
||||
$agents = get_posts(array(
|
||||
'post_type' => 'agent',
|
||||
'posts_per_page' => 1,
|
||||
'post_status' => 'publish',
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'agent_mls_id',
|
||||
'value' => $list_agent_id,
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
));
|
||||
|
||||
if (empty($agents)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$agent = $agents[0];
|
||||
$agent_email = get_field('agent_email', $agent->ID);
|
||||
$agent_name = $agent->post_title;
|
||||
$agent_disabled = get_field('agent_disabled', $agent->ID);
|
||||
|
||||
// Don't send to disabled agents
|
||||
if ($agent_disabled || !is_email($agent_email)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build email content
|
||||
$sender_name = sanitize_text_field($posted_data['your-name'] ?? $posted_data['name'] ?? 'Website Visitor');
|
||||
$sender_email = sanitize_email($posted_data['your-email'] ?? $posted_data['email'] ?? '');
|
||||
$sender_phone = sanitize_text_field($posted_data['your-phone'] ?? $posted_data['phone'] ?? '');
|
||||
$message = sanitize_textarea_field($posted_data['your-message'] ?? $posted_data['message'] ?? $posted_data['comments'] ?? '');
|
||||
$default_message = sanitize_textarea_field($posted_data['default-message'] ?? '');
|
||||
$property_address = sanitize_text_field($posted_data['property-address'] ?? '');
|
||||
$listing_id = sanitize_text_field($posted_data['listing-id'] ?? '');
|
||||
$property_url = esc_url($posted_data['property-url'] ?? '');
|
||||
|
||||
$subject = sprintf('[HomeProz] Property Inquiry - %s', $property_address ?: 'MLS# ' . $listing_id);
|
||||
|
||||
$body = "Hello {$agent_name},\n\n";
|
||||
$body .= "You have received a new property inquiry for one of your HomeProz listings.\n\n";
|
||||
$body .= "---\n\n";
|
||||
|
||||
$body .= "Property Details:\n";
|
||||
if ($property_address) {
|
||||
$body .= "Address: {$property_address}\n";
|
||||
}
|
||||
if ($listing_id) {
|
||||
$body .= "MLS#: {$listing_id}\n";
|
||||
}
|
||||
if ($property_url) {
|
||||
$body .= "View Listing: {$property_url}\n";
|
||||
}
|
||||
$body .= "\n";
|
||||
|
||||
if ($default_message) {
|
||||
$body .= "Inquiry:\n{$default_message}\n\n";
|
||||
}
|
||||
|
||||
if ($message) {
|
||||
$body .= "Additional Message:\n{$message}\n\n";
|
||||
}
|
||||
|
||||
$body .= "---\n\n";
|
||||
$body .= "Contact Information:\n";
|
||||
$body .= "Name: {$sender_name}\n";
|
||||
|
||||
if ($sender_email) {
|
||||
$body .= "Email: {$sender_email}\n";
|
||||
}
|
||||
|
||||
if ($sender_phone) {
|
||||
$body .= "Phone: {$sender_phone}\n";
|
||||
}
|
||||
|
||||
$body .= "\n---\n";
|
||||
$body .= "This is an automated copy sent to you as the listing agent.\n";
|
||||
$body .= "The office has also received a copy of this inquiry.";
|
||||
|
||||
$headers = array('Content-Type: text/plain; charset=UTF-8');
|
||||
|
||||
if ($sender_email) {
|
||||
$headers[] = 'Reply-To: ' . $sender_name . ' <' . $sender_email . '>';
|
||||
}
|
||||
|
||||
wp_mail($agent_email, $subject, $body, $headers);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
# Man Pages Directory
|
||||
|
||||
This directory contains manual pages for the HomeProz theme - technical reference documentation written in the style and tone of Unix man pages.
|
||||
|
||||
## Purpose
|
||||
|
||||
Man pages serve as authoritative technical reference for the codebase. They are:
|
||||
|
||||
- **Factual**: Document what exists, not tutorials on how to use it
|
||||
- **Complete**: Cover all aspects of a feature or system
|
||||
- **Stoic**: Written in neutral, technical prose without marketing language
|
||||
- **Versioned**: Updated when functionality changes
|
||||
- **Referenceable**: Structured for quick lookup of specific information
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **LLM Context**: Provide specification when working on related features
|
||||
2. **Change Tracking**: Reference when updating functionality to ensure completeness
|
||||
3. **End-User Docs Source**: Base material for generating user-friendly documentation
|
||||
4. **Onboarding**: Technical reference for developers new to the codebase
|
||||
|
||||
## File Format
|
||||
|
||||
Files use `.txt` extension with the following structure:
|
||||
|
||||
```
|
||||
NAME
|
||||
feature-name - one line description
|
||||
|
||||
SYNOPSIS
|
||||
Brief overview of what this documents
|
||||
|
||||
DESCRIPTION
|
||||
Detailed explanation of the feature/system
|
||||
|
||||
SECTIONS
|
||||
Organized by logical groupings
|
||||
|
||||
SEE ALSO
|
||||
Related man pages or documentation
|
||||
|
||||
HISTORY
|
||||
Change log with dates
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Files are named by their subject in lowercase with hyphens:
|
||||
|
||||
- `cms-fields.txt` - CMS editable fields reference
|
||||
- `theme-options.txt` - Theme options documentation
|
||||
- `custom-post-types.txt` - CPT specifications
|
||||
- `page-templates.txt` - Template hierarchy and usage
|
||||
|
||||
## Writing Style
|
||||
|
||||
- Use present tense
|
||||
- Avoid first/second person
|
||||
- Be specific about locations (file paths, admin URLs)
|
||||
- Include default values where applicable
|
||||
- Note dependencies and requirements
|
||||
- Mark optional vs required items
|
||||
|
||||
## Maintenance
|
||||
|
||||
When modifying functionality documented in a man page:
|
||||
|
||||
1. Update the man page in the same commit/session
|
||||
2. Add entry to HISTORY section with date
|
||||
3. Mark new items with notation if tracking changes
|
||||
@@ -0,0 +1,524 @@
|
||||
NAME
|
||||
cms-fields - CMS editable content fields reference for HomeProz theme
|
||||
|
||||
SYNOPSIS
|
||||
Complete reference of all content that is editable via the WordPress
|
||||
admin interface, including ACF fields, theme options, menus, and
|
||||
widget areas.
|
||||
|
||||
DESCRIPTION
|
||||
This document catalogs every piece of editable content in the HomeProz
|
||||
WordPress theme. Each entry specifies:
|
||||
|
||||
- The content element
|
||||
- Where it appears on the site
|
||||
- How to edit it (admin location)
|
||||
- Field type and constraints
|
||||
- Default value if applicable
|
||||
|
||||
Items marked with *NEW were added during the CMS field migration
|
||||
(TASK-001) and should be verified for correct functionality.
|
||||
|
||||
THEME OPTIONS
|
||||
Location: Appearance > Theme Options
|
||||
|
||||
Contact Information Tab
|
||||
theme_phone Phone number displayed site-wide
|
||||
Type: text
|
||||
Default: 507-516-4870
|
||||
|
||||
theme_email Email address displayed site-wide
|
||||
Type: email
|
||||
Default: info@homeprozrealestate.com
|
||||
|
||||
theme_address Office address displayed in footer/contact
|
||||
Type: textarea
|
||||
Default: 111 E Clark St, Albert Lea, MN 56007
|
||||
|
||||
Properties Page Tab
|
||||
properties_hero_title Properties archive hero heading
|
||||
Type: text
|
||||
Default: Find Your Perfect Property
|
||||
|
||||
properties_hero_subtitle Properties archive hero subheading
|
||||
Type: textarea
|
||||
Default: Browse our selection of homes...
|
||||
|
||||
properties_hero_background Properties archive hero background image
|
||||
Type: image (url)
|
||||
|
||||
Social Media Tab
|
||||
theme_facebook Facebook page URL
|
||||
Type: url
|
||||
|
||||
theme_tiktok TikTok profile URL
|
||||
Type: url
|
||||
|
||||
theme_instagram Instagram profile URL
|
||||
Type: url
|
||||
|
||||
theme_youtube YouTube channel URL
|
||||
Type: url
|
||||
|
||||
Footer & Branding Tab
|
||||
theme_footer_tagline Short description in footer about section
|
||||
Type: textarea
|
||||
Default: Your trusted partner in Minnesota...
|
||||
|
||||
theme_broker_info Legal broker disclosure text in footer
|
||||
Type: textarea
|
||||
Default: HomeProz Real Estate LLC DBA...
|
||||
|
||||
theme_org_description SEO/schema organization description
|
||||
Type: textarea
|
||||
|
||||
Office Hours Tab
|
||||
theme_office_hours Business hours (repeater)
|
||||
Sub-fields: day (text), time (text)
|
||||
Default: Mon-Fri 9-5, Weekends By Appointment
|
||||
|
||||
Location Tab
|
||||
theme_latitude Office latitude for maps/schema
|
||||
Type: number
|
||||
Default: 43.6477
|
||||
|
||||
theme_longitude Office longitude for maps/schema
|
||||
Type: number
|
||||
Default: -93.3683
|
||||
|
||||
theme_map_embed Google Maps embed iframe code
|
||||
Type: textarea
|
||||
|
||||
*NEW 404 Page Tab
|
||||
theme_404_title Large heading on 404 page
|
||||
Type: text
|
||||
Default: 404
|
||||
|
||||
theme_404_subtitle Subheading on 404 page
|
||||
Type: text
|
||||
Default: Page Not Found
|
||||
|
||||
theme_404_message Explanatory message on 404 page
|
||||
Type: textarea
|
||||
Default: The page you're looking for...
|
||||
|
||||
theme_404_primary_button Primary button text
|
||||
Type: text
|
||||
Default: Go Home
|
||||
|
||||
theme_404_secondary_button Secondary button text
|
||||
Type: text
|
||||
Default: View Properties
|
||||
|
||||
*NEW Footer Copyright Tab
|
||||
theme_footer_copyright Copyright text in footer (year added automatically)
|
||||
Type: text
|
||||
Default: HomeProz Real Estate LLC. All rights reserved.
|
||||
Note: Web design credit is fixed and not editable
|
||||
|
||||
HOMEPAGE FIELDS
|
||||
Location: Pages > Front Page (Homepage) > Edit
|
||||
|
||||
Hero Section Tab
|
||||
home_hero_title Main hero heading
|
||||
Type: text
|
||||
Default: Find Your Dream Home Today
|
||||
|
||||
home_hero_subtitle Hero subheading/description
|
||||
Type: textarea
|
||||
|
||||
home_hero_primary_cta_text Primary button text
|
||||
Type: text
|
||||
Default: View All Properties
|
||||
|
||||
home_hero_primary_cta_url Primary button URL
|
||||
Type: url
|
||||
Default: /properties/
|
||||
|
||||
home_hero_secondary_cta_text Secondary button text
|
||||
Type: text
|
||||
Default: Contact Us
|
||||
|
||||
home_hero_secondary_cta_url Secondary button URL
|
||||
Type: url
|
||||
Default: /contact/
|
||||
|
||||
home_hero_background Hero background image
|
||||
Type: image (url)
|
||||
|
||||
Service Cards Tab
|
||||
home_services_title Service section heading
|
||||
Type: text
|
||||
Default: Go With The Proz
|
||||
|
||||
home_services_subtitle Service section subheading
|
||||
Type: textarea
|
||||
|
||||
home_service_cards Service cards (repeater, max 6)
|
||||
Sub-fields: icon, title, description,
|
||||
button_text, button_url
|
||||
|
||||
Featured Sections Tab
|
||||
home_featured_homes_title Featured homes heading
|
||||
Type: text
|
||||
Default: Featured Homes
|
||||
|
||||
home_featured_homes_subtitle Featured homes subheading
|
||||
Type: textarea
|
||||
|
||||
home_commercial_title Commercial section heading
|
||||
Type: text
|
||||
Default: Commercial & Land
|
||||
|
||||
home_commercial_subtitle Commercial section subheading
|
||||
Type: textarea
|
||||
|
||||
CTA Section Tab
|
||||
home_cta_title Bottom CTA heading
|
||||
Type: text
|
||||
|
||||
home_cta_text Bottom CTA description
|
||||
Type: textarea
|
||||
|
||||
home_cta_button_text CTA button text
|
||||
Type: text
|
||||
Default: Contact Us Today
|
||||
|
||||
home_cta_button_url CTA button URL
|
||||
Type: url
|
||||
|
||||
Property Types Tab
|
||||
home_property_types_title Property types heading
|
||||
Type: text
|
||||
|
||||
home_property_types_subtitle Property types subheading
|
||||
Type: textarea
|
||||
|
||||
*NEW Hero Gallery Tab
|
||||
home_hero_gallery Rotating hero background images
|
||||
Type: gallery (max 10)
|
||||
Returns: URL
|
||||
Fallback: Theme default images
|
||||
|
||||
PAGE HERO FIELDS
|
||||
Location: Any page (except homepage) > Edit
|
||||
|
||||
Applies to all pages via Page Hero field group:
|
||||
|
||||
hero_title Override page title in hero
|
||||
Type: text
|
||||
Default: Uses page title
|
||||
|
||||
hero_subtitle Description below hero title
|
||||
Type: textarea
|
||||
Max: 200 characters
|
||||
|
||||
hero_background Hero section background image
|
||||
Type: image (url)
|
||||
|
||||
*NEW ABOUT PAGE FIELDS
|
||||
Location: Pages > About > Edit
|
||||
|
||||
about_team_title Team section heading
|
||||
Type: text
|
||||
Default: Meet Our Team
|
||||
|
||||
about_team_subtitle Team section subheading
|
||||
Type: textarea
|
||||
Default: A dedicated group of...
|
||||
|
||||
about_featured_image Company story section image
|
||||
Type: image (url)
|
||||
Fallback: assets/images/about-us.webp
|
||||
|
||||
about_broker_title Broker section heading
|
||||
Type: text
|
||||
Default: Broker Information
|
||||
|
||||
about_broker_text Broker information content (HTML)
|
||||
Type: wysiwyg
|
||||
Default: HomeProz Real Estate LLC...
|
||||
|
||||
about_cta_title CTA section heading
|
||||
Type: text
|
||||
Default: Ready to Work With Us?
|
||||
|
||||
about_cta_text CTA section description
|
||||
Type: textarea
|
||||
|
||||
about_cta_button_text CTA button text
|
||||
Type: text
|
||||
Default: Get in Touch
|
||||
|
||||
about_cta_button_url CTA button URL
|
||||
Type: url
|
||||
Default: /contact/
|
||||
|
||||
*NEW CONTACT PAGE FIELDS
|
||||
Location: Pages > Contact > Edit
|
||||
|
||||
contact_form_title Form section heading
|
||||
Type: text
|
||||
Default: Send Us a Message
|
||||
|
||||
contact_info_title Info section heading
|
||||
Type: text
|
||||
Default: Contact Information
|
||||
|
||||
*NEW PROPERTY INQUIRY PAGE FIELDS
|
||||
Location: Pages > Property Inquiry > Edit
|
||||
|
||||
inquiry_hero_title Hero section heading
|
||||
Type: text
|
||||
Default: Request Property Information
|
||||
|
||||
inquiry_hero_subtitle Hero section subheading
|
||||
Type: text
|
||||
Default: Get more details about this listing
|
||||
|
||||
inquiry_preview_title Property preview section heading
|
||||
Type: text
|
||||
Default: Property You're Inquiring About
|
||||
|
||||
*NEW THANK YOU PAGE FIELDS
|
||||
Location: Pages > Inquiry Thank You > Edit
|
||||
|
||||
thankyou_hero_title Hero section heading
|
||||
Type: text
|
||||
Default: Thank You!
|
||||
|
||||
thankyou_hero_subtitle Hero section subheading
|
||||
Type: text
|
||||
Default: Your inquiry has been submitted
|
||||
|
||||
thankyou_heading Main content heading
|
||||
Type: text
|
||||
Default: We've Received Your Request
|
||||
|
||||
thankyou_message Confirmation message
|
||||
Type: textarea
|
||||
Default: One of our team members...
|
||||
|
||||
thankyou_property_heading Property section heading
|
||||
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
|
||||
|
||||
Pricing & Status Tab
|
||||
property_price Listing price (numbers only)
|
||||
Type: number
|
||||
Required: yes
|
||||
|
||||
mls_number MLS listing number
|
||||
Type: text
|
||||
|
||||
external_listing_url External listing page URL
|
||||
Type: url
|
||||
|
||||
Address Tab
|
||||
street_address Street address
|
||||
Type: text
|
||||
Required: yes
|
||||
|
||||
city City name
|
||||
Type: text
|
||||
Required: yes
|
||||
Default: Albert Lea
|
||||
|
||||
state State abbreviation
|
||||
Type: text
|
||||
Required: yes
|
||||
Default: MN
|
||||
|
||||
zip_code ZIP code
|
||||
Type: text
|
||||
Required: yes
|
||||
|
||||
Property Details Tab
|
||||
bedrooms Number of bedrooms
|
||||
Type: number (0-20)
|
||||
|
||||
bathrooms Number of bathrooms
|
||||
Type: number (0-20, step 0.5)
|
||||
|
||||
square_feet Interior square footage
|
||||
Type: number
|
||||
|
||||
lot_size Lot size description
|
||||
Type: text
|
||||
|
||||
year_built Year constructed
|
||||
Type: number (1800-2100)
|
||||
|
||||
garage Garage size
|
||||
Type: select (None/1-4+ Car)
|
||||
|
||||
Features Tab
|
||||
short_description Brief card description
|
||||
Type: textarea
|
||||
Max: 250 characters
|
||||
|
||||
property_features Property amenities
|
||||
Type: checkbox (18 options)
|
||||
|
||||
Photo Gallery Tab
|
||||
property_gallery Property photos
|
||||
Type: gallery (max 30)
|
||||
Returns: ID
|
||||
|
||||
Documents Tab
|
||||
property_documents Supporting documents (repeater, max 10)
|
||||
Sub-fields: file, label
|
||||
File types: pdf, doc, docx, xls, xlsx
|
||||
|
||||
Listing Agent Tab
|
||||
listing_agent Assigned agent
|
||||
Type: post_object (agent CPT)
|
||||
Optional: yes
|
||||
|
||||
AGENT FIELDS
|
||||
Location: Agents > [Agent] > Edit
|
||||
|
||||
Contact Info Tab
|
||||
agent_phone Primary phone number
|
||||
Type: text
|
||||
|
||||
agent_email Email address
|
||||
Type: email
|
||||
|
||||
agent_website Personal website URL
|
||||
Type: url
|
||||
|
||||
agent_title Job title
|
||||
Type: text
|
||||
Default: Real Estate Agent
|
||||
|
||||
agent_license License number
|
||||
Type: text
|
||||
|
||||
agent_mls_id NorthstarMLS agent ID
|
||||
Type: text
|
||||
|
||||
Biography Tab
|
||||
agent_short_bio Brief bio for cards
|
||||
Type: textarea
|
||||
Max: 300 characters
|
||||
|
||||
Photo Gallery Tab
|
||||
agent_gallery Additional agent photos
|
||||
Type: gallery (max 20)
|
||||
Returns: ID
|
||||
|
||||
Social Media Tab
|
||||
agent_social_links Social profiles (repeater, max 10)
|
||||
Sub-fields: platform (select), url
|
||||
|
||||
Settings Tab
|
||||
agent_order Display order on agents page
|
||||
Type: number (0-999)
|
||||
Default: 10
|
||||
|
||||
agent_disabled Hide agent from site
|
||||
Type: true_false
|
||||
Default: false
|
||||
|
||||
PAGE TEMPLATES
|
||||
Each template has its own field group. Location varies by template.
|
||||
|
||||
Content with Sidebar (page-templates/content-sidebar.php)
|
||||
sidebar_callouts Sidebar callout boxes (repeater)
|
||||
page_cta_title CTA section title
|
||||
page_cta_text CTA section text
|
||||
page_cta_button_text CTA button text
|
||||
page_cta_button_url CTA button URL
|
||||
|
||||
Alternating Blocks (page-templates/alternating-blocks.php)
|
||||
alt_hero_title Hero title
|
||||
alt_hero_subtitle Hero subtitle
|
||||
alt_hero_background Hero background image
|
||||
content_blocks Alternating content blocks (repeater)
|
||||
alt_cta_* CTA section fields
|
||||
|
||||
Service Detail (page-templates/service-detail.php)
|
||||
service_hero_* Hero section fields
|
||||
service_intro Introduction content (wysiwyg)
|
||||
service_features Feature cards (repeater)
|
||||
service_faq FAQ items (repeater)
|
||||
service_cta_* CTA section fields
|
||||
|
||||
Card Grid (page-templates/card-grid.php)
|
||||
grid_title Section title
|
||||
grid_subtitle Section subtitle
|
||||
grid_intro Introduction (wysiwyg)
|
||||
grid_columns Number of columns (2/3/4)
|
||||
grid_cards Card items (repeater)
|
||||
grid_cta_* CTA section fields
|
||||
|
||||
Long-Form Article (page-templates/long-form-article.php)
|
||||
article_subtitle Optional subtitle
|
||||
show_breadcrumbs Toggle breadcrumbs
|
||||
related_links Related links (repeater)
|
||||
|
||||
Landing Page (page-templates/landing-page.php)
|
||||
landing_hero_* Hero section fields
|
||||
landing_benefits Benefits list (repeater)
|
||||
landing_testimonial_* Testimonial fields
|
||||
landing_final_cta_* Final CTA fields
|
||||
|
||||
NAVIGATION MENUS
|
||||
Location: Appearance > Menus
|
||||
|
||||
primary Main header navigation
|
||||
Location: Header
|
||||
|
||||
footer Footer link list
|
||||
Location: Footer
|
||||
|
||||
TAXONOMIES
|
||||
Properties use these taxonomies (editable in Properties sidebar):
|
||||
|
||||
property_status Active, Pending, Sold
|
||||
property_type Residential, Commercial, Land, etc.
|
||||
property_location Geographic areas/communities
|
||||
|
||||
HELPER FUNCTIONS
|
||||
For theme developers accessing CMS content:
|
||||
|
||||
homeproz_get_option($key, $default)
|
||||
Get theme option by key with fallback
|
||||
|
||||
homeproz_get_acf_option($key, $default)
|
||||
Get ACF theme option with field mapping
|
||||
|
||||
homeproz_get_office_hours()
|
||||
Get office hours array from theme options
|
||||
|
||||
SEE ALSO
|
||||
inc/acf-fields.php ACF field definitions
|
||||
inc/theme-functions.php Helper functions
|
||||
tasks/TASK-001-AUDIT-REPORT.md Original audit findings
|
||||
|
||||
HISTORY
|
||||
2024-12-29 Initial creation from TASK-001 migration
|
||||
Added *NEW fields for About, Contact, 404, Inquiry,
|
||||
Thank You pages, and footer credits
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Name: Contact Agent
|
||||
* Template for contacting a specific agent
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get agent ID from URL (using agent_id to avoid conflict with agent post type query var)
|
||||
$agent_id = isset($_GET['agent_id']) ? absint($_GET['agent_id']) : 0;
|
||||
|
||||
// If no agent ID, try to get by slug
|
||||
if (!$agent_id && isset($_GET['name'])) {
|
||||
$agent_slug = sanitize_title($_GET['name']);
|
||||
$agent_post = get_page_by_path($agent_slug, OBJECT, 'agent');
|
||||
if ($agent_post) {
|
||||
$agent_id = $agent_post->ID;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to about page if no valid agent
|
||||
if (!$agent_id) {
|
||||
wp_redirect(home_url('/about/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get agent post
|
||||
$agent = get_post($agent_id);
|
||||
|
||||
// Check if agent exists and is published
|
||||
if (!$agent || $agent->post_type !== 'agent' || $agent->post_status !== 'publish') {
|
||||
wp_redirect(home_url('/about/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if agent is disabled
|
||||
$agent_disabled = get_field('agent_disabled', $agent_id);
|
||||
if ($agent_disabled) {
|
||||
wp_redirect(home_url('/about/'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get agent data
|
||||
$agent_name = $agent->post_title;
|
||||
$agent_title = get_field('agent_title', $agent_id);
|
||||
$agent_email = get_field('agent_email', $agent_id);
|
||||
$agent_phone = get_field('agent_phone', $agent_id);
|
||||
$agent_photo_id = get_post_thumbnail_id($agent_id);
|
||||
$agent_photo_url = $agent_photo_id ? wp_get_attachment_image_url($agent_photo_id, 'medium') : '';
|
||||
$agent_permalink = get_permalink($agent_id);
|
||||
|
||||
// Default message
|
||||
$default_message = "Hello " . $agent_name . ",\n\nI would like to get in touch with you regarding real estate services.";
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<main id="primary" class="site-main contact-agent-page-main">
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="archive-hero">
|
||||
<div class="container">
|
||||
<h1 class="archive-hero-title">Contact <?php echo esc_html($agent_name); ?></h1>
|
||||
<p class="archive-hero-subtitle">Send a message directly to <?php echo esc_html($agent_name); ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Contact Form Section -->
|
||||
<section class="contact-agent-section">
|
||||
<div class="container">
|
||||
<div class="contact-agent-grid">
|
||||
<!-- Form -->
|
||||
<div class="contact-agent-form-wrapper">
|
||||
<?php
|
||||
// Check for Contact Form 7
|
||||
if (function_exists('wpcf7_contact_form')) {
|
||||
$contact_form = get_page_by_title('Contact Agent Form', OBJECT, 'wpcf7_contact_form');
|
||||
if ($contact_form) {
|
||||
echo do_shortcode('[contact-form-7 id="' . $contact_form->ID . '" title="Contact Agent Form"]');
|
||||
} else {
|
||||
// Fallback - use generic contact form
|
||||
$generic_form = get_page_by_title('Contact Form', OBJECT, 'wpcf7_contact_form');
|
||||
if ($generic_form) {
|
||||
echo do_shortcode('[contact-form-7 id="' . $generic_form->ID . '" title="Contact Form"]');
|
||||
} else {
|
||||
// Fallback form markup
|
||||
?>
|
||||
<form class="contact-agent-form" action="" method="post">
|
||||
<input type="hidden" name="agent-id" value="<?php echo esc_attr($agent_id); ?>">
|
||||
<input type="hidden" name="agent-name" value="<?php echo esc_attr($agent_name); ?>">
|
||||
<input type="hidden" name="agent-email" value="<?php echo esc_attr($agent_email); ?>">
|
||||
|
||||
<div class="form-group">
|
||||
<label>Your Message</label>
|
||||
<div class="readonly-message-display"><?php echo esc_html($default_message); ?></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="contact-comments">Additional Comments</label>
|
||||
<textarea id="contact-comments" name="comments" rows="4" placeholder="Tell us how we can help you..."></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="contact-name">Your Name <span class="required">*</span></label>
|
||||
<input type="text" id="contact-name" name="name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-2col">
|
||||
<div class="form-group">
|
||||
<label for="contact-email">Email <span class="required">*</span></label>
|
||||
<input type="email" id="contact-email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="contact-phone">Phone <span class="required">*</span></label>
|
||||
<input type="tel" id="contact-phone" name="phone" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">Send Message</button>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- Agent Preview -->
|
||||
<div class="contact-agent-preview">
|
||||
<h3 class="preview-title">You're Contacting</h3>
|
||||
|
||||
<div class="agent-preview-card">
|
||||
<div class="agent-preview-photo">
|
||||
<?php if ($agent_photo_url) : ?>
|
||||
<img src="<?php echo esc_url($agent_photo_url); ?>" alt="<?php echo esc_attr($agent_name); ?>">
|
||||
<?php else : ?>
|
||||
<div class="agent-preview-placeholder">
|
||||
<svg width="48" height="48" 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-preview-info">
|
||||
<?php if ($agent_title) : ?>
|
||||
<span class="agent-preview-title"><?php echo esc_html($agent_title); ?></span>
|
||||
<?php endif; ?>
|
||||
<h4 class="agent-preview-name"><?php echo esc_html($agent_name); ?></h4>
|
||||
|
||||
<div class="agent-preview-contact">
|
||||
<?php if ($agent_phone) : ?>
|
||||
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $agent_phone)); ?>" class="agent-preview-link">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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($agent_phone); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($agent_email) : ?>
|
||||
<a href="mailto:<?php echo esc_attr($agent_email); ?>" class="agent-preview-link">
|
||||
<svg width="16" height="16" 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>
|
||||
Email
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="<?php echo esc_url($agent_permalink); ?>" class="agent-preview-profile-link">
|
||||
View Full Profile
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var agentId = <?php echo json_encode($agent_id); ?>;
|
||||
var agentName = <?php echo json_encode($agent_name); ?>;
|
||||
var agentEmail = <?php echo json_encode($agent_email); ?>;
|
||||
var defaultMessage = <?php echo json_encode($default_message); ?>;
|
||||
var thankYouUrl = <?php echo json_encode(home_url('/contact-thank-you/?agent_id=' . $agent_id)); ?>;
|
||||
|
||||
// Populate CF7 hidden fields if the form exists
|
||||
var form = document.querySelector('.wpcf7-form');
|
||||
if (form) {
|
||||
var fields = {
|
||||
'agent-id': agentId,
|
||||
'agent-name': agentName,
|
||||
'agent-email': agentEmail,
|
||||
'default-message': defaultMessage
|
||||
};
|
||||
|
||||
for (var name in fields) {
|
||||
var input = form.querySelector('input[name="' + name + '"]');
|
||||
if (input) {
|
||||
input.value = fields[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the display div with the message
|
||||
var displayDiv = form.querySelector('.readonly-message-display');
|
||||
if (displayDiv) {
|
||||
displayDiv.textContent = defaultMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to thank you page after successful submission
|
||||
document.addEventListener('wpcf7mailsent', function(event) {
|
||||
window.location.href = thankYouUrl;
|
||||
}, false);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Name: Contact Thank You
|
||||
* Template for contact form thank you page
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if we have an agent ID (using agent_id to avoid conflict with agent post type query var)
|
||||
$agent_id = isset($_GET['agent_id']) ? absint($_GET['agent_id']) : 0;
|
||||
$agent = null;
|
||||
$agent_name = '';
|
||||
$agent_permalink = '';
|
||||
|
||||
if ($agent_id) {
|
||||
$agent = get_post($agent_id);
|
||||
if ($agent && $agent->post_type === 'agent' && $agent->post_status === 'publish') {
|
||||
$agent_name = $agent->post_title;
|
||||
$agent_permalink = get_permalink($agent_id);
|
||||
}
|
||||
}
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<main id="primary" class="site-main inquiry-thank-you-page-main">
|
||||
<section class="thank-you-section">
|
||||
<div class="container">
|
||||
<div class="thank-you-content">
|
||||
<!-- Thank You Message -->
|
||||
<div class="thank-you-message">
|
||||
<div class="thank-you-icon">
|
||||
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h2>Message Sent Successfully</h2>
|
||||
<p>
|
||||
Thank you for reaching out<?php if ($agent_name) : ?> to <?php echo esc_html($agent_name); ?><?php endif; ?>.
|
||||
Your message has been received and <?php echo $agent_name ? 'they' : 'our team'; ?> will get back to you as soon as possible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="thank-you-actions">
|
||||
<?php if ($agent_permalink) : ?>
|
||||
<a href="<?php echo esc_url($agent_permalink); ?>" 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="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
Back to Agent Profile
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?php echo esc_url(home_url('/properties/')); ?>" class="btn btn-outline">
|
||||
<svg width="18" height="18" 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"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
Browse Properties
|
||||
</a>
|
||||
<a href="<?php echo esc_url(home_url('/')); ?>" class="btn btn-outline">
|
||||
<svg width="18" height="18" 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>
|
||||
Back to Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -52,7 +52,8 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
<div class="contact-grid">
|
||||
<!-- Contact Form -->
|
||||
<div class="contact-form-wrapper">
|
||||
<h2 class="contact-form-title">Send Us a Message</h2>
|
||||
<?php $form_title = get_field('contact_form_title') ?: 'Send Us a Message'; ?>
|
||||
<h2 class="contact-form-title"><?php echo esc_html($form_title); ?></h2>
|
||||
|
||||
<?php if ($property_inquiry) : ?>
|
||||
<div class="property-inquiry-display">
|
||||
@@ -135,7 +136,8 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
|
||||
<!-- Contact Info -->
|
||||
<div class="contact-info-wrapper">
|
||||
<h2 class="contact-info-title">Contact Information</h2>
|
||||
<?php $info_title = get_field('contact_info_title') ?: 'Contact Information'; ?>
|
||||
<h2 class="contact-info-title"><?php echo esc_html($info_title); ?></h2>
|
||||
|
||||
<div class="contact-info-list">
|
||||
<?php if ($address) : ?>
|
||||
@@ -192,6 +194,7 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php $office_hours = homeproz_get_office_hours(); ?>
|
||||
<div class="contact-info-item">
|
||||
<div class="contact-info-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
@@ -202,8 +205,9 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
<div class="contact-info-content">
|
||||
<h4 class="contact-info-label">Office Hours</h4>
|
||||
<p class="contact-info-value">
|
||||
Monday - Friday: 9:00 AM - 5:00 PM<br>
|
||||
Weekends: By Appointment
|
||||
<?php foreach ($office_hours as $index => $hour_row) : ?>
|
||||
<?php echo esc_html($hour_row['day']); ?>: <?php echo esc_html($hour_row['time']); ?><?php if ($index < count($office_hours) - 1) : ?><br><?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,8 +215,15 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
|
||||
<!-- Map -->
|
||||
<div class="contact-map">
|
||||
<?php
|
||||
$map_embed = homeproz_get_acf_option('map_embed');
|
||||
if ($map_embed) :
|
||||
echo $map_embed;
|
||||
else :
|
||||
// Fallback to address-based embed
|
||||
?>
|
||||
<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"
|
||||
src="https://www.google.com/maps?q=<?php echo urlencode($address); ?>&z=15&output=embed"
|
||||
width="100%"
|
||||
height="375"
|
||||
style="border:0;"
|
||||
@@ -221,6 +232,7 @@ $property_link = isset($_GET['property_url']) ? esc_url(urldecode($_GET['propert
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
title="HomeProz Office Location">
|
||||
</iframe>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Name: Property Inquiry Thank You
|
||||
* Thank you page after property inquiry submission
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get MLS listing key from URL (passed after form submission)
|
||||
$listing_key = isset($_GET['listing']) ? sanitize_text_field($_GET['listing']) : '';
|
||||
|
||||
// Try to get property if listing key provided
|
||||
$property = null;
|
||||
if ($listing_key && function_exists('mls_get_property')) {
|
||||
$property = mls_get_property($listing_key);
|
||||
}
|
||||
|
||||
// Property URL for return link
|
||||
$property_url = $listing_key ? home_url('/properties/?listing=' . $listing_key) : '';
|
||||
$search_url = get_post_type_archive_link('property');
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<?php
|
||||
// Get thank you page content from ACF
|
||||
$ty_hero_title = get_field('thankyou_hero_title') ?: 'Thank You!';
|
||||
$ty_hero_subtitle = get_field('thankyou_hero_subtitle') ?: 'Your inquiry has been submitted';
|
||||
$ty_heading = get_field('thankyou_heading') ?: "We've Received Your Request";
|
||||
$ty_message = get_field('thankyou_message') ?: 'One of our team members will review your inquiry and get back to you shortly. We typically respond within 24 hours during business days.';
|
||||
$ty_property_heading = get_field('thankyou_property_heading') ?: 'Thank you for your interest in:';
|
||||
?>
|
||||
<main id="primary" class="site-main inquiry-thank-you-page-main">
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="archive-hero">
|
||||
<div class="container">
|
||||
<h1 class="archive-hero-title"><?php echo esc_html($ty_hero_title); ?></h1>
|
||||
<p class="archive-hero-subtitle"><?php echo esc_html($ty_hero_subtitle); ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Thank You Content -->
|
||||
<section class="thank-you-section">
|
||||
<div class="container">
|
||||
<div class="thank-you-content">
|
||||
<div class="thank-you-message">
|
||||
<svg class="thank-you-icon" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>
|
||||
<h2><?php echo esc_html($ty_heading); ?></h2>
|
||||
<p><?php echo esc_html($ty_message); ?></p>
|
||||
</div>
|
||||
|
||||
<?php if ($property) : ?>
|
||||
<div class="thank-you-property">
|
||||
<h3><?php echo esc_html($ty_property_heading); ?></h3>
|
||||
<?php
|
||||
set_query_var('minimal_property', $property);
|
||||
get_template_part('template-parts/property/property-card-minimal');
|
||||
?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="thank-you-actions">
|
||||
<?php if ($property_url) : ?>
|
||||
<a href="<?php echo esc_url($property_url); ?>" class="btn btn-primary">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
||||
</svg>
|
||||
Return to Property
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="<?php echo esc_url($search_url); ?>" class="btn btn-outline">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="M21 21l-4.35-4.35"/>
|
||||
</svg>
|
||||
Continue Searching
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
/**
|
||||
* Template Name: Property Inquiry
|
||||
* Template for property information request form
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get MLS listing key from URL
|
||||
$listing_key = isset($_GET['listing']) ? sanitize_text_field($_GET['listing']) : '';
|
||||
|
||||
// Redirect to properties page if no listing specified
|
||||
if (!$listing_key || !function_exists('mls_get_property')) {
|
||||
wp_redirect(get_post_type_archive_link('property'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Fetch property from MLS database
|
||||
$property = mls_get_property($listing_key);
|
||||
|
||||
if (!$property) {
|
||||
wp_redirect(get_post_type_archive_link('property'));
|
||||
exit;
|
||||
}
|
||||
|
||||
// Extract property data
|
||||
$price = $property->list_price;
|
||||
$listing_id = $property->listing_id;
|
||||
|
||||
// Format address
|
||||
$address_parts = array();
|
||||
if ($property->street_number) {
|
||||
$address_parts[] = $property->street_number;
|
||||
}
|
||||
if ($property->street_name) {
|
||||
$address_parts[] = $property->street_name;
|
||||
}
|
||||
if ($property->street_suffix) {
|
||||
$address_parts[] = $property->street_suffix;
|
||||
}
|
||||
if ($property->unit_number) {
|
||||
$address_parts[] = '#' . $property->unit_number;
|
||||
}
|
||||
$street_address = implode(' ', $address_parts);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
// 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 . ").";
|
||||
|
||||
get_header();
|
||||
?>
|
||||
|
||||
<main id="primary" class="site-main property-inquiry-page-main">
|
||||
|
||||
<?php
|
||||
// Get inquiry page content from ACF
|
||||
$hero_title = get_field('inquiry_hero_title') ?: 'Request Property Information';
|
||||
$hero_subtitle = get_field('inquiry_hero_subtitle') ?: 'Get more details about this listing';
|
||||
?>
|
||||
<!-- Hero -->
|
||||
<section class="archive-hero">
|
||||
<div class="container">
|
||||
<h1 class="archive-hero-title"><?php echo esc_html($hero_title); ?></h1>
|
||||
<p class="archive-hero-subtitle"><?php echo esc_html($hero_subtitle); ?></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Inquiry Form Section -->
|
||||
<section class="property-inquiry-section">
|
||||
<div class="container">
|
||||
<div class="property-inquiry-grid">
|
||||
<!-- Form -->
|
||||
<div class="property-inquiry-form-wrapper">
|
||||
<?php
|
||||
// Check for Contact Form 7
|
||||
if (function_exists('wpcf7_contact_form')) {
|
||||
$inquiry_form = get_page_by_title('Property Inquiry Form', OBJECT, 'wpcf7_contact_form');
|
||||
if ($inquiry_form) {
|
||||
echo do_shortcode('[contact-form-7 id="' . $inquiry_form->ID . '" title="Property Inquiry Form"]');
|
||||
} else {
|
||||
// Fallback form
|
||||
?>
|
||||
<form class="property-inquiry-form" action="" method="post">
|
||||
<input type="hidden" name="listing-key" value="<?php echo esc_attr($listing_key); ?>">
|
||||
<input type="hidden" name="listing-id" value="<?php echo esc_attr($listing_id); ?>">
|
||||
<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); ?>">
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="inquiry-name">Your Name <span class="required">*</span></label>
|
||||
<input type="text" id="inquiry-name" name="name" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row form-row-2col">
|
||||
<div class="form-group">
|
||||
<label for="inquiry-email">Email <span class="required">*</span></label>
|
||||
<input type="email" id="inquiry-email" name="email" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inquiry-phone">Phone <span class="required">*</span></label>
|
||||
<input type="tel" id="inquiry-phone" name="phone" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">Send Inquiry</button>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<!-- Property Preview -->
|
||||
<div class="property-inquiry-preview">
|
||||
<?php $preview_title = get_field('inquiry_preview_title') ?: "Property You're Inquiring About"; ?>
|
||||
<h3 class="preview-title"><?php echo esc_html($preview_title); ?></h3>
|
||||
<?php
|
||||
set_query_var('minimal_property', $property);
|
||||
get_template_part('template-parts/property/property-card-minimal');
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var listingKey = <?php echo json_encode($listing_key); ?>;
|
||||
var thankYouUrl = <?php echo json_encode(home_url('/inquiry-thank-you/?listing=' . $listing_key)); ?>;
|
||||
var defaultMessage = <?php echo json_encode($default_message); ?>;
|
||||
|
||||
// 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); ?>,
|
||||
'property-price': <?php echo json_encode(number_format($price)); ?>,
|
||||
'property-url': <?php echo json_encode($property_url); ?>,
|
||||
'default-message': defaultMessage
|
||||
};
|
||||
|
||||
for (var name in fields) {
|
||||
var input = form.querySelector('input[name="' + name + '"]');
|
||||
if (input) {
|
||||
input.value = fields[name];
|
||||
}
|
||||
}
|
||||
|
||||
// Populate the display div with the message
|
||||
var displayDiv = form.querySelector('.readonly-message-display');
|
||||
if (displayDiv) {
|
||||
displayDiv.textContent = defaultMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect to thank you page after successful submission
|
||||
document.addEventListener('wpcf7mailsent', function(event) {
|
||||
window.location.href = thankYouUrl;
|
||||
}, false);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<?php
|
||||
get_footer();
|
||||
@@ -39,6 +39,7 @@ while (have_posts()) :
|
||||
$agent_short_bio = get_field('agent_short_bio', $agent_id);
|
||||
$agent_gallery = get_field('agent_gallery', $agent_id);
|
||||
$agent_social_links = get_field('agent_social_links', $agent_id);
|
||||
$agent_credentials = get_field('agent_credentials', $agent_id);
|
||||
|
||||
// Get featured image
|
||||
$featured_image_id = get_post_thumbnail_id($agent_id);
|
||||
@@ -97,8 +98,19 @@ while (have_posts()) :
|
||||
Email Agent
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo esc_url(home_url('/contact-agent/?agent_id=' . $agent_id)); ?>" 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>
|
||||
Send Message
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ($agent_credentials) : ?>
|
||||
<p class="agent-credentials"><?php echo wp_kses_post($agent_credentials); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Social Links -->
|
||||
<?php if ($agent_social_links && is_array($agent_social_links)) : ?>
|
||||
<div class="agent-social-links">
|
||||
@@ -160,70 +172,15 @@ while (have_posts()) :
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Contact Details (mobile-friendly, above listings) -->
|
||||
<?php if ($agent_phone || $agent_email) : ?>
|
||||
<section class="agent-section agent-contact-details">
|
||||
<h2 class="section-title">Contact <?php the_title(); ?></h2>
|
||||
<div class="contact-details-grid">
|
||||
<?php if ($agent_phone) : ?>
|
||||
<a href="tel:<?php echo esc_attr(preg_replace('/[^0-9]/', '', $agent_phone)); ?>" class="contact-detail-item">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<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>
|
||||
<div class="contact-detail-content">
|
||||
<span class="contact-detail-label">Phone</span>
|
||||
<span class="contact-detail-value"><?php echo esc_html($agent_phone); ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($agent_email) : ?>
|
||||
<a href="mailto:<?php echo esc_attr($agent_email); ?>" class="contact-detail-item">
|
||||
<svg width="24" height="24" 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>
|
||||
<div class="contact-detail-content">
|
||||
<span class="contact-detail-label">Email</span>
|
||||
<span class="contact-detail-value"><?php echo esc_html($agent_email); ?></span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($agent_website) : ?>
|
||||
<a href="<?php echo esc_url($agent_website); ?>" target="_blank" rel="noopener noreferrer" class="contact-detail-item">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>
|
||||
<div class="contact-detail-content">
|
||||
<span class="contact-detail-label">Website</span>
|
||||
<span class="contact-detail-value">Visit Website</span>
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Photo Gallery -->
|
||||
<?php if ($agent_gallery && is_array($agent_gallery) && count($agent_gallery) > 0) : ?>
|
||||
<section class="agent-section agent-gallery-section">
|
||||
<h2 class="section-title">Photo Gallery</h2>
|
||||
<div class="agent-gallery-grid">
|
||||
<?php foreach ($agent_gallery as $image_id) :
|
||||
$image_url = wp_get_attachment_image_url($image_id, 'medium_large');
|
||||
$image_full = wp_get_attachment_image_url($image_id, 'full');
|
||||
$image_alt = get_post_meta($image_id, '_wp_attachment_image_alt', true);
|
||||
?>
|
||||
<a href="<?php echo esc_url($image_full); ?>" class="gallery-item" target="_blank">
|
||||
<img src="<?php echo esc_url($image_url); ?>" alt="<?php echo esc_attr($image_alt ?: get_the_title()); ?>">
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<!-- Image Gallery -->
|
||||
<?php
|
||||
if ($agent_gallery && is_array($agent_gallery) && count($agent_gallery) > 0) {
|
||||
get_template_part('template-parts/agent/agent-gallery', null, array(
|
||||
'gallery' => $agent_gallery,
|
||||
'agent_id' => $agent_id,
|
||||
));
|
||||
}
|
||||
?>
|
||||
|
||||
<!-- Agent's Listings -->
|
||||
<?php
|
||||
@@ -307,10 +264,11 @@ while (have_posts()) :
|
||||
</div>
|
||||
|
||||
<!-- Quick Contact Form Placeholder -->
|
||||
<?php $agent_first_name = explode(' ', get_the_title())[0]; ?>
|
||||
<div class="sidebar-widget agent-quick-contact">
|
||||
<h3 class="widget-title">Send a Message</h3>
|
||||
<p class="widget-note">Interested in working with <?php the_title(); ?>? Get in touch today.</p>
|
||||
<a href="<?php echo esc_url(home_url('/contact/')); ?>?agent=<?php echo esc_attr(get_the_title()); ?>" class="btn btn-primary btn-block">Contact Agent</a>
|
||||
<a href="<?php echo esc_url(home_url('/contact-agent/?agent_id=' . $agent_id)); ?>" class="btn btn-primary btn-block">Contact <?php echo esc_html($agent_first_name); ?></a>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,11 @@ import './main.scss';
|
||||
// Import component JS
|
||||
import '../template-parts/header/navigation.js';
|
||||
import '../template-parts/components/hero-section.js';
|
||||
import '../template-parts/components/hero-location-search.js';
|
||||
import '../template-parts/home/featured-listings.js';
|
||||
import '../template-parts/property/property-filters.js';
|
||||
import '../template-parts/property/property-gallery.js';
|
||||
import '../template-parts/agent/agent-gallery.js';
|
||||
import '../template-parts/content/content-mortgage-calculator.js';
|
||||
|
||||
(function($) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Tasks Directory
|
||||
|
||||
This directory contains task files - structured instructions for performing one-time operations on the codebase by an LLM.
|
||||
|
||||
## What is a Task?
|
||||
|
||||
A task is a detailed specification for a discrete operation that modifies, audits, or enhances the codebase. Tasks are:
|
||||
|
||||
- **One-time operations**: Each task is executed once to completion, then marked done
|
||||
- **Self-contained**: Contains all context needed to understand and execute the work
|
||||
- **Phased**: Broken into clear phases (audit, plan, execute, document)
|
||||
- **Approval-gated**: Requires human approval before destructive or significant changes
|
||||
|
||||
## Task File Structure
|
||||
|
||||
Each task file should include:
|
||||
|
||||
1. **Title**: Clear name describing the operation
|
||||
2. **Objective**: What the task accomplishes
|
||||
3. **Scope**: What areas of the codebase are affected
|
||||
4. **Phases**: Step-by-step breakdown of work
|
||||
5. **Deliverables**: What artifacts are produced
|
||||
6. **Status**: Current state (pending, in-progress, blocked, complete)
|
||||
|
||||
## Task Lifecycle
|
||||
|
||||
```
|
||||
PENDING → IN_PROGRESS → AWAITING_APPROVAL → EXECUTING → DOCUMENTING → COMPLETE
|
||||
```
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Task files use the format: `TASK-{number}-{short-description}.md`
|
||||
|
||||
Example: `TASK-001-cms-field-audit.md`
|
||||
|
||||
## Executing a Task
|
||||
|
||||
When asked to execute a task:
|
||||
|
||||
1. Read the task file completely
|
||||
2. Follow phases in order
|
||||
3. Pause at approval gates
|
||||
4. Update status as work progresses
|
||||
5. Produce all listed deliverables
|
||||
6. Mark complete when finished
|
||||
@@ -0,0 +1,250 @@
|
||||
# TASK-001: CMS Field Audit Report
|
||||
|
||||
**Generated**: 2024-12-29
|
||||
**Status**: COMPLETED
|
||||
|
||||
## Executive Summary
|
||||
|
||||
The HomeProz theme has extensive ACF (Advanced Custom Fields) integration. Most content is already CMS-editable. This audit identified **12 areas with hardcoded content** that should be migrated to CMS fields.
|
||||
|
||||
---
|
||||
|
||||
## Already CMS-Editable (No Changes Needed)
|
||||
|
||||
### Theme Options (Appearance > Theme Options)
|
||||
| Field | Location | Status |
|
||||
|-------|----------|--------|
|
||||
| Phone Number | Header, Footer, Contact | Editable |
|
||||
| Email Address | Footer, Contact | Editable |
|
||||
| Office Address | Footer, Contact | Editable |
|
||||
| Facebook URL | Header, Footer | Editable |
|
||||
| TikTok URL | Header, Footer | Editable |
|
||||
| Instagram URL | (not currently displayed) | Editable |
|
||||
| YouTube URL | (not currently displayed) | Editable |
|
||||
| Footer Tagline | Footer | Editable |
|
||||
| Broker Information | Footer | Editable |
|
||||
| Organization Description | Schema/SEO | Editable |
|
||||
| Office Hours | Footer | Editable |
|
||||
| Office Latitude/Longitude | Schema/SEO | Editable |
|
||||
| Google Maps Embed Code | (defined but not used) | Editable |
|
||||
| Properties Hero Title | Properties Archive | Editable |
|
||||
| Properties Hero Subtitle | Properties Archive | Editable |
|
||||
| Properties Hero Background | Properties Archive | Editable |
|
||||
|
||||
### Homepage (Front Page Editor)
|
||||
| Field | Section | Status |
|
||||
|-------|---------|--------|
|
||||
| Hero Title | Hero | Editable |
|
||||
| Hero Subtitle | Hero | Editable |
|
||||
| Hero Primary CTA Text/URL | Hero | Editable |
|
||||
| Hero Secondary CTA Text/URL | Hero | Editable |
|
||||
| Hero Background Image | Hero | Editable |
|
||||
| Services Section Title | Service Cards | Editable |
|
||||
| Services Section Subtitle | Service Cards | Editable |
|
||||
| Service Cards (repeater) | Service Cards | Editable |
|
||||
| Featured Homes Title/Subtitle | Featured Section | Editable |
|
||||
| Commercial Title/Subtitle | Featured Section | Editable |
|
||||
| Property Types Title/Subtitle | Property Types | Editable |
|
||||
| CTA Title/Text/Button | CTA Section | Editable |
|
||||
|
||||
### Pages (Page Editor - Page Hero Group)
|
||||
All pages except homepage have:
|
||||
| Field | Status |
|
||||
|-------|--------|
|
||||
| Hero Title | Editable |
|
||||
| Hero Subtitle | Editable |
|
||||
| Hero Background Image | Editable |
|
||||
|
||||
### Navigation Menus (Appearance > Menus)
|
||||
| Location | Status |
|
||||
|----------|--------|
|
||||
| Primary Menu | Editable |
|
||||
| Footer Menu | Editable |
|
||||
|
||||
### Custom Post Types
|
||||
| CPT | Fields | Status |
|
||||
|-----|--------|--------|
|
||||
| Property | Price, MLS#, Address, Beds/Baths/SqFt, Features, Gallery, Documents, Agent | All Editable |
|
||||
| Agent | Phone, Email, Title, License, Bio, Gallery, Social Links, Order, Disabled | All Editable |
|
||||
|
||||
### Page Templates (6 templates, all have ACF fields)
|
||||
- Content with Sidebar
|
||||
- Alternating Blocks
|
||||
- Service Detail
|
||||
- Card Grid
|
||||
- Long-Form Article
|
||||
- Landing Page
|
||||
|
||||
---
|
||||
|
||||
## Hardcoded Content (Migration Required)
|
||||
|
||||
### 1. About Page - Team Section Headers
|
||||
**File**: `page-about.php` (lines 63-64)
|
||||
**Current**:
|
||||
```php
|
||||
<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>
|
||||
```
|
||||
**Recommendation**: Add ACF fields `about_team_title` and `about_team_subtitle` to About Page field group
|
||||
**Priority**: Medium
|
||||
|
||||
---
|
||||
|
||||
### 2. About Page - Broker Information Section
|
||||
**File**: `page-about.php` (lines 183-188)
|
||||
**Current**:
|
||||
```php
|
||||
<h3 class="about-broker-title">Broker Information</h3>
|
||||
<p class="about-broker-text">
|
||||
HomeProz Real Estate LLC DBA LandProz Real Estate, LLC<br>
|
||||
111 East Clark Street, Albert Lea, MN 56007<br>
|
||||
Broker Brian Haugen - MN | Broker/Auctioneer Greg Jensen - MN, IA - 24-21
|
||||
</p>
|
||||
```
|
||||
**Recommendation**: Add ACF fields `about_broker_title` and `about_broker_text` (wysiwyg) to About Page field group
|
||||
**Priority**: High (legal/compliance content)
|
||||
|
||||
---
|
||||
|
||||
### 3. About Page - CTA Section
|
||||
**File**: `page-about.php` (lines 195-201)
|
||||
**Current**:
|
||||
```php
|
||||
get_template_part('template-parts/components/cta-section', null, array(
|
||||
'title' => 'Ready to Work With Us?',
|
||||
'text' => 'Contact our team today to start your real estate journey.',
|
||||
'button_text' => 'Get in Touch',
|
||||
'button_url' => home_url('/contact/'),
|
||||
));
|
||||
```
|
||||
**Recommendation**: Add ACF fields `about_cta_title`, `about_cta_text`, `about_cta_button_text`, `about_cta_button_url` to About Page field group
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
### 4. About Page - Featured Image
|
||||
**File**: `page-about.php` (line 45)
|
||||
**Current**:
|
||||
```php
|
||||
<img src="<?php echo esc_url(get_template_directory_uri() . '/assets/images/about-us.webp'); ?>"
|
||||
```
|
||||
**Recommendation**: Add ACF image field `about_featured_image` to About Page field group
|
||||
**Priority**: Medium
|
||||
|
||||
---
|
||||
|
||||
### 5. Contact Page - Section Titles
|
||||
**File**: `page-contact.php` (lines 55, 138)
|
||||
**Current**:
|
||||
```php
|
||||
<h2 class="contact-form-title">Send Us a Message</h2>
|
||||
<h2 class="contact-info-title">Contact Information</h2>
|
||||
```
|
||||
**Recommendation**: Add ACF fields `contact_form_title` and `contact_info_title` to Contact Page field group
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
### 6. Contact Page - Office Hours Display
|
||||
**File**: `page-contact.php` (lines 204-207)
|
||||
**Current**:
|
||||
```php
|
||||
<p class="contact-info-value">
|
||||
Monday - Friday: 9:00 AM - 5:00 PM<br>
|
||||
Weekends: By Appointment
|
||||
</p>
|
||||
```
|
||||
**Recommendation**: Use existing `homeproz_get_office_hours()` function (already in theme options)
|
||||
**Priority**: High (inconsistency - footer uses dynamic hours, contact uses hardcoded)
|
||||
|
||||
---
|
||||
|
||||
### 7. Contact Page - Google Maps Embed
|
||||
**File**: `page-contact.php` (lines 214-223)
|
||||
**Current**: Hardcoded iframe with specific embed URL
|
||||
**Recommendation**: Use existing `theme_map_embed` ACF field from Theme Options (field exists but not used here)
|
||||
**Priority**: High (theme option exists but not utilized)
|
||||
|
||||
---
|
||||
|
||||
### 8. Homepage - Hero Gallery Images
|
||||
**File**: `front-page.php` (lines 69-76)
|
||||
**Current**:
|
||||
```php
|
||||
$gallery_source_images = array(
|
||||
'assets/images/hero-original.png',
|
||||
'assets/images/hero-gallery/hero-2.png',
|
||||
...
|
||||
);
|
||||
```
|
||||
**Recommendation**: Add ACF gallery field `home_hero_gallery` to Homepage Content field group
|
||||
**Priority**: Medium
|
||||
|
||||
---
|
||||
|
||||
### 9. 404 Page - All Content
|
||||
**File**: `404.php` (lines 20-31)
|
||||
**Current**: All text hardcoded
|
||||
**Recommendation**: Add Theme Options fields: `404_title`, `404_subtitle`, `404_message`, `404_primary_button_text`, `404_secondary_button_text`
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
### 10. Property Inquiry Page - Hero Content
|
||||
**File**: `page-property-inquiry.php`
|
||||
**Current**: Hardcoded "Request Property Information" and subtitle
|
||||
**Recommendation**: Add Page Hero fields or specific ACF fields
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
### 11. Thank You Page - Content
|
||||
**File**: `page-inquiry-thank-you.php`
|
||||
**Current**: Hardcoded thank you message and titles
|
||||
**Recommendation**: Add ACF fields for thank you page content
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
### 12. Footer - Credits Line
|
||||
**File**: `site-footer.php` (lines 187-188)
|
||||
**Current**:
|
||||
```php
|
||||
Web Design by <a href="https://hanson.xyz" target="_blank" rel="noopener">HansonXyz</a>
|
||||
```
|
||||
**Recommendation**: Add Theme Options field `footer_credits` (text or wysiwyg)
|
||||
**Priority**: Low
|
||||
|
||||
---
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: High Priority (Fix inconsistencies)
|
||||
1. Contact Page: Use `homeproz_get_office_hours()` for office hours display
|
||||
2. Contact Page: Use `theme_map_embed` for Google Maps
|
||||
3. About Page: Add broker information fields (legal/compliance)
|
||||
|
||||
### Phase 2: Medium Priority (Content flexibility)
|
||||
1. About Page: Add team section title/subtitle fields
|
||||
2. About Page: Add featured image field
|
||||
3. Homepage: Add hero gallery field
|
||||
|
||||
### Phase 3: Low Priority (Nice to have)
|
||||
1. Contact Page: Add form/info section titles
|
||||
2. About Page: Add CTA section fields
|
||||
3. 404 Page: Add customization fields
|
||||
4. Property Inquiry/Thank You: Add content fields
|
||||
5. Footer: Add credits field
|
||||
|
||||
---
|
||||
|
||||
## Approval Required
|
||||
|
||||
Before proceeding to Phase 4 (Migration Execution), please review this report and confirm:
|
||||
|
||||
1. **Approve all migrations** - Proceed with all items listed
|
||||
2. **Approve with modifications** - Specify which items to include/exclude
|
||||
3. **Request changes** - Need more information or different approach
|
||||
|
||||
**Note**: All migrations will preserve existing content as default values. No content will be lost.
|
||||
@@ -0,0 +1,105 @@
|
||||
# TASK-001: CMS Field Migration Complete
|
||||
|
||||
**Completed**: 2024-12-29
|
||||
**Status**: COMPLETE
|
||||
|
||||
## Summary
|
||||
|
||||
All 12 hardcoded content items identified in the audit have been migrated to CMS-editable ACF fields. The theme now allows full content management through the WordPress admin interface.
|
||||
|
||||
## Changes Made
|
||||
|
||||
### High Priority (Fixed Inconsistencies)
|
||||
|
||||
1. **Contact Page Office Hours** - `page-contact.php:195-211`
|
||||
- Changed from hardcoded "Monday - Friday: 9:00 AM - 5:00 PM" to `homeproz_get_office_hours()`
|
||||
- Now uses Theme Options > Office Hours repeater
|
||||
|
||||
2. **Contact Page Google Maps** - `page-contact.php:214-234`
|
||||
- Changed from hardcoded iframe to `homeproz_get_acf_option('map_embed')`
|
||||
- Fallback to address-based embed if theme option empty
|
||||
|
||||
3. **About Page Broker Information** - `page-about.php:187-201`
|
||||
- New ACF fields: `about_broker_title`, `about_broker_text`
|
||||
- Wysiwyg field for HTML formatting
|
||||
|
||||
### Medium Priority (Content Flexibility)
|
||||
|
||||
4. **About Page Team Section** - `page-about.php:66-73`
|
||||
- New ACF fields: `about_team_title`, `about_team_subtitle`
|
||||
|
||||
5. **About Page Featured Image** - `page-about.php:44-50`
|
||||
- New ACF field: `about_featured_image`
|
||||
- Fallback to default `assets/images/about-us.webp`
|
||||
|
||||
6. **Homepage Hero Gallery** - `front-page.php:67-92`
|
||||
- New ACF field: `home_hero_gallery`
|
||||
- Fallback to default theme images if empty
|
||||
|
||||
### Low Priority (Nice to Have)
|
||||
|
||||
7. **Contact Page Section Titles** - `page-contact.php:55-56, 139-140`
|
||||
- New ACF fields: `contact_form_title`, `contact_info_title`
|
||||
|
||||
8. **About Page CTA Section** - `page-about.php:203-216`
|
||||
- New ACF fields: `about_cta_title`, `about_cta_text`, `about_cta_button_text`, `about_cta_button_url`
|
||||
|
||||
9. **404 Page Content** - `404.php:16-48`
|
||||
- New Theme Options: `theme_404_title`, `theme_404_subtitle`, `theme_404_message`, `theme_404_primary_button`, `theme_404_secondary_button`
|
||||
|
||||
10. **Property Inquiry Page** - `page-property-inquiry.php:73-84, 146-147`
|
||||
- New ACF fields: `inquiry_hero_title`, `inquiry_hero_subtitle`, `inquiry_preview_title`
|
||||
|
||||
11. **Thank You Page** - `page-inquiry-thank-you.php:30-63`
|
||||
- New ACF fields: `thankyou_hero_title`, `thankyou_hero_subtitle`, `thankyou_heading`, `thankyou_message`, `thankyou_property_heading`
|
||||
|
||||
12. **Footer Credits** - `template-parts/footer/site-footer.php:187-192`
|
||||
- New Theme Option: `theme_footer_credits`
|
||||
- Supports HTML for links
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Templates
|
||||
- `page-contact.php` - Office hours, map embed, section titles
|
||||
- `page-about.php` - Team section, featured image, broker info, CTA
|
||||
- `front-page.php` - Hero gallery
|
||||
- `404.php` - All content
|
||||
- `page-property-inquiry.php` - Hero and preview titles
|
||||
- `page-inquiry-thank-you.php` - All content
|
||||
- `template-parts/footer/site-footer.php` - Credits
|
||||
|
||||
### ACF Configuration
|
||||
- `inc/acf-fields.php` - Added field groups:
|
||||
- Theme Options: 404 Page tab, Footer Credits tab
|
||||
- Homepage Content: Hero Gallery tab
|
||||
- About Page Content (new group)
|
||||
- Contact Page Content (new group)
|
||||
- Property Inquiry Page Content (new group)
|
||||
- Thank You Page Content (new group)
|
||||
|
||||
## Documentation Updated
|
||||
|
||||
- `man/cms-fields.txt` - Comprehensive CMS fields reference
|
||||
- `tasks/TASK-001-AUDIT-REPORT.md` - Status updated to COMPLETED
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
All new fields should be verified in WordPress admin:
|
||||
|
||||
- [ ] Theme Options > 404 Page - All 5 fields
|
||||
- [ ] Theme Options > Footer Credits - Credits field
|
||||
- [ ] Homepage > Hero Gallery - Gallery uploads and displays
|
||||
- [ ] About Page - All 8 new fields
|
||||
- [ ] Contact Page - Both section title fields
|
||||
- [ ] Contact Page - Office hours displays from theme options
|
||||
- [ ] Contact Page - Map uses theme option or fallback
|
||||
- [ ] Property Inquiry - All 3 fields
|
||||
- [ ] Thank You Page - All 5 fields
|
||||
|
||||
## Notes
|
||||
|
||||
- All fields have sensible defaults matching previous hardcoded values
|
||||
- No content was lost during migration
|
||||
- Fallbacks ensure site functions if fields are empty
|
||||
- HTML is allowed in broker info and footer credits (using wysiwyg/wp_kses_post)
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# TASK-001: CMS Field Audit and Migration
|
||||
|
||||
**Status**: COMPLETE
|
||||
**Created**: 2024-12-29
|
||||
**Last Updated**: 2024-12-29
|
||||
**Completed**: 2024-12-29
|
||||
|
||||
## Objective
|
||||
|
||||
Audit all pages on the HomeProz website to identify content that is hardcoded in templates versus content that is editable via the WordPress CMS (Advanced Custom Fields, theme options, menus, widgets, etc.). Then migrate all hardcoded content to CMS-editable fields.
|
||||
|
||||
## Scope
|
||||
|
||||
- All page templates in the theme
|
||||
- All template parts
|
||||
- Theme options and site-wide settings
|
||||
- Navigation menus
|
||||
- Custom post types and their fields
|
||||
- Any hardcoded text, images, URLs, or configuration
|
||||
|
||||
## Phases
|
||||
|
||||
### Phase 1: Audit (No Changes)
|
||||
|
||||
Systematically review every page template and identify:
|
||||
|
||||
1. **CMS-Editable Content** (already dynamic):
|
||||
- ACF fields on pages
|
||||
- Theme options (phone, email, address, etc.)
|
||||
- WordPress menus
|
||||
- Post/page content via the_content()
|
||||
- Custom post type fields
|
||||
- Widget areas
|
||||
|
||||
2. **Hardcoded Content** (needs migration):
|
||||
- Static text in templates
|
||||
- Hardcoded image paths
|
||||
- Hardcoded URLs
|
||||
- Static configuration values
|
||||
- Inline content that should be editable
|
||||
|
||||
### Phase 2: Report Generation
|
||||
|
||||
Produce `AUDIT-REPORT.md` containing:
|
||||
|
||||
- Page-by-page breakdown
|
||||
- Each content element identified
|
||||
- Current state (editable/hardcoded)
|
||||
- Recommended migration approach for hardcoded items:
|
||||
- ACF field on page
|
||||
- ACF field group on post type
|
||||
- Theme option (site-wide)
|
||||
- Menu location
|
||||
- Widget area
|
||||
- Other
|
||||
|
||||
### Phase 3: Approval Gate
|
||||
|
||||
**STOP HERE** - Present report to user for review and approval before proceeding.
|
||||
|
||||
### Phase 4: Migration Execution
|
||||
|
||||
Upon approval, systematically:
|
||||
|
||||
1. Create necessary ACF field groups
|
||||
2. Create theme option fields if needed
|
||||
3. Update templates to use dynamic values
|
||||
4. Populate fields with current hardcoded values
|
||||
5. Test each page for correctness
|
||||
|
||||
### Phase 5: Documentation
|
||||
|
||||
Produce final documentation:
|
||||
|
||||
1. `MIGRATION-COMPLETE.md` - Summary of all changes made
|
||||
2. Update `man/cms-fields.txt` - Complete reference of all CMS-editable fields
|
||||
3. Mark new fields with `*NEW` designation for testing
|
||||
|
||||
## Deliverables
|
||||
|
||||
| Deliverable | Phase | Description |
|
||||
|-------------|-------|-------------|
|
||||
| `AUDIT-REPORT.md` | 2 | Full audit findings and migration plan |
|
||||
| `MIGRATION-COMPLETE.md` | 5 | Summary of executed changes |
|
||||
| `man/cms-fields.txt` | 5 | Man page documenting all CMS fields |
|
||||
| ACF field groups | 4 | New field configurations in WordPress |
|
||||
| Updated templates | 4 | PHP files modified to use dynamic content |
|
||||
|
||||
## Pages to Audit
|
||||
|
||||
- [ ] Front Page (`front-page.php`)
|
||||
- [ ] Properties Archive (`archive-property.php`)
|
||||
- [ ] Single Property (`single-property.php`, `single-property-mls.php`)
|
||||
- [ ] Agents Archive (`archive-agent.php`)
|
||||
- [ ] Single Agent (`single-agent.php`)
|
||||
- [ ] About Page (`page-about.php`)
|
||||
- [ ] Contact Page (`page-contact.php`)
|
||||
- [ ] Property Inquiry (`page-property-inquiry.php`)
|
||||
- [ ] Inquiry Thank You (`page-inquiry-thank-you.php`)
|
||||
- [ ] Blog Archive (`home.php` or `index.php`)
|
||||
- [ ] Single Post (`single.php`)
|
||||
- [ ] Header (`template-parts/header/`)
|
||||
- [ ] Footer (`template-parts/footer/`)
|
||||
- [ ] 404 Page (`404.php`)
|
||||
- [ ] Search Results (`search.php`)
|
||||
- [ ] Page Templates (`page-templates/`)
|
||||
- [ ] Generic Page (`page.php`)
|
||||
|
||||
## Content Types to Check
|
||||
|
||||
- Hero sections (titles, subtitles, backgrounds, CTAs)
|
||||
- Navigation menus
|
||||
- Contact information
|
||||
- Social media links
|
||||
- Office hours
|
||||
- Taglines and descriptions
|
||||
- Button text and URLs
|
||||
- Image galleries and sliders
|
||||
- Testimonials
|
||||
- Service descriptions
|
||||
- Footer content
|
||||
- Legal/compliance text
|
||||
- Form labels and placeholders
|
||||
- Error messages
|
||||
- Empty state messages
|
||||
|
||||
## Notes
|
||||
|
||||
- Preserve all existing functionality
|
||||
- Maintain backwards compatibility
|
||||
- Use sensible defaults for new fields
|
||||
- Group related fields logically in ACF
|
||||
- Consider user experience when organizing field groups
|
||||
@@ -0,0 +1,262 @@
|
||||
/**
|
||||
* Agent Gallery JavaScript
|
||||
*
|
||||
* Thumbnail slider with lightbox functionality
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
(function($) {
|
||||
'use strict';
|
||||
|
||||
// Early return if not on agent page
|
||||
if (!$('.Single_Agent').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var AgentGallery = {
|
||||
$gallery: null,
|
||||
$thumbsSlider: null,
|
||||
$thumbs: null,
|
||||
$lightbox: null,
|
||||
$lightboxImage: null,
|
||||
$lightboxCounter: null,
|
||||
images: [],
|
||||
currentIndex: 0,
|
||||
isTransitioning: false,
|
||||
slideDuration: 300,
|
||||
|
||||
init: function() {
|
||||
this.$gallery = $('.agent-gallery');
|
||||
|
||||
if (!this.$gallery.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$thumbsSlider = this.$gallery.find('.agent-gallery-thumbs-slider');
|
||||
this.$thumbs = this.$gallery.find('.agent-gallery-thumb');
|
||||
this.$lightbox = this.$gallery.find('.agent-gallery-lightbox');
|
||||
this.$lightboxImage = this.$lightbox.find('.lightbox-image');
|
||||
this.$lightboxCounter = this.$lightbox.find('.lightbox-current');
|
||||
|
||||
// Load images data
|
||||
var $dataScript = this.$gallery.find('.agent-gallery-data');
|
||||
if ($dataScript.length) {
|
||||
try {
|
||||
this.images = JSON.parse($dataScript.text());
|
||||
} catch (e) {
|
||||
console.error('Failed to parse gallery data');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.initSlick();
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
initSlick: function() {
|
||||
// Only init slick if more images than can fit
|
||||
this.$thumbsSlider.slick({
|
||||
slidesToShow: 6,
|
||||
slidesToScroll: 3,
|
||||
arrows: true,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
speed: 300,
|
||||
autoplay: false,
|
||||
variableWidth: false,
|
||||
prevArrow: '<button type="button" class="slick-prev" aria-label="Previous"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg></button>',
|
||||
nextArrow: '<button type="button" class="slick-next" aria-label="Next"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg></button>',
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 768,
|
||||
settings: {
|
||||
slidesToShow: 4,
|
||||
slidesToScroll: 2
|
||||
}
|
||||
},
|
||||
{
|
||||
breakpoint: 480,
|
||||
settings: {
|
||||
slidesToShow: 3,
|
||||
slidesToScroll: 2
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
},
|
||||
|
||||
bindEvents: function() {
|
||||
var self = this;
|
||||
|
||||
// Thumbnail clicks open lightbox
|
||||
this.$thumbs.on('click', function() {
|
||||
var index = parseInt($(this).data('index'));
|
||||
self.openLightbox(index);
|
||||
});
|
||||
|
||||
// Close lightbox - clicking overlay, container, or close button
|
||||
this.$lightbox.find('.lightbox-close, .lightbox-overlay, .lightbox-container').on('click', function(e) {
|
||||
// Only close if clicking directly on these elements, not their children (except close btn)
|
||||
if (e.target === this || $(this).hasClass('lightbox-close')) {
|
||||
self.closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
// Lightbox navigation
|
||||
this.$lightbox.find('.lightbox-prev').on('click', function() {
|
||||
self.slideLightboxImage('prev');
|
||||
});
|
||||
|
||||
this.$lightbox.find('.lightbox-next').on('click', function() {
|
||||
self.slideLightboxImage('next');
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
$(document).on('keydown', function(e) {
|
||||
if (!self.$lightbox.is('[aria-hidden="false"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.key) {
|
||||
case 'Escape':
|
||||
self.closeLightbox();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
self.slideLightboxImage('prev');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
self.slideLightboxImage('next');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// Swipe support for lightbox
|
||||
this.bindSwipeEvents();
|
||||
},
|
||||
|
||||
bindSwipeEvents: function() {
|
||||
var self = this;
|
||||
var startX = 0;
|
||||
var threshold = 50;
|
||||
|
||||
var $container = this.$lightbox.find('.lightbox-image-container');
|
||||
|
||||
$container[0].addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 1) {
|
||||
startX = e.touches[0].clientX;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
$container[0].addEventListener('touchend', function(e) {
|
||||
if (e.changedTouches.length !== 1) return;
|
||||
|
||||
var deltaX = e.changedTouches[0].clientX - startX;
|
||||
|
||||
if (Math.abs(deltaX) > threshold) {
|
||||
if (deltaX > 0) {
|
||||
self.slideLightboxImage('prev');
|
||||
} else {
|
||||
self.slideLightboxImage('next');
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
},
|
||||
|
||||
openLightbox: function(index) {
|
||||
this.currentIndex = index;
|
||||
this.updateLightboxImage();
|
||||
this.$lightbox.attr('aria-hidden', 'false');
|
||||
$('body').addClass('lightbox-open');
|
||||
},
|
||||
|
||||
closeLightbox: function() {
|
||||
this.$lightbox.attr('aria-hidden', 'true');
|
||||
$('body').removeClass('lightbox-open');
|
||||
},
|
||||
|
||||
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%';
|
||||
|
||||
var $container = this.$lightbox.find('.lightbox-image-container');
|
||||
|
||||
// Create new image
|
||||
var $newImage = $('<img class="lightbox-slide-image" />');
|
||||
$newImage.attr('src', image.url);
|
||||
$newImage.attr('alt', image.alt || 'Gallery 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)'
|
||||
});
|
||||
|
||||
$container.css({
|
||||
'position': 'relative',
|
||||
'overflow': 'hidden'
|
||||
});
|
||||
|
||||
$container.append($newImage);
|
||||
|
||||
this.$lightboxImage.css('transition', 'transform ' + this.slideDuration + 'ms ease-out');
|
||||
$newImage.css('transition', 'transform ' + this.slideDuration + 'ms ease-out');
|
||||
|
||||
// Trigger reflow
|
||||
$newImage[0].offsetHeight;
|
||||
|
||||
this.$lightboxImage.css('transform', 'translateX(' + slideTo + ')');
|
||||
$newImage.css('transform', 'translateX(0)');
|
||||
|
||||
setTimeout(function() {
|
||||
self.$lightboxImage.attr('src', image.url);
|
||||
self.$lightboxImage.attr('alt', image.alt || 'Gallery photo');
|
||||
self.$lightboxImage.css({ 'transition': '', 'transform': '' });
|
||||
$newImage.remove();
|
||||
self.isTransitioning = false;
|
||||
self.$lightboxCounter.text(newIndex + 1);
|
||||
}, this.slideDuration);
|
||||
|
||||
this.currentIndex = newIndex;
|
||||
},
|
||||
|
||||
updateLightboxImage: function() {
|
||||
var image = this.images[this.currentIndex];
|
||||
this.$lightboxImage.attr('src', image.url);
|
||||
this.$lightboxImage.attr('alt', image.alt || 'Gallery photo');
|
||||
this.$lightboxCounter.text(this.currentIndex + 1);
|
||||
}
|
||||
};
|
||||
|
||||
$(function() {
|
||||
AgentGallery.init();
|
||||
});
|
||||
|
||||
})(jQuery);
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
/**
|
||||
* Agent Gallery Template Part
|
||||
*
|
||||
* Thumbnail slider with lightbox
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$gallery = isset($args['gallery']) ? $args['gallery'] : array();
|
||||
$agent_id = isset($args['agent_id']) ? $args['agent_id'] : get_the_ID();
|
||||
|
||||
// Build images array
|
||||
$images = array();
|
||||
|
||||
if ($gallery && is_array($gallery)) {
|
||||
foreach ($gallery as $attachment_id) {
|
||||
$images[] = array(
|
||||
'id' => $attachment_id,
|
||||
'thumb' => wp_get_attachment_image_url($attachment_id, 'thumbnail'),
|
||||
'full' => wp_get_attachment_image_url($attachment_id, 'full'),
|
||||
'alt' => get_post_meta($attachment_id, '_wp_attachment_image_alt', true),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$image_count = count($images);
|
||||
|
||||
if ($image_count === 0) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="agent-section agent-gallery-section">
|
||||
<h2 class="section-title">Image Gallery</h2>
|
||||
|
||||
<div class="agent-gallery" data-gallery-count="<?php echo esc_attr($image_count); ?>">
|
||||
<!-- Thumbnail Slider -->
|
||||
<div class="agent-gallery-thumbs-slider">
|
||||
<?php foreach ($images as $index => $image) : ?>
|
||||
<button
|
||||
type="button"
|
||||
class="agent-gallery-thumb"
|
||||
data-index="<?php echo esc_attr($index); ?>"
|
||||
>
|
||||
<img src="<?php echo esc_url($image['thumb']); ?>" alt="<?php echo esc_attr($image['alt'] ?: 'Gallery photo'); ?>" />
|
||||
</button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<!-- Lightbox -->
|
||||
<div class="agent-gallery-lightbox" aria-hidden="true">
|
||||
<div class="lightbox-overlay"></div>
|
||||
<div class="lightbox-container">
|
||||
<button class="lightbox-close" type="button" aria-label="Close gallery">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<?php if ($image_count > 1) : ?>
|
||||
<button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous image">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="lightbox-image-container">
|
||||
<img class="lightbox-image" src="" alt="" />
|
||||
</div>
|
||||
|
||||
<?php if ($image_count > 1) : ?>
|
||||
<button class="lightbox-nav lightbox-next" type="button" aria-label="Next image">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="lightbox-counter">
|
||||
<span class="lightbox-current">1</span> / <span class="lightbox-total"><?php echo esc_html($image_count); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Store image data for JS -->
|
||||
<script type="application/json" class="agent-gallery-data">
|
||||
<?php echo wp_json_encode(array_map(function($img) {
|
||||
return array(
|
||||
'url' => $img['full'],
|
||||
'alt' => $img['alt'],
|
||||
);
|
||||
}, $images)); ?>
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -35,6 +35,21 @@
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
// Show profile button hover state when card is hovered
|
||||
// but not when phone/email buttons are being hovered
|
||||
&:hover:not(:has(.agent-action-btn:not(.agent-action-profile):hover)) {
|
||||
.agent-action-profile {
|
||||
background-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agent-card-link {
|
||||
|
||||
@@ -105,6 +105,14 @@
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.agent-credentials {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// Contact Actions
|
||||
.agent-contact-actions {
|
||||
display: flex;
|
||||
@@ -194,15 +202,15 @@
|
||||
|
||||
.contact-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
@media (max-width: 1400px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (min-width: 900px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,4 +431,186 @@
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Agent Gallery - Thumbnail Slider
|
||||
.agent-gallery {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.agent-gallery-thumbs-slider {
|
||||
.slick-track {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.slick-slide {
|
||||
outline: none;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
// Slick arrows
|
||||
.slick-prev,
|
||||
.slick-next {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background-color: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 50%;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-accent);
|
||||
border-color: var(--color-accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
&.slick-disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.slick-prev {
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
.slick-next {
|
||||
right: -16px;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-gallery-thumb {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
padding: 0;
|
||||
border: 2px solid transparent;
|
||||
background: var(--color-bg-card);
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.2s ease, transform 0.2s ease;
|
||||
|
||||
@media (min-width: 1000px) {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
// Agent Gallery Lightbox (reuses property lightbox styles)
|
||||
.agent-gallery-lightbox {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 1000;
|
||||
display: none;
|
||||
|
||||
&[aria-hidden="false"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.lightbox-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-color: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
.lightbox-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 10;
|
||||
padding: 0.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-nav {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
padding: 1rem;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
&.lightbox-prev {
|
||||
left: 1rem;
|
||||
}
|
||||
|
||||
&.lightbox-next {
|
||||
right: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-image-container {
|
||||
max-width: 90vw;
|
||||
max-height: calc(100vh - 8rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lightbox-image {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 8rem);
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.lightbox-counter {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
border-radius: 0.25rem;
|
||||
color: white;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Shows inline ghost text suggestion as user types.
|
||||
* Tab to accept, auto-fills on blur or Enter.
|
||||
* Submits city or zip params to the properties page.
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
@@ -38,8 +39,8 @@
|
||||
*/
|
||||
function initForm($form) {
|
||||
var $input = $form.find('.hero-location-input');
|
||||
var $latInput = $form.find('input[name="lat"]');
|
||||
var $lngInput = $form.find('input[name="lng"]');
|
||||
var $cityInput = $form.find('input[name="city"]');
|
||||
var $zipInput = $form.find('input[name="zip"]');
|
||||
var $geoBtn = $form.find('.hero-geolocation-btn');
|
||||
|
||||
// Create ghost text element
|
||||
@@ -110,8 +111,15 @@
|
||||
function acceptSuggestion() {
|
||||
if (currentSuggestion) {
|
||||
$input.val(currentSuggestion.label);
|
||||
$latInput.val(currentSuggestion.lat);
|
||||
$lngInput.val(currentSuggestion.lng);
|
||||
// Set city or zip based on type
|
||||
// For cities, use full "City, SS" format
|
||||
if (currentSuggestion.type === 'city') {
|
||||
$cityInput.val(currentSuggestion.label);
|
||||
$zipInput.val('');
|
||||
} else {
|
||||
$zipInput.val(currentSuggestion.value);
|
||||
$cityInput.val('');
|
||||
}
|
||||
$ghost.empty().hide();
|
||||
currentSuggestion = null;
|
||||
return true;
|
||||
@@ -125,8 +133,8 @@
|
||||
function resolveInput() {
|
||||
var query = $input.val().trim();
|
||||
|
||||
// Already have coordinates set
|
||||
if ($latInput.val() && $lngInput.val()) {
|
||||
// Already have city or zip set
|
||||
if ($cityInput.val() || $zipInput.val()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -134,16 +142,26 @@
|
||||
var exact = findExactMatch(query);
|
||||
if (exact) {
|
||||
$input.val(exact.label);
|
||||
$latInput.val(exact.lat);
|
||||
$lngInput.val(exact.lng);
|
||||
if (exact.type === 'city') {
|
||||
$cityInput.val(exact.label);
|
||||
$zipInput.val('');
|
||||
} else {
|
||||
$zipInput.val(exact.value);
|
||||
$cityInput.val('');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have a ghost suggestion that starts with what user typed, use it
|
||||
if (currentSuggestion) {
|
||||
$input.val(currentSuggestion.label);
|
||||
$latInput.val(currentSuggestion.lat);
|
||||
$lngInput.val(currentSuggestion.lng);
|
||||
if (currentSuggestion.type === 'city') {
|
||||
$cityInput.val(currentSuggestion.label);
|
||||
$zipInput.val('');
|
||||
} else {
|
||||
$zipInput.val(currentSuggestion.value);
|
||||
$cityInput.val('');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -151,8 +169,13 @@
|
||||
var suggestion = findSuggestion(query);
|
||||
if (suggestion) {
|
||||
$input.val(suggestion.label);
|
||||
$latInput.val(suggestion.lat);
|
||||
$lngInput.val(suggestion.lng);
|
||||
if (suggestion.type === 'city') {
|
||||
$cityInput.val(suggestion.label);
|
||||
$zipInput.val('');
|
||||
} else {
|
||||
$zipInput.val(suggestion.value);
|
||||
$cityInput.val('');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -186,9 +209,9 @@
|
||||
|
||||
// Input event - update ghost text
|
||||
$input.on('input', function() {
|
||||
// Clear coordinates when user types
|
||||
$latInput.val('');
|
||||
$lngInput.val('');
|
||||
// Clear city/zip when user types
|
||||
$cityInput.val('');
|
||||
$zipInput.val('');
|
||||
updateGhost();
|
||||
});
|
||||
|
||||
@@ -229,9 +252,9 @@
|
||||
// If no location resolved and input is empty, allow through without location filter
|
||||
var query = $input.val().trim();
|
||||
if (!resolved && query === '') {
|
||||
$latInput.prop('disabled', true);
|
||||
$lngInput.prop('disabled', true);
|
||||
$form.find('input[name="radius"]').prop('disabled', true);
|
||||
// Disable empty hidden fields so they don't appear in URL
|
||||
$cityInput.prop('disabled', true);
|
||||
$zipInput.prop('disabled', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -244,12 +267,17 @@
|
||||
}, 1000);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable unused field to keep URL clean
|
||||
if ($cityInput.val()) {
|
||||
$zipInput.prop('disabled', true);
|
||||
} else if ($zipInput.val()) {
|
||||
$cityInput.prop('disabled', true);
|
||||
}
|
||||
});
|
||||
|
||||
// Geolocation button
|
||||
$geoBtn.on('click', function() {
|
||||
handleGeolocation($form, $input, $latInput, $lngInput, $(this), $ghost);
|
||||
});
|
||||
// Geolocation button - hide for now (lat/lng not supported yet)
|
||||
$geoBtn.hide();
|
||||
|
||||
// Window resize - reposition ghost
|
||||
$(window).on('resize', function() {
|
||||
|
||||
@@ -56,9 +56,8 @@ $style = $background_image ? 'background-image: url(' . esc_url($background_imag
|
||||
>
|
||||
<div class="hero-location-dropdown" id="hero-location-dropdown-mobile"></div>
|
||||
</div>
|
||||
<input type="hidden" name="lat" id="hero-location-lat-mobile">
|
||||
<input type="hidden" name="lng" id="hero-location-lng-mobile">
|
||||
<input type="hidden" name="radius" value="30">
|
||||
<input type="hidden" name="city" id="hero-location-city-mobile">
|
||||
<input type="hidden" name="zip" id="hero-location-zip-mobile">
|
||||
<button type="button" class="btn btn-icon hero-geolocation-btn" id="hero-geolocation-btn-mobile" title="Use my location">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -77,12 +76,21 @@ $style = $background_image ? 'background-image: url(' . esc_url($background_imag
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($primary_cta_text || $secondary_cta_text) : ?>
|
||||
<?php if ($primary_cta_text) : ?>
|
||||
<div class="hero-section-actions">
|
||||
<a href="<?php echo esc_url(home_url('/properties/?near_me=1')); ?>" class="btn btn-primary btn-sm hero-nearme-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
Properties Near Me
|
||||
</a>
|
||||
|
||||
<?php if ($primary_cta_text && $primary_cta_url) : ?>
|
||||
<a href="<?php echo esc_url($primary_cta_url); ?>" class="btn btn-primary">
|
||||
<a href="<?php echo esc_url($primary_cta_url); ?>" class="btn btn-primary btn-sm">
|
||||
<?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">
|
||||
<svg width="16" height="16" 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"/>
|
||||
@@ -91,12 +99,6 @@ $style = $background_image ? 'background-image: url(' . esc_url($background_imag
|
||||
<?php echo esc_html($primary_cta_text); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($secondary_cta_text && $secondary_cta_url) : ?>
|
||||
<a href="<?php echo esc_url($secondary_cta_url); ?>" class="btn btn-secondary">
|
||||
<?php echo esc_html($secondary_cta_text); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -145,11 +145,6 @@
|
||||
flex-wrap: wrap;
|
||||
gap: 0.875rem;
|
||||
justify-content: center;
|
||||
|
||||
.btn {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.675rem 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-section--card .hero-location-search {
|
||||
|
||||
@@ -74,9 +74,8 @@ $gallery_data = !empty($gallery_images) ? esc_attr(wp_json_encode($gallery_image
|
||||
>
|
||||
<div class="hero-location-dropdown" id="hero-location-dropdown"></div>
|
||||
</div>
|
||||
<input type="hidden" name="lat" id="hero-location-lat">
|
||||
<input type="hidden" name="lng" id="hero-location-lng">
|
||||
<input type="hidden" name="radius" value="30">
|
||||
<input type="hidden" name="city" id="hero-location-city">
|
||||
<input type="hidden" name="zip" id="hero-location-zip">
|
||||
<button type="button" class="btn btn-icon hero-geolocation-btn" id="hero-geolocation-btn" title="Use my location">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
@@ -95,29 +94,33 @@ $gallery_data = !empty($gallery_images) ? esc_attr(wp_json_encode($gallery_image
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($primary_cta_text || $secondary_cta_text) : ?>
|
||||
<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; ?>
|
||||
|
||||
<?php if ($secondary_cta_text && $secondary_cta_url) : ?>
|
||||
<a href="<?php echo esc_url($secondary_cta_url); ?>" class="btn btn-secondary">
|
||||
<?php echo esc_html($secondary_cta_text); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if ($primary_cta_text) : ?>
|
||||
<div class="hero-section-actions">
|
||||
<a href="<?php echo esc_url(home_url('/properties/?near_me=1')); ?>" class="btn btn-primary hero-nearme-btn">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M12 2v3M12 19v3M2 12h3M19 12h3"/>
|
||||
<circle cx="12" cy="12" r="8"/>
|
||||
</svg>
|
||||
Properties Near Me
|
||||
</a>
|
||||
|
||||
<?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; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="hero-split-image"
|
||||
<?php echo $background_image ? 'style="background-image: url(' . esc_url($background_image) . ');"' : ''; ?>
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
max-width: 800px;
|
||||
background-color: var(--color-bg-dark);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
|
||||
@@ -12,37 +12,47 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Service card data
|
||||
$services = array(
|
||||
array(
|
||||
'icon' => 'home',
|
||||
'title' => 'Find a Home',
|
||||
'description' => 'Browse our selection of homes for sale in Albert Lea and surrounding Minnesota communities. Your dream home is waiting.',
|
||||
'button_text' => 'Search Homes',
|
||||
'button_url' => home_url('/properties/?type=residential'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'sell',
|
||||
'title' => 'Sell Your Home',
|
||||
'description' => 'Ready to sell? Our experienced agents will help you get the best price for your property in today\'s market.',
|
||||
'button_text' => 'Get Started',
|
||||
'button_url' => home_url('/contact/?subject=selling'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'book',
|
||||
'title' => 'Resources & Guides',
|
||||
'description' => 'First-time buyer? Preparing to sell? Our guides walk you through every step of the real estate process.',
|
||||
'button_text' => 'Learn More',
|
||||
'button_url' => home_url('/resources/'),
|
||||
),
|
||||
);
|
||||
// Get section title/subtitle from ACF (on homepage)
|
||||
$section_title = get_field('home_services_title') ?: 'Go With The Proz';
|
||||
$section_subtitle = get_field('home_services_subtitle') ?: 'Whether you\'re buying or selling, we\'re here to guide you every step of the way.';
|
||||
|
||||
// Get service cards from ACF or use defaults
|
||||
$acf_services = get_field('home_service_cards');
|
||||
if ($acf_services && is_array($acf_services) && count($acf_services) > 0) {
|
||||
$services = $acf_services;
|
||||
} else {
|
||||
// Default service card data
|
||||
$services = array(
|
||||
array(
|
||||
'icon' => 'home',
|
||||
'title' => 'Find a Home',
|
||||
'description' => 'Browse our selection of homes for sale in Albert Lea and surrounding Minnesota communities. Your dream home is waiting.',
|
||||
'button_text' => 'Search Homes',
|
||||
'button_url' => home_url('/properties/?type=residential'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'sell',
|
||||
'title' => 'Sell Your Home',
|
||||
'description' => 'Ready to sell? Our experienced agents will help you get the best price for your property in today\'s market.',
|
||||
'button_text' => 'Get Started',
|
||||
'button_url' => home_url('/contact/?subject=selling'),
|
||||
),
|
||||
array(
|
||||
'icon' => 'book',
|
||||
'title' => 'Resources & Guides',
|
||||
'description' => 'First-time buyer? Preparing to sell? Our guides walk you through every step of the real estate process.',
|
||||
'button_text' => 'Learn More',
|
||||
'button_url' => home_url('/resources/'),
|
||||
),
|
||||
);
|
||||
}
|
||||
?>
|
||||
|
||||
<section class="service-cards-section">
|
||||
<div class="container">
|
||||
<header class="service-cards-header">
|
||||
<h2 class="service-cards-title">Go With The Proz</h2>
|
||||
<p class="service-cards-subtitle">Whether you're buying or selling, we're here to guide you every step of the way.</p>
|
||||
<h2 class="service-cards-title"><?php echo esc_html($section_title); ?></h2>
|
||||
<p class="service-cards-subtitle"><?php echo esc_html($section_subtitle); ?></p>
|
||||
</header>
|
||||
|
||||
<div class="service-cards-grid">
|
||||
@@ -50,28 +60,93 @@ $services = array(
|
||||
<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">
|
||||
<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>
|
||||
<?php elseif ($service['icon'] === 'key') : ?>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>
|
||||
<?php elseif ($service['icon'] === 'sell') : ?>
|
||||
<svg width="48" height="48" 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"/>
|
||||
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
||||
<line x1="12" y1="11" x2="12" y2="17"/>
|
||||
<line x1="9" y1="14" x2="15" y2="14"/>
|
||||
</svg>
|
||||
<?php elseif ($service['icon'] === 'book') : ?>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
|
||||
</svg>
|
||||
<?php endif; ?>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<?php
|
||||
$icon = $service['icon'] ?? 'home';
|
||||
switch ($icon) {
|
||||
case 'home':
|
||||
echo '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>';
|
||||
echo '<polyline points="9 22 9 12 15 12 15 22"/>';
|
||||
break;
|
||||
case 'key':
|
||||
echo '<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>';
|
||||
break;
|
||||
case 'sell':
|
||||
echo '<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>';
|
||||
echo '<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>';
|
||||
echo '<line x1="12" y1="11" x2="12" y2="17"/>';
|
||||
echo '<line x1="9" y1="14" x2="15" y2="14"/>';
|
||||
break;
|
||||
case 'book':
|
||||
echo '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>';
|
||||
echo '<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>';
|
||||
break;
|
||||
case 'dollar-sign':
|
||||
echo '<line x1="12" y1="1" x2="12" y2="23"/>';
|
||||
echo '<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>';
|
||||
break;
|
||||
case 'users':
|
||||
echo '<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>';
|
||||
echo '<circle cx="9" cy="7" r="4"/>';
|
||||
echo '<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>';
|
||||
echo '<path d="M16 3.13a4 4 0 0 1 0 7.75"/>';
|
||||
break;
|
||||
case 'map-pin':
|
||||
echo '<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>';
|
||||
echo '<circle cx="12" cy="10" r="3"/>';
|
||||
break;
|
||||
case 'building':
|
||||
echo '<rect x="4" y="2" width="16" height="20" rx="2" ry="2"/>';
|
||||
echo '<path d="M9 22v-4h6v4"/>';
|
||||
echo '<line x1="8" y1="6" x2="8" y2="6.01"/>';
|
||||
echo '<line x1="16" y1="6" x2="16" y2="6.01"/>';
|
||||
echo '<line x1="12" y1="6" x2="12" y2="6.01"/>';
|
||||
echo '<line x1="8" y1="10" x2="8" y2="10.01"/>';
|
||||
echo '<line x1="16" y1="10" x2="16" y2="10.01"/>';
|
||||
echo '<line x1="12" y1="10" x2="12" y2="10.01"/>';
|
||||
echo '<line x1="8" y1="14" x2="8" y2="14.01"/>';
|
||||
echo '<line x1="16" y1="14" x2="16" y2="14.01"/>';
|
||||
echo '<line x1="12" y1="14" x2="12" y2="14.01"/>';
|
||||
break;
|
||||
case 'search':
|
||||
echo '<circle cx="11" cy="11" r="8"/>';
|
||||
echo '<line x1="21" y1="21" x2="16.65" y2="16.65"/>';
|
||||
break;
|
||||
case 'handshake':
|
||||
echo '<path d="M20.42 4.58a5.4 5.4 0 0 0-7.65 0l-.77.78-.77-.78a5.4 5.4 0 0 0-7.65 0C1.46 6.7 1.33 10.28 4 13l8 8 8-8c2.67-2.72 2.54-6.3.42-8.42z"/>';
|
||||
break;
|
||||
case 'phone':
|
||||
echo '<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"/>';
|
||||
break;
|
||||
case 'mail':
|
||||
echo '<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"/>';
|
||||
echo '<polyline points="22,6 12,13 2,6"/>';
|
||||
break;
|
||||
case 'star':
|
||||
echo '<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>';
|
||||
break;
|
||||
case 'shield':
|
||||
echo '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>';
|
||||
break;
|
||||
case 'clipboard':
|
||||
echo '<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"/>';
|
||||
echo '<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>';
|
||||
echo '<line x1="12" y1="11" x2="12" y2="17"/>';
|
||||
echo '<line x1="9" y1="14" x2="15" y2="14"/>';
|
||||
break;
|
||||
case 'document':
|
||||
echo '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>';
|
||||
echo '<polyline points="14 2 14 8 20 8"/>';
|
||||
echo '<line x1="16" y1="13" x2="8" y2="13"/>';
|
||||
echo '<line x1="16" y1="17" x2="8" y2="17"/>';
|
||||
break;
|
||||
default:
|
||||
// Fallback to home icon
|
||||
echo '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>';
|
||||
echo '<polyline points="9 22 9 12 15 12 15 22"/>';
|
||||
}
|
||||
?>
|
||||
</svg>
|
||||
</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>
|
||||
|
||||
@@ -0,0 +1,547 @@
|
||||
/**
|
||||
* Property Inquiry and Thank You Page Styles
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// ============================================
|
||||
// Property Inquiry Page
|
||||
// ============================================
|
||||
|
||||
.property-inquiry-page-main {
|
||||
.property-inquiry-section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.property-inquiry-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.property-inquiry-form-wrapper {
|
||||
background-color: var(--color-bg-card);
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.property-inquiry-preview {
|
||||
.preview-title {
|
||||
font-family: 'Inter', 'Droid Sans', Arial, sans-serif;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Form styles
|
||||
.wpcf7-form,
|
||||
.property-inquiry-form {
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row-2col {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="tel"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
textarea.readonly-message,
|
||||
textarea[readonly],
|
||||
.readonly-message-display {
|
||||
background-color: rgba(159, 55, 48, 0.1);
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-text);
|
||||
cursor: default;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Thank You Page
|
||||
// ============================================
|
||||
|
||||
.inquiry-thank-you-page-main {
|
||||
.thank-you-section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.thank-you-content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.thank-you-message {
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
.thank-you-icon {
|
||||
color: var(--color-success);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.75rem;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.thank-you-property {
|
||||
margin-bottom: 2.5rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.property-card-minimal {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.thank-you-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--color-border);
|
||||
color: var(--color-text);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Minimal Property Card
|
||||
// ============================================
|
||||
|
||||
.property-card-minimal {
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
.property-card-minimal-link {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
.property-card-minimal-image img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.property-card-minimal-image {
|
||||
position: relative;
|
||||
width: 140px;
|
||||
min-width: 140px;
|
||||
height: 120px;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-dark);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
font-size: 0.625rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.property-card-minimal-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.property-card-minimal-content {
|
||||
padding: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.property-card-minimal-price {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.property-card-minimal-address {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.street {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.city-state {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.property-card-minimal-meta {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.property-card-minimal-mls {
|
||||
font-size: 0.625rem;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
// Badge styles (shared)
|
||||
.badge-active {
|
||||
background-color: var(--color-success);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: var(--color-warning);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.badge-sold {
|
||||
background-color: var(--color-sold);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Contact Agent Page
|
||||
// ============================================
|
||||
|
||||
.contact-agent-page-main {
|
||||
.contact-agent-section {
|
||||
padding: 3rem 0;
|
||||
}
|
||||
|
||||
.contact-agent-grid {
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
grid-template-columns: 1fr 360px;
|
||||
gap: 3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.contact-agent-form-wrapper {
|
||||
background-color: var(--color-bg-card);
|
||||
padding: 2rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.contact-agent-preview {
|
||||
.preview-title {
|
||||
font-family: 'Inter', 'Droid Sans', Arial, sans-serif;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-preview-card {
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--color-border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-preview-photo {
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
background-color: var(--color-bg-dark);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-preview-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.agent-preview-info {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.agent-preview-title {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.agent-preview-name {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.375rem;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.agent-preview-contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.agent-preview-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
|
||||
svg {
|
||||
color: var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent-light);
|
||||
}
|
||||
}
|
||||
|
||||
.agent-preview-profile-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-accent-hover);
|
||||
background-color: rgba(159, 55, 48, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
// Form styles - inherit from property inquiry
|
||||
.wpcf7-form,
|
||||
.contact-agent-form {
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-row-2col {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"],
|
||||
input[type="tel"],
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--color-bg-dark);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-text);
|
||||
font-size: 1rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.readonly-message-display {
|
||||
background-color: rgba(159, 55, 48, 0.1);
|
||||
border: 1px solid var(--color-accent);
|
||||
border-radius: 0.375rem;
|
||||
color: var(--color-text);
|
||||
cursor: default;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
width: 100%;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,15 +16,12 @@ $address = homeproz_get_option('address');
|
||||
$facebook = homeproz_get_option('facebook');
|
||||
$tiktok = homeproz_get_option('tiktok');
|
||||
|
||||
// Office hours - can be moved to theme options later
|
||||
$office_hours = array(
|
||||
'Mon-Fri' => '9:00am - 4:00pm',
|
||||
'Saturday' => 'By Appointment',
|
||||
'Sunday' => 'By Appointment',
|
||||
);
|
||||
// Footer content from Theme Options
|
||||
$footer_tagline = homeproz_get_acf_option('footer_tagline');
|
||||
$broker_info = homeproz_get_acf_option('broker_info');
|
||||
|
||||
// Broker info
|
||||
$broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
|
||||
// Office hours from Theme Options
|
||||
$office_hours = homeproz_get_office_hours();
|
||||
?>
|
||||
|
||||
<footer id="colophon" class="site-footer">
|
||||
@@ -39,7 +36,7 @@ $broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
|
||||
<span class="site-title"><?php bloginfo('name'); ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<p class="footer-tagline">Your trusted partner in Minnesota and Iowa real estate. Finding homes, building futures.</p>
|
||||
<p class="footer-tagline"><?php echo esc_html($footer_tagline); ?></p>
|
||||
|
||||
<?php if ($facebook || $tiktok) : ?>
|
||||
<div class="footer-social">
|
||||
@@ -70,7 +67,7 @@ $broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
|
||||
'menu_class' => 'footer-menu',
|
||||
'container' => false,
|
||||
'depth' => 1,
|
||||
'fallback_cb' => 'homeproz_footer_fallback_menu',
|
||||
'fallback_cb' => false,
|
||||
));
|
||||
?>
|
||||
</div>
|
||||
@@ -119,10 +116,10 @@ $broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
|
||||
<div class="footer-column footer-hours">
|
||||
<h4 class="footer-heading">Office Hours</h4>
|
||||
<ul class="hours-list">
|
||||
<?php foreach ($office_hours as $day => $hours) : ?>
|
||||
<?php foreach ($office_hours as $hour_row) : ?>
|
||||
<li class="hours-item">
|
||||
<span class="hours-day"><?php echo esc_html($day); ?></span>
|
||||
<span class="hours-time"><?php echo esc_html($hours); ?></span>
|
||||
<span class="hours-day"><?php echo esc_html($hour_row['day']); ?></span>
|
||||
<span class="hours-time"><?php echo esc_html($hour_row['time']); ?></span>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
@@ -131,58 +128,70 @@ $broker_info = 'HomeProz Real Estate LLC DBA LandProz Real Estate, LLC';
|
||||
|
||||
<!-- Legal & Compliance Section -->
|
||||
<div class="footer-legal">
|
||||
<?php
|
||||
$legal_links = get_field('theme_legal_links', 'option');
|
||||
if ($legal_links && is_array($legal_links)) :
|
||||
?>
|
||||
<div class="footer-legal-inner">
|
||||
<!-- Privacy Policy -->
|
||||
<a href="<?php echo esc_url(home_url('/privacy-policy/')); ?>" class="legal-item">
|
||||
<?php foreach ($legal_links as $link) :
|
||||
$icon = $link['icon'] ?? 'document';
|
||||
?>
|
||||
<a href="<?php echo esc_url($link['url']); ?>" class="legal-item">
|
||||
<?php if ($icon !== 'none') : ?>
|
||||
<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"/>
|
||||
<?php
|
||||
switch ($icon) {
|
||||
case 'shield':
|
||||
echo '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>';
|
||||
break;
|
||||
case 'home':
|
||||
echo '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>';
|
||||
echo '<line x1="8" y1="14" x2="16" y2="14"/>';
|
||||
echo '<line x1="8" y1="17" x2="16" y2="17"/>';
|
||||
break;
|
||||
case 'document':
|
||||
echo '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>';
|
||||
echo '<polyline points="14 2 14 8 20 8"/>';
|
||||
echo '<line x1="16" y1="13" x2="8" y2="13"/>';
|
||||
echo '<line x1="16" y1="17" x2="8" y2="17"/>';
|
||||
break;
|
||||
case 'clipboard':
|
||||
echo '<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"/>';
|
||||
echo '<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>';
|
||||
echo '<line x1="12" y1="11" x2="12" y2="17"/>';
|
||||
echo '<line x1="9" y1="14" x2="15" y2="14"/>';
|
||||
break;
|
||||
case 'info':
|
||||
echo '<circle cx="12" cy="12" r="10"/>';
|
||||
echo '<line x1="12" y1="16" x2="12" y2="12"/>';
|
||||
echo '<line x1="12" y1="8" x2="12.01" y2="8"/>';
|
||||
break;
|
||||
}
|
||||
?>
|
||||
</svg>
|
||||
<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>
|
||||
<span>Fair Housing</span>
|
||||
</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>
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html($link['label']); ?></span>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="footer-license"><?php echo esc_html($broker_info); ?></p>
|
||||
|
||||
<!-- Association Logos -->
|
||||
<div class="footer-association-logos">
|
||||
<img src="<?php echo esc_url(get_template_directory_uri() . '/assets/images/logos/semr-white.webp'); ?>" alt="Southeast Minnesota Realtors" class="association-logo" loading="lazy">
|
||||
<img src="<?php echo esc_url(get_template_directory_uri() . '/assets/images/logos/realtor-equal-housing-white.webp'); ?>" alt="Equal Housing Opportunity and Realtor" class="association-logo" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Bottom -->
|
||||
<div class="footer-bottom">
|
||||
<p class="copyright">
|
||||
© <?php echo date('Y'); ?> <?php bloginfo('name'); ?>. All rights reserved.
|
||||
</p>
|
||||
<p class="footer-credits">
|
||||
Web Design by <a href="https://hanson.xyz" target="_blank" rel="noopener">HansonXyz</a>
|
||||
<?php
|
||||
$copyright_text = homeproz_get_acf_option('footer_copyright', 'HomeProz Real Estate LLC. All rights reserved.');
|
||||
?>
|
||||
<p class="footer-bottom-text">
|
||||
© <?php echo date('Y'); ?> <?php echo esc_html($copyright_text); ?> • Web Design by <a href="https://hanson.xyz" target="_blank" rel="noopener">HansonXyz</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -255,6 +255,33 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// Association Logos
|
||||
.footer-association-logos {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
.association-logo {
|
||||
height: 40px;
|
||||
width: auto;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
gap: 1.5rem;
|
||||
|
||||
.association-logo {
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporary Alt Page Link
|
||||
.footer-temp-link {
|
||||
text-align: center;
|
||||
@@ -283,31 +310,18 @@
|
||||
|
||||
// Footer Bottom
|
||||
.footer-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 2rem;
|
||||
border-top: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.footer-bottom-text {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.8125rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.copyright {
|
||||
color: var(--color-sold);
|
||||
}
|
||||
|
||||
.footer-credits a {
|
||||
a {
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
|
||||
|
||||
@@ -67,9 +67,28 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Shuffle and take up to 3 listings
|
||||
var shuffled = this.shuffleArray(this.listings);
|
||||
var selected = shuffled.slice(0, 3);
|
||||
// Prioritize HomeProz listings, then shuffle remaining
|
||||
var homeprozListings = [];
|
||||
var otherListings = [];
|
||||
|
||||
for (var i = 0; i < this.listings.length; i++) {
|
||||
if (this.listings[i].is_homeproz) {
|
||||
homeprozListings.push(this.listings[i]);
|
||||
} else {
|
||||
otherListings.push(this.listings[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle each group independently
|
||||
homeprozListings = this.shuffleArray(homeprozListings);
|
||||
otherListings = this.shuffleArray(otherListings);
|
||||
|
||||
// Take HomeProz first, then pad with others if needed
|
||||
var selected = homeprozListings.slice(0, 3);
|
||||
if (selected.length < 3) {
|
||||
var needed = 3 - selected.length;
|
||||
selected = selected.concat(otherListings.slice(0, needed));
|
||||
}
|
||||
|
||||
// Build HTML for each listing
|
||||
var html = '';
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* Minimal Property Card Template
|
||||
*
|
||||
* A compact property card for use in thank you pages, confirmations, etc.
|
||||
* Expects $property object from MLS database to be passed via set_query_var('minimal_property', $property)
|
||||
*
|
||||
* @package HomeProz
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$property = get_query_var('minimal_property');
|
||||
|
||||
if (!$property) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract property data
|
||||
$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];
|
||||
}
|
||||
}
|
||||
|
||||
// Format address
|
||||
$address_parts = array();
|
||||
if ($property->street_number) {
|
||||
$address_parts[] = $property->street_number;
|
||||
}
|
||||
if ($property->street_name) {
|
||||
$address_parts[] = $property->street_name;
|
||||
}
|
||||
if ($property->street_suffix) {
|
||||
$address_parts[] = $property->street_suffix;
|
||||
}
|
||||
$street_address = implode(' ', $address_parts);
|
||||
|
||||
$city_state = '';
|
||||
if ($property->city) {
|
||||
$city_state = $property->city;
|
||||
}
|
||||
if ($property->state_or_province) {
|
||||
$city_state .= ', ' . $property->state_or_province;
|
||||
}
|
||||
|
||||
// Property URL
|
||||
$property_url = home_url('/properties/?listing=' . $listing_key);
|
||||
|
||||
// Status class
|
||||
$status_class = 'badge-active';
|
||||
if ($status === 'Pending') {
|
||||
$status_class = 'badge-pending';
|
||||
} elseif ($status === 'Sold' || $status === 'Closed') {
|
||||
$status_class = 'badge-sold';
|
||||
}
|
||||
?>
|
||||
|
||||
<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 : ?>
|
||||
<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"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<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 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">
|
||||
<span class="street"><?php echo esc_html($street_address); ?></span>
|
||||
<span class="city-state"><?php echo esc_html($city_state); ?></span>
|
||||
</div>
|
||||
<?php if ($bedrooms || $bathrooms || $square_feet) : ?>
|
||||
<div class="property-card-minimal-meta">
|
||||
<?php if ($bedrooms) : ?>
|
||||
<span><?php echo esc_html($bedrooms); ?> bd</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($bathrooms) : ?>
|
||||
<span><?php echo esc_html($bathrooms); ?> ba</span>
|
||||
<?php endif; ?>
|
||||
<?php if ($square_feet) : ?>
|
||||
<span><?php echo esc_html(number_format($square_feet)); ?> sqft</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="property-card-minimal-mls">MLS# <?php echo esc_html($listing_id); ?></div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -60,8 +60,20 @@ if ($status === 'Pending') {
|
||||
$status_class = 'badge-sold';
|
||||
}
|
||||
|
||||
// Get thumbnail image URL (index 1 = first image)
|
||||
$image_url = function_exists('mls_get_image_url') ? mls_get_image_url($listing_key, 1, 'thumb') : '';
|
||||
// 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') : '';
|
||||
}
|
||||
$has_image = !empty($image_url);
|
||||
?>
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-accent-hover);
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.4);
|
||||
|
||||
.property-card-link {
|
||||
color: var(--color-accent-hover);
|
||||
|
||||
@@ -15,7 +15,7 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
// Get current filter values from URL (same as main filter)
|
||||
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
|
||||
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '';
|
||||
$current_location = isset($_GET['city']) ? sanitize_text_field($_GET['city']) : '';
|
||||
$current_zip = isset($_GET['zip']) ? sanitize_text_field($_GET['zip']) : '';
|
||||
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
|
||||
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
|
||||
@@ -41,11 +41,13 @@ $mls_cities = homeproz_get_mls_cities(50);
|
||||
</div>
|
||||
|
||||
<div class="filter-item-sticky">
|
||||
<select name="property_location" class="filter-select" aria-label="City">
|
||||
<select name="city" class="filter-select" aria-label="City">
|
||||
<option value="">All Cities</option>
|
||||
<?php foreach ($mls_cities as $city) : ?>
|
||||
<option value="<?php echo esc_attr($city); ?>" <?php selected($current_location, $city); ?>>
|
||||
<?php echo esc_html($city); ?>
|
||||
<?php foreach ($mls_cities as $city_obj) :
|
||||
$city_label = $city_obj->city . ', ' . $city_obj->state_code;
|
||||
?>
|
||||
<option value="<?php echo esc_attr($city_label); ?>" <?php selected($current_location, $city_label); ?>>
|
||||
<?php echo esc_html($city_label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
|
||||
@@ -1160,6 +1160,23 @@
|
||||
|
||||
// Form submission (Search button)
|
||||
this.$form.on('submit', function(e) {
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
|
||||
if (isGridView) {
|
||||
// Grid view: add view=grid hidden field and let form submit naturally
|
||||
e.preventDefault();
|
||||
var formData = self.getFormData();
|
||||
var queryParts = ['view=grid'];
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
queryParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
}
|
||||
}
|
||||
window.location.href = homeprozAjax.archiveUrl + '?' + queryParts.join('&');
|
||||
return;
|
||||
}
|
||||
|
||||
// Map view: use AJAX
|
||||
e.preventDefault();
|
||||
self.filterProperties(1);
|
||||
});
|
||||
@@ -1198,7 +1215,7 @@
|
||||
self.onFilterChange();
|
||||
});
|
||||
|
||||
// Reset buttons - preserve scroll position across page reload
|
||||
// Reset buttons - clear filters and reload
|
||||
$(document).on('click', 'a.filters-reset', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -1216,32 +1233,49 @@
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// Get current scroll position
|
||||
var scrollY = $(window).scrollTop();
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
|
||||
// Cap scroll at the position just below the filter bar (same as after map viewport change)
|
||||
var $filters = $('.property-filters').first();
|
||||
var $masthead = $('#masthead');
|
||||
if ($filters.length) {
|
||||
var mastheadHeight = $masthead.length ? $masthead.outerHeight() : 0;
|
||||
var maxScroll = $filters.offset().top + $filters.outerHeight() - mastheadHeight;
|
||||
scrollY = Math.min(scrollY, Math.max(0, maxScroll));
|
||||
if (isGridView) {
|
||||
// Grid view: simple reset, no scroll preservation
|
||||
window.location.href = '/properties/?view=grid';
|
||||
} else {
|
||||
// Map view: preserve scroll position across page reload
|
||||
var scrollY = $(window).scrollTop();
|
||||
|
||||
// Cap scroll at the position just below the filter bar
|
||||
var $filters = $('.property-filters').first();
|
||||
var $masthead = $('#masthead');
|
||||
if ($filters.length) {
|
||||
var mastheadHeight = $masthead.length ? $masthead.outerHeight() : 0;
|
||||
var maxScroll = $filters.offset().top + $filters.outerHeight() - mastheadHeight;
|
||||
scrollY = Math.min(scrollY, Math.max(0, maxScroll));
|
||||
}
|
||||
|
||||
window.location.href = '/properties/#scroll=' + Math.round(scrollY);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
// Navigate with scroll position in hash, then force reload
|
||||
window.location.href = '/properties/#scroll=' + Math.round(scrollY);
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
// Pagination clicks (delegated)
|
||||
this.$results.on('click', '.pagination a', function(e) {
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
|
||||
if (isGridView) {
|
||||
// Grid view: let browser handle navigation with query params
|
||||
return true;
|
||||
}
|
||||
|
||||
// Map view: use AJAX
|
||||
e.preventDefault();
|
||||
var page = self.getPageFromUrl($(this).attr('href'));
|
||||
self.filterProperties(page);
|
||||
});
|
||||
|
||||
// Handle browser back/forward via hash change
|
||||
// Handle browser back/forward via hash change (map view only)
|
||||
$(window).on('hashchange', function() {
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
if (isGridView) return;
|
||||
|
||||
var page = self.getPageFromHash();
|
||||
self.filterProperties(page, false);
|
||||
});
|
||||
@@ -1723,53 +1757,80 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Update browser URL (all state in hash to avoid WordPress query var conflicts)
|
||||
* Stores: filters, page, scroll position, map position/zoom
|
||||
* Update browser URL
|
||||
* Grid view: uses query params (?view=grid&page=X) - simple, no state tracking
|
||||
* Map view: uses hash for full state (filters, page, scroll, map position)
|
||||
*/
|
||||
updateUrl: function(formData, page) {
|
||||
var hashParts = [];
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
|
||||
// Add non-empty filters to hash
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
hashParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
if (isGridView) {
|
||||
// Grid view: simple query params, no scroll/map state
|
||||
var queryParts = ['view=grid'];
|
||||
|
||||
// Add non-empty filters
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
queryParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
hashParts.push('page=' + page);
|
||||
}
|
||||
|
||||
// Add scroll position (unless blocked - requires user scroll to unblock)
|
||||
if (!this._scrollBlocked) {
|
||||
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
if (scrollY > 0) {
|
||||
hashParts.push('scroll=' + Math.round(scrollY));
|
||||
if (page > 1) {
|
||||
queryParts.push('page=' + page);
|
||||
}
|
||||
|
||||
var newUrl = homeprozAjax.archiveUrl + '?' + queryParts.join('&');
|
||||
history.replaceState(null, '', newUrl);
|
||||
} else {
|
||||
// Map view: full state in hash
|
||||
var hashParts = [];
|
||||
|
||||
// Add non-empty filters to hash
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
hashParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
}
|
||||
}
|
||||
|
||||
if (page > 1) {
|
||||
hashParts.push('page=' + page);
|
||||
}
|
||||
|
||||
// Add scroll position (unless blocked - requires user scroll to unblock)
|
||||
// Only in map view - grid view uses native browser scroll
|
||||
if (!this._scrollBlocked) {
|
||||
var scrollY = window.pageYOffset || document.documentElement.scrollTop;
|
||||
if (scrollY > 0) {
|
||||
hashParts.push('scroll=' + Math.round(scrollY));
|
||||
}
|
||||
}
|
||||
|
||||
// Add map state if map exists
|
||||
if (PropertyMap.map) {
|
||||
var center = PropertyMap.map.getCenter();
|
||||
var zoom = PropertyMap.map.getZoom();
|
||||
hashParts.push('lat=' + center.lat.toFixed(6));
|
||||
hashParts.push('lng=' + center.lng.toFixed(6));
|
||||
hashParts.push('zoom=' + zoom);
|
||||
}
|
||||
|
||||
var newUrl = homeprozAjax.archiveUrl + (hashParts.length ? '#' + hashParts.join('&') : '');
|
||||
history.replaceState(null, '', newUrl);
|
||||
}
|
||||
|
||||
// Add map state if map exists
|
||||
if (PropertyMap.map) {
|
||||
var center = PropertyMap.map.getCenter();
|
||||
var zoom = PropertyMap.map.getZoom();
|
||||
hashParts.push('lat=' + center.lat.toFixed(6));
|
||||
hashParts.push('lng=' + center.lng.toFixed(6));
|
||||
hashParts.push('zoom=' + zoom);
|
||||
}
|
||||
|
||||
var newUrl = homeprozAjax.archiveUrl + (hashParts.length ? '#' + hashParts.join('&') : '');
|
||||
|
||||
// Use replaceState to avoid adding history entries for every page
|
||||
history.replaceState(null, '', newUrl);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update URL with current state (called on scroll and map move)
|
||||
* Debounced to avoid excessive history updates
|
||||
* Only runs in map view - grid view uses standard navigation
|
||||
*/
|
||||
updateUrlState: function() {
|
||||
var self = this;
|
||||
|
||||
// Skip URL updates in grid view - uses standard navigation
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
if (isGridView) return;
|
||||
|
||||
// Debounce URL updates
|
||||
clearTimeout(this._urlUpdateTimeout);
|
||||
this._urlUpdateTimeout = setTimeout(function() {
|
||||
@@ -1817,14 +1878,19 @@
|
||||
* Get page number from pagination link URL
|
||||
*/
|
||||
getPageFromUrl: function(url) {
|
||||
// Check hash first
|
||||
// Check hash first (map view)
|
||||
var hashMatch = url.match(/#page=(\d+)/);
|
||||
if (hashMatch) {
|
||||
return parseInt(hashMatch[1]);
|
||||
}
|
||||
// Fallback to query param for server-rendered links
|
||||
var queryMatch = url.match(/[?&]paged=(\d+)/);
|
||||
return queryMatch ? parseInt(queryMatch[1]) : 1;
|
||||
// Check query param 'page' (grid view)
|
||||
var pageMatch = url.match(/[?&]page=(\d+)/);
|
||||
if (pageMatch) {
|
||||
return parseInt(pageMatch[1]);
|
||||
}
|
||||
// Fallback to WordPress 'paged' query var
|
||||
var pagedMatch = url.match(/[?&]paged=(\d+)/);
|
||||
return pagedMatch ? parseInt(pagedMatch[1]) : 1;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -1834,6 +1900,19 @@
|
||||
var self = this;
|
||||
var formData = this.getFormData();
|
||||
|
||||
// Grid view: redirect with query params
|
||||
var isGridView = !ResponsiveView.isMapView || window.innerWidth < ResponsiveView.breakpoint;
|
||||
if (isGridView) {
|
||||
var queryParts = ['view=grid'];
|
||||
for (var key in formData) {
|
||||
if (formData[key]) {
|
||||
queryParts.push(key + '=' + encodeURIComponent(formData[key]));
|
||||
}
|
||||
}
|
||||
window.location.href = homeprozAjax.archiveUrl + '?' + queryParts.join('&');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update map filters
|
||||
if (PropertyMap.map) {
|
||||
PropertyMap.currentFilters = {
|
||||
@@ -1933,7 +2012,8 @@
|
||||
// - Skip if we have filters (onFilterChange will fit to filtered bounds)
|
||||
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
|
||||
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
|
||||
var skipInitialFit = hasUrlState || hasFilters;
|
||||
var isNearMeMode = typeof homeprozMapData !== 'undefined' && homeprozMapData.isNearMeMode;
|
||||
var skipInitialFit = hasUrlState || hasFilters || isNearMeMode;
|
||||
|
||||
// If we're going to reposition the map (initial fit), show spinner immediately
|
||||
// This prevents showing server-rendered results that aren't viewport-filtered
|
||||
@@ -1998,7 +2078,8 @@
|
||||
// Determine if we should skip initial fit
|
||||
var hasUrlState = PropertyFilters.pendingRestoreState && PropertyFilters.pendingRestoreState.lat !== null;
|
||||
var hasFilters = formData.city || formData.zip || formData.property_type || formData.min_price || formData.max_price || formData.beds;
|
||||
var skipInitialFit = hasUrlState || hasFilters;
|
||||
var isNearMeMode = typeof homeprozMapData !== 'undefined' && homeprozMapData.isNearMeMode;
|
||||
var skipInitialFit = hasUrlState || hasFilters || isNearMeMode;
|
||||
|
||||
// If we're going to reposition the map (initial fit), show spinner immediately
|
||||
if (!skipInitialFit) {
|
||||
@@ -2098,7 +2179,7 @@
|
||||
// cardsWidth = cardColumns * cardWidth + (cardColumns - 1) * cardGap
|
||||
|
||||
// Start with max columns that could fit, work down
|
||||
for (var cols = 5; cols >= 1; cols--) {
|
||||
for (var cols = 5; cols >= 2; cols--) {
|
||||
var cardsWidth = (cols * this.cardWidth) + ((cols - 1) * this.cardGap);
|
||||
// mapWidth = 0.33 * layoutWidth, so:
|
||||
// layoutWidth = mapWidth + mapGap + cardsWidth
|
||||
@@ -2114,11 +2195,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: 1 column
|
||||
var cardsWidth = this.cardWidth;
|
||||
var layoutWidth = (this.mapGap + cardsWidth) / (1 - this.mapRatio);
|
||||
this.setProperties(Math.min(layoutWidth, availableWidth), 1, '.property-map-layout');
|
||||
this.setProperties(Math.min(layoutWidth, availableWidth), 1, '.property-list-container');
|
||||
// 1 column or less: use full available width (no centering narrow layout)
|
||||
// This prevents the "shrunk" appearance at 1024-1340px viewports
|
||||
this.setProperties(availableWidth, 1, '.property-map-layout');
|
||||
this.setProperties(availableWidth, 1, '.property-list-container');
|
||||
},
|
||||
|
||||
calculateGridLayout: function(availableWidth) {
|
||||
@@ -2827,8 +2907,215 @@
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Near Me Handler
|
||||
* Handles geolocation for "Properties Near Me" feature
|
||||
*/
|
||||
var NearMeHandler = {
|
||||
RADIUS_MILES: 30,
|
||||
userLocation: null, // Stores {lat, lng} when near-me is used
|
||||
|
||||
init: function() {
|
||||
// Check if we're in near-me mode
|
||||
if (typeof homeprozMapData === 'undefined' || !homeprozMapData.isNearMeMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$overlay = $('#near-me-overlay');
|
||||
this.$title = $('#near-me-title');
|
||||
this.$message = $('#near-me-message');
|
||||
this.$main = $('.property-archive-main');
|
||||
this.$nearMeBtn = $('.view-toggle-nearme');
|
||||
|
||||
if (!this.$overlay.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start geolocation request
|
||||
this.requestLocation();
|
||||
},
|
||||
|
||||
requestLocation: function() {
|
||||
var self = this;
|
||||
|
||||
// Check if geolocation is supported
|
||||
if (!navigator.geolocation) {
|
||||
console.log('[NearMe] Geolocation not supported');
|
||||
this.showError('Location services are not supported by your browser.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request location
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
self.onLocationSuccess(position);
|
||||
},
|
||||
function(error) {
|
||||
console.log('[NearMe] Geolocation error:', error.code, error.message);
|
||||
self.onLocationError(error);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: false,
|
||||
timeout: 15000,
|
||||
maximumAge: 300000 // 5 minutes cache
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
onLocationSuccess: function(position) {
|
||||
var lat = position.coords.latitude;
|
||||
var lng = position.coords.longitude;
|
||||
var self = this;
|
||||
|
||||
console.log('[NearMe] Location:', lat.toFixed(4), lng.toFixed(4));
|
||||
|
||||
// Update overlay message while we calculate zoom
|
||||
this.$title.text('Finding nearby properties...');
|
||||
this.$message.text('Calculating the best view for your area.');
|
||||
|
||||
// Call server to calculate optimal zoom level
|
||||
$.ajax({
|
||||
url: homeprozMapData.clusterEndpoint,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'homeproz_calculate_near_me_zoom',
|
||||
lat: lat,
|
||||
lng: lng
|
||||
},
|
||||
success: function(response) {
|
||||
var zoomLevel = 9; // Default fallback
|
||||
if (response.success && response.data && response.data.zoom) {
|
||||
zoomLevel = response.data.zoom;
|
||||
console.log('[NearMe] Zoom:', zoomLevel, '(' + response.data.count + ' properties)');
|
||||
}
|
||||
|
||||
self.showMapWithLocation(lat, lng, zoomLevel);
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
console.log('[NearMe] Zoom calculation failed:', error);
|
||||
// Fall back to default zoom
|
||||
self.showMapWithLocation(lat, lng, 9);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
showMapWithLocation: function(lat, lng, zoomLevel) {
|
||||
var self = this;
|
||||
|
||||
// Store user location for viewport checking
|
||||
this.userLocation = { lat: lat, lng: lng };
|
||||
|
||||
// Remove near-me mode class to show content
|
||||
this.$main.removeClass('is-near-me-mode');
|
||||
|
||||
// Hide overlay
|
||||
this.$overlay.fadeOut(300, function() {
|
||||
$(this).remove();
|
||||
});
|
||||
|
||||
// Wait for map to initialize, then set view
|
||||
var attempts = 0;
|
||||
var checkMap = setInterval(function() {
|
||||
attempts++;
|
||||
|
||||
if (typeof PropertyMap !== 'undefined' && PropertyMap.map) {
|
||||
clearInterval(checkMap);
|
||||
|
||||
// CRITICAL: Tell Leaflet to recalculate container size
|
||||
// The map was hidden, so it doesn't know its dimensions
|
||||
PropertyMap.map.invalidateSize();
|
||||
|
||||
// Small delay to let tiles recalculate, then set view
|
||||
setTimeout(function() {
|
||||
PropertyMap.map.setView([lat, lng], zoomLevel);
|
||||
|
||||
// Add a marker for user's location
|
||||
var userIcon = L.divIcon({
|
||||
className: 'user-location-marker',
|
||||
html: '<div class="user-location-pulse"></div><div class="user-location-dot"></div>',
|
||||
iconSize: [24, 24],
|
||||
iconAnchor: [12, 12]
|
||||
});
|
||||
|
||||
L.marker([lat, lng], { icon: userIcon })
|
||||
.addTo(PropertyMap.map)
|
||||
.bindPopup('Your location');
|
||||
|
||||
// Trigger property fetch for the new viewport
|
||||
PropertyMap.loadClusters();
|
||||
|
||||
// Set up viewport monitoring for Near Me button state
|
||||
self.setupViewportMonitoring();
|
||||
self.updateNearMeButtonState();
|
||||
}, 100);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Timeout after 5 seconds if map doesn't initialize
|
||||
setTimeout(function() {
|
||||
if (attempts > 0) {
|
||||
clearInterval(checkMap);
|
||||
}
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
setupViewportMonitoring: function() {
|
||||
var self = this;
|
||||
if (PropertyMap.map) {
|
||||
PropertyMap.map.on('moveend', function() {
|
||||
self.updateNearMeButtonState();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
updateNearMeButtonState: function() {
|
||||
if (!this.userLocation || !PropertyMap.map) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bounds = PropertyMap.map.getBounds();
|
||||
var userLatLng = L.latLng(this.userLocation.lat, this.userLocation.lng);
|
||||
var isInView = bounds.contains(userLatLng);
|
||||
|
||||
if (isInView) {
|
||||
this.$nearMeBtn.addClass('active');
|
||||
} else {
|
||||
this.$nearMeBtn.removeClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
onLocationError: function(error) {
|
||||
var message = 'Unable to determine your location.';
|
||||
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
message = 'Location access was denied. Please enable location services or click the button below to browse all properties.';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
message = 'Your location could not be determined. Please try again or browse all properties.';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
message = 'Location request timed out. Please try again or browse all properties.';
|
||||
break;
|
||||
}
|
||||
|
||||
this.showError(message);
|
||||
},
|
||||
|
||||
showError: function(message) {
|
||||
this.$overlay.addClass('has-error');
|
||||
this.$title.text('Location unavailable');
|
||||
this.$message.text(message);
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize on document ready
|
||||
$(function() {
|
||||
// Handle near-me mode first
|
||||
if (typeof homeprozMapData !== 'undefined' && homeprozMapData.isNearMeMode) {
|
||||
NearMeHandler.init();
|
||||
}
|
||||
|
||||
PropertyFilters.init();
|
||||
ResponsiveView.init();
|
||||
LayoutCalculator.init();
|
||||
|
||||
@@ -14,7 +14,7 @@ if (!defined('ABSPATH')) {
|
||||
|
||||
// Get current filter values from URL
|
||||
$current_type = isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '';
|
||||
$current_location = isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '';
|
||||
$current_location = isset($_GET['city']) ? sanitize_text_field($_GET['city']) : '';
|
||||
$current_zip = isset($_GET['zip']) ? sanitize_text_field($_GET['zip']) : '';
|
||||
$current_min_price = isset($_GET['min_price']) ? intval($_GET['min_price']) : '';
|
||||
$current_max_price = isset($_GET['max_price']) ? intval($_GET['max_price']) : '';
|
||||
@@ -42,11 +42,13 @@ $mls_cities = homeproz_get_mls_cities(50); // Cities with 50+ listings
|
||||
|
||||
<div class="filter-item">
|
||||
<label for="filter-location" class="filter-label">City</label>
|
||||
<select name="property_location" id="filter-location" class="filter-select">
|
||||
<select name="city" id="filter-location" class="filter-select">
|
||||
<option value="">All Cities</option>
|
||||
<?php foreach ($mls_cities as $city) : ?>
|
||||
<option value="<?php echo esc_attr($city); ?>" <?php selected($current_location, $city); ?>>
|
||||
<?php echo esc_html($city); ?>
|
||||
<?php foreach ($mls_cities as $city_obj) :
|
||||
$city_label = $city_obj->city . ', ' . $city_obj->state_code;
|
||||
?>
|
||||
<option value="<?php echo esc_attr($city_label); ?>" <?php selected($current_location, $city_label); ?>>
|
||||
<?php echo esc_html($city_label); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
@@ -102,7 +104,7 @@ $mls_cities = homeproz_get_mls_cities(50); // Cities with 50+ listings
|
||||
|
||||
<div class="filter-item filter-item-button">
|
||||
<label class="filter-label"> </label>
|
||||
<a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-secondary">Reset</a>
|
||||
<a href="<?php echo esc_url(get_post_type_archive_link('property')); ?>" class="btn btn-secondary filters-reset">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -98,6 +98,10 @@
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.view-toggle-nearme {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive View Containers
|
||||
@@ -155,16 +159,22 @@
|
||||
}
|
||||
}
|
||||
|
||||
.property-map-container {
|
||||
// Sidebar container - stretches to fill grid cell, contains the sticky content
|
||||
.property-sidebar {
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
padding-bottom: 20px; // Creates gap at bottom so sticky content stops earlier
|
||||
}
|
||||
}
|
||||
|
||||
// Inner wrapper - this is the sticky element
|
||||
.property-sidebar-content {
|
||||
@media (min-width: 1024px) {
|
||||
position: sticky;
|
||||
top: 100px;
|
||||
height: calc(50vh - 75px);
|
||||
}
|
||||
|
||||
// View toggle inside map container
|
||||
.view-toggle {
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
@@ -179,7 +189,7 @@
|
||||
border: 1px solid var(--color-border);
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
height: calc(100% - 44px); // Account for view toggle height + margin
|
||||
height: calc(50vh - 75px); // Fixed height for map on desktop
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +208,9 @@
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
// JS sets --card-columns, fallback to 2
|
||||
grid-template-columns: repeat(var(--card-columns, 2), 400px);
|
||||
// When 1 column: use 1fr to fill available space
|
||||
// When 2+ columns: use fixed 400px widths
|
||||
grid-template-columns: repeat(var(--card-columns, 2), minmax(350px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
@@ -282,44 +294,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Small clusters (< 100 properties) - smaller, neutral
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 126, 99, 0.6);
|
||||
|
||||
div {
|
||||
background-color: rgba(181, 126, 99, 0.9);
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
}
|
||||
background-color: rgba(120, 120, 120, 0.45);
|
||||
}
|
||||
|
||||
// Medium clusters (100-200 properties)
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(181, 126, 99, 0.7);
|
||||
|
||||
div {
|
||||
background-color: rgba(181, 126, 99, 0.95);
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
font-size: 13px;
|
||||
}
|
||||
background-color: rgba(200, 80, 80, 0.5);
|
||||
}
|
||||
|
||||
// Large clusters (> 200 properties) - bolder, less transparent
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(181, 126, 99, 0.8);
|
||||
background-color: rgba(220, 40, 40, 0.7);
|
||||
}
|
||||
|
||||
div {
|
||||
background-color: rgba(181, 126, 99, 1);
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-left: 0;
|
||||
margin-top: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
// Inner div styling (text must be fully opaque)
|
||||
.marker-cluster-small div {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-left: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.marker-cluster-medium div {
|
||||
width: 27px;
|
||||
height: 27px;
|
||||
margin-left: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.marker-cluster-large div {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-left: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.marker-cluster-small div,
|
||||
.marker-cluster-medium div,
|
||||
.marker-cluster-large div {
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
// Density dots (zoom 1-11)
|
||||
@@ -728,3 +746,119 @@
|
||||
#property-results.infinite-scroll-enabled .pagination {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
// ===========================================
|
||||
// NEAR ME LOCATION PROMPT
|
||||
// ===========================================
|
||||
|
||||
// Hide map content when in near-me mode until location is resolved
|
||||
.is-near-me-mode {
|
||||
.property-map-layout,
|
||||
.grid-view-container,
|
||||
.property-filters {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.near-me-overlay {
|
||||
padding: 4rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.near-me-prompt {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.near-me-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--color-accent);
|
||||
|
||||
svg {
|
||||
animation: pulse-location 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-location {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.near-me-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.near-me-message {
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.near-me-fallback {
|
||||
// Button already styled via .btn .btn-primary
|
||||
}
|
||||
|
||||
// Error state
|
||||
.near-me-overlay.has-error {
|
||||
.near-me-icon {
|
||||
color: var(--color-text-muted);
|
||||
|
||||
svg {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User location marker on map
|
||||
.user-location-marker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.user-location-dot {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: var(--color-accent);
|
||||
border: 2px solid white;
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.user-location-pulse {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(var(--color-accent-rgb, 181, 101, 29), 0.3);
|
||||
border-radius: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: user-location-pulse 2s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes user-location-pulse {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.5);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@
|
||||
* Initialize
|
||||
*/
|
||||
init: function() {
|
||||
// Only run on single property pages
|
||||
if (!$('.Single_Property').length && !$('.Single_Property_MLS').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$gallery = $('.property-gallery');
|
||||
this.$lightbox = $('#property-lightbox');
|
||||
|
||||
@@ -69,10 +74,21 @@
|
||||
// Load images data
|
||||
var $dataScript = $('#gallery-images-data');
|
||||
if ($dataScript.length) {
|
||||
this.images = JSON.parse($dataScript.text());
|
||||
try {
|
||||
this.images = JSON.parse($dataScript.text());
|
||||
} catch (e) {
|
||||
console.error('PropertyGallery: Failed to parse gallery data');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.images.length === 0) {
|
||||
if (!this.images || this.images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate we have required elements
|
||||
if (!this.$mainImage.length || !this.$lightboxImage.length) {
|
||||
console.error('PropertyGallery: Missing required elements');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,9 +167,12 @@
|
||||
self.openLightbox(self.currentIndex);
|
||||
});
|
||||
|
||||
// Close lightbox
|
||||
this.$lightbox.find('.lightbox-close, .lightbox-overlay').on('click', function() {
|
||||
self.closeLightbox();
|
||||
// Close lightbox - clicking overlay, container, or close button
|
||||
this.$lightbox.find('.lightbox-close, .lightbox-overlay, .lightbox-container').on('click', function(e) {
|
||||
// Only close if clicking directly on these elements, not their children (except close btn)
|
||||
if (e.target === this || $(this).hasClass('lightbox-close')) {
|
||||
self.closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
// Lightbox navigation
|
||||
@@ -339,9 +358,16 @@
|
||||
if (newIndex >= this.images.length) newIndex = 0;
|
||||
}
|
||||
|
||||
var image = this.images[newIndex];
|
||||
|
||||
// Validate image data
|
||||
if (!image || !image.url) {
|
||||
console.error('PropertyGallery: Invalid main image at index', newIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
var image = this.images[newIndex];
|
||||
var slideFrom = direction === 'next' ? '100%' : '-100%';
|
||||
var slideTo = direction === 'next' ? '-100%' : '100%';
|
||||
|
||||
@@ -425,6 +451,12 @@
|
||||
|
||||
var image = this.images[index];
|
||||
|
||||
// Validate image data
|
||||
if (!image || !image.url) {
|
||||
console.error('PropertyGallery: Invalid image data at index', index);
|
||||
return;
|
||||
}
|
||||
|
||||
if (useFade) {
|
||||
// Fade transition for autoplay
|
||||
this.isTransitioning = true;
|
||||
@@ -589,9 +621,16 @@
|
||||
if (newIndex >= this.images.length) newIndex = 0;
|
||||
}
|
||||
|
||||
var image = this.images[newIndex];
|
||||
|
||||
// Validate image data
|
||||
if (!image || !image.url) {
|
||||
console.error('PropertyGallery: Invalid lightbox image at index', newIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
var image = this.images[newIndex];
|
||||
var slideFrom = direction === 'next' ? '100%' : '-100%';
|
||||
var slideTo = direction === 'next' ? '-100%' : '100%';
|
||||
|
||||
@@ -670,6 +709,10 @@
|
||||
*/
|
||||
updateLightboxImage: function() {
|
||||
var image = this.images[this.currentIndex];
|
||||
if (!image || !image.url) {
|
||||
console.error('PropertyGallery: Invalid lightbox image at index', this.currentIndex);
|
||||
return;
|
||||
}
|
||||
this.$lightboxImage.attr('src', image.url);
|
||||
this.$lightboxImage.attr('alt', image.alt || 'Property photo');
|
||||
this.$lightboxCounter.text(this.currentIndex + 1);
|
||||
|
||||
@@ -37,8 +37,8 @@ $center_lat = isset($_GET['lat']) ? floatval($_GET['lat']) : '';
|
||||
$center_lng = isset($_GET['lng']) ? floatval($_GET['lng']) : '';
|
||||
$radius = isset($_GET['radius']) ? intval($_GET['radius']) : 30;
|
||||
|
||||
// Pagination
|
||||
$paged = get_query_var('paged') ? get_query_var('paged') : 1;
|
||||
// Pagination - check query param first (grid view), then WordPress query var
|
||||
$paged = isset($_GET['page']) ? max(1, intval($_GET['page'])) : (get_query_var('paged') ? get_query_var('paged') : 1);
|
||||
$per_page = 12;
|
||||
|
||||
// Build filter args for count and properties
|
||||
@@ -88,6 +88,14 @@ $mls_args = array_merge($filter_args, array(
|
||||
'order' => 'DESC',
|
||||
));
|
||||
|
||||
// Get featured property IDs to prioritize after HomeProz listings
|
||||
if (function_exists('homeproz_get_featured_mls_ids')) {
|
||||
$featured_ids = homeproz_get_featured_mls_ids();
|
||||
if (!empty($featured_ids)) {
|
||||
$mls_args['featured_ids'] = $featured_ids;
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch only the properties we need for this page
|
||||
$paged_properties = mls_get_properties($mls_args);
|
||||
?>
|
||||
@@ -116,17 +124,42 @@ $paged_properties = mls_get_properties($mls_args);
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Pagination with hash-based page numbers (prevents WordPress server-side conflicts)
|
||||
// Pagination - use query params for grid view, hash for map view
|
||||
$base_url = get_post_type_archive_link('property');
|
||||
$pagination = paginate_links(array(
|
||||
'base' => $base_url . '#page=%#%',
|
||||
'format' => '',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $max_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
));
|
||||
$is_grid_view = isset($_GET['view']) && $_GET['view'] === 'grid';
|
||||
|
||||
if ($is_grid_view) {
|
||||
// Grid view: use query params (?view=grid&page=X)
|
||||
// Build base with current filters preserved
|
||||
$query_args = array('view' => 'grid');
|
||||
if ($current_type) $query_args['property_type'] = $current_type;
|
||||
if ($current_location) $query_args['city'] = $current_location;
|
||||
if ($current_zip) $query_args['zip'] = $current_zip;
|
||||
if ($current_min_price) $query_args['min_price'] = $current_min_price;
|
||||
if ($current_max_price) $query_args['max_price'] = $current_max_price;
|
||||
if ($current_beds) $query_args['beds'] = $current_beds;
|
||||
|
||||
$pagination = paginate_links(array(
|
||||
'base' => add_query_arg(array_merge($query_args, array('page' => '%#%')), $base_url),
|
||||
'format' => '',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $max_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
));
|
||||
} else {
|
||||
// Map view: use hash (#page=X)
|
||||
$pagination = paginate_links(array(
|
||||
'base' => $base_url . '#page=%#%',
|
||||
'format' => '',
|
||||
'current' => max(1, $paged),
|
||||
'total' => $max_pages,
|
||||
'prev_text' => '← Previous',
|
||||
'next_text' => 'Next →',
|
||||
'type' => 'array',
|
||||
));
|
||||
}
|
||||
|
||||
if ($pagination) :
|
||||
?>
|
||||
|
||||
@@ -88,6 +88,10 @@ 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;
|
||||
|
||||
// Format lot size
|
||||
$lot_display = '';
|
||||
if ($lot_size) {
|
||||
@@ -130,9 +134,20 @@ get_header();
|
||||
<!-- Gallery -->
|
||||
<?php
|
||||
$image_count = count($media);
|
||||
if ($image_count > 0) :
|
||||
$has_override_photo = !empty($override_photo);
|
||||
if ($image_count > 0 || $has_override_photo) :
|
||||
// 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
|
||||
foreach ($media as $item) {
|
||||
$full_url = function_exists('mls_get_image_url')
|
||||
? mls_get_image_url($listing_key, $item->media_order, 'full')
|
||||
@@ -175,7 +190,27 @@ get_header();
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($image_count > 1) : ?>
|
||||
<?php if ($image_count > 1) :
|
||||
// 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
|
||||
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;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!-- Thumbnails -->
|
||||
<div class="gallery-thumbnails-container">
|
||||
<button class="gallery-thumbnails-nav gallery-thumbnails-prev" type="button" aria-label="Previous thumbnails" disabled>
|
||||
@@ -185,12 +220,7 @@ get_header();
|
||||
</button>
|
||||
<div class="gallery-thumbnails-viewport">
|
||||
<div class="gallery-thumbnails">
|
||||
<?php foreach ($media as $index => $item) :
|
||||
$thumb_url = function_exists('mls_get_image_url')
|
||||
? mls_get_image_url($listing_key, $item->media_order, 'thumb')
|
||||
: '';
|
||||
if (!$thumb_url) continue;
|
||||
?>
|
||||
<?php foreach ($thumbnail_images as $index => $thumb_url) : ?>
|
||||
<button
|
||||
class="gallery-thumbnail <?php echo $index === 0 ? 'is-active' : ''; ?>"
|
||||
type="button"
|
||||
@@ -322,21 +352,34 @@ get_header();
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Directions -->
|
||||
<?php if ($directions) : ?>
|
||||
<section class="property-directions">
|
||||
<h2 class="section-title">Directions</h2>
|
||||
<p><?php echo esc_html($directions); ?></p>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
<!-- Location and Directions -->
|
||||
<?php if ($property->latitude && $property->longitude) :
|
||||
$google_maps_url = 'https://www.google.com/maps/search/?api=1&query=' . urlencode($full_address);
|
||||
?>
|
||||
<section class="property-location-section">
|
||||
<h2 class="section-title">Location and Directions</h2>
|
||||
|
||||
<!-- Map -->
|
||||
<?php if ($property->latitude && $property->longitude) : ?>
|
||||
<section class="property-map-section">
|
||||
<h2 class="section-title">Location</h2>
|
||||
<div id="property-location-map" class="property-location-map"
|
||||
data-lat="<?php echo esc_attr($property->latitude); ?>"
|
||||
data-lng="<?php echo esc_attr($property->longitude); ?>">
|
||||
<p class="property-address-link">
|
||||
<a href="<?php echo esc_url($google_maps_url); ?>" target="_blank" rel="noopener noreferrer">
|
||||
<?php echo esc_html($full_address); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<?php if ($directions) : ?>
|
||||
<p class="property-directions-text"><?php echo esc_html($directions); ?></p>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="property-location-map">
|
||||
<iframe
|
||||
src="https://www.google.com/maps?q=<?php echo urlencode($full_address); ?>&z=15&output=embed"
|
||||
width="100%"
|
||||
height="300"
|
||||
style="border:0;"
|
||||
allowfullscreen=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer-when-downgrade"
|
||||
title="Property Location: <?php echo esc_attr($full_address); ?>">
|
||||
</iframe>
|
||||
</div>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
@@ -373,14 +416,10 @@ get_header();
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$contact_url = add_query_arg(array(
|
||||
'property' => urlencode($full_address),
|
||||
'property_url' => urlencode(home_url('/properties/?listing=' . $listing_key)),
|
||||
'property-inquiry' => urlencode($full_address),
|
||||
), home_url('/contact/'));
|
||||
$inquiry_url = add_query_arg('listing', $listing_key, home_url('/property-inquiry/'));
|
||||
?>
|
||||
<a href="<?php echo esc_url($contact_url); ?>" class="btn btn-primary btn-block">
|
||||
Contact About This Property
|
||||
<a href="<?php echo esc_url($inquiry_url); ?>" class="btn btn-primary btn-block">
|
||||
Request More Information
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -408,30 +447,6 @@ get_header();
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php if ($property->latitude && $property->longitude) : ?>
|
||||
<!-- Leaflet for property map -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script>
|
||||
(function() {
|
||||
var mapEl = document.getElementById('property-location-map');
|
||||
if (!mapEl) return;
|
||||
|
||||
var lat = parseFloat(mapEl.dataset.lat);
|
||||
var lng = parseFloat(mapEl.dataset.lng);
|
||||
|
||||
var map = L.map('property-location-map').setView([lat, lng], 15);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© OpenStreetMap'
|
||||
}).addTo(map);
|
||||
|
||||
L.marker([lat, lng]).addTo(map);
|
||||
})();
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<script>
|
||||
// Copy link button
|
||||
(function() {
|
||||
|
||||
@@ -614,24 +614,37 @@ body.lightbox-open {
|
||||
}
|
||||
}
|
||||
|
||||
// Location Map
|
||||
.property-map-section {
|
||||
// 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 {
|
||||
height: 300px;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Directions
|
||||
.property-directions {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
p {
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.6;
|
||||
iframe {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
const { test, expect } = require('@playwright/test');
|
||||
|
||||
test.describe('Map pan scroll state', () => {
|
||||
test('should not preserve scroll position when map pans', async ({ page }) => {
|
||||
// Go to properties page with filters
|
||||
await page.goto('https://homeproz.dev.hanson.xyz/properties/?min_price=250000&beds=2');
|
||||
|
||||
// Wait for map to load
|
||||
await page.waitForSelector('#property-map');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Get initial scroll position
|
||||
const initialScroll = await page.evaluate(() => window.scrollY);
|
||||
console.log('Initial scroll:', initialScroll);
|
||||
|
||||
// Get initial URL
|
||||
const initialUrl = page.url();
|
||||
console.log('Initial URL:', initialUrl);
|
||||
|
||||
// Zoom in on the map (simulate mouse wheel)
|
||||
const map = page.locator('#property-map');
|
||||
await map.hover();
|
||||
|
||||
// Zoom in
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await page.mouse.wheel(0, -500);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Wait for map to settle and content to load
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check URL after zoom in
|
||||
const urlAfterZoomIn = page.url();
|
||||
console.log('URL after zoom in:', urlAfterZoomIn);
|
||||
|
||||
// Get scroll position
|
||||
const scrollAfterZoomIn = await page.evaluate(() => window.scrollY);
|
||||
console.log('Scroll after zoom in:', scrollAfterZoomIn);
|
||||
|
||||
// Check if scroll is in URL
|
||||
const hasScrollAfterZoomIn = urlAfterZoomIn.includes('scroll=');
|
||||
console.log('Has scroll in URL after zoom in:', hasScrollAfterZoomIn);
|
||||
|
||||
// Now zoom out
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await page.mouse.wheel(0, 500);
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Wait for map to settle
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Check URL after zoom out
|
||||
const urlAfterZoomOut = page.url();
|
||||
console.log('URL after zoom out:', urlAfterZoomOut);
|
||||
|
||||
// Get scroll position
|
||||
const scrollAfterZoomOut = await page.evaluate(() => window.scrollY);
|
||||
console.log('Scroll after zoom out:', scrollAfterZoomOut);
|
||||
|
||||
// Check if scroll is in URL - it should NOT be there after map pan
|
||||
const hasScrollAfterZoomOut = urlAfterZoomOut.includes('scroll=');
|
||||
console.log('Has scroll in URL after zoom out:', hasScrollAfterZoomOut);
|
||||
|
||||
// Parse the scroll value if it exists
|
||||
if (hasScrollAfterZoomOut) {
|
||||
const scrollMatch = urlAfterZoomOut.match(/scroll=(\d+)/);
|
||||
if (scrollMatch) {
|
||||
console.log('Scroll value in URL:', scrollMatch[1]);
|
||||
console.log('Actual window scroll:', scrollAfterZoomOut);
|
||||
}
|
||||
}
|
||||
|
||||
// The scroll should not be preserved after map pan
|
||||
expect(hasScrollAfterZoomOut).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
/*
|
||||
* Link styles
|
||||
* https://github.com/WordPress/gutenberg/issues/42319
|
||||
*/
|
||||
a {
|
||||
text-decoration-thickness: 1px !important;
|
||||
text-underline-offset: .1em;
|
||||
}
|
||||