(function($){
let communities=[]
let filterOptions={ cities: [], states: [], zips: [] }
let userLocation=null
let map=null
let markers=[]
let infoWindow=null
let filterTimeout=null
let zipLatLng=null
let zipMarker=null
let nonceRefreshAttempted=false
let currentMapBounds=null
let boundsFilteringEnabled=true
let isMapOperationInProgress=false
let isUserInteraction=false
function debounce(func, delay=300){
return function(){
const context=this
const args=arguments
clearTimeout(filterTimeout)
filterTimeout=setTimeout(()=> func.apply(context, args), delay)
}}
function refreshNonce(callback){
if(nonceRefreshAttempted){
console.log('Nonce refresh already attempted, skipping')
if(callback) callback(false)
return
}
nonceRefreshAttempted=true
console.log('Attempting to refresh nonce...')
$.ajax({
url: CGL.ajax_url,
type: 'POST',
data: { action: 'cgl_refresh_nonce' },
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-WP-Nonce-Refresh': '1',
'Cache-Control': 'no-cache'
},
timeout: 10000,
success: function(res){
if(res.success&&res.data&&res.data.nonce){
const oldNonce=CGL.nonce.substring(0, 10)
CGL.nonce=res.data.nonce
console.log('✓ Nonce refreshed successfully!')
console.log('  Old:', oldNonce + '... → New:', CGL.nonce.substring(0, 10) + '...')
setTimeout(function(){ nonceRefreshAttempted=false }, 5000)
if(callback) callback(true)
}else{
console.warn('Nonce refresh returned invalid response:', res)
if(callback) callback(false)
}},
error: function(xhr, status, error){
console.error('Failed to refresh nonce:', {
status: xhr.status,
statusText: status,
error: error,
responseText: xhr.responseText ? xhr.responseText.substring(0, 200):'none'
})
if(xhr.status===403){
console.error('⚠️ Nonce refresh still blocked with 403!')
console.error('This usually means:')
console.error('1. Cloudflare WAF is still blocking /wp-admin/admin-ajax.php')
console.error('2. WordPress security plugin is blocking the request')
console.error('3. Server firewall is blocking the request')
}else if(xhr.status===0){
console.error('Network error - request blocked before reaching server')
}
if(callback) callback(false)
}})
}
function ajaxWithRetry(options){
const originalError=options.error
const originalSuccess=options.success
let retryAttempted=false
if(!options.timeout){
options.timeout=30000 
}
options.error=function(xhr, status, error){
console.log('AJAX request failed:', {
action: options.data?.action||'unknown',
status: xhr.status,
statusText: status,
error: error
})
if(xhr.status===403&&!retryAttempted){
console.log('403 error detected, attempting to refresh nonce and retry...')
retryAttempted=true
refreshNonce(function(success){
if(success){
console.log('Retrying request with new nonce...')
if(typeof options.data==='string'){
options.data=options.data.replace(/nonce=[^&]*/, 'nonce=' + CGL.nonce)
}else if(typeof options.data==='object'){
options.data.nonce=CGL.nonce
}
options.error=originalError
$.ajax(options)
}else{
console.error('Could not refresh nonce, request failed permanently')
if(xhr.status===403){
alert('Security token expired. Please refresh the page to continue.')
}
if(originalError) originalError(xhr, status, error)
}})
}else{
if(originalError) originalError(xhr, status, error)
}}
options.success=function(res){
retryAttempted=false
if(originalSuccess) originalSuccess(res)
}
return $.ajax(options)
}
function isCommunityInBounds(community){
if(!currentMapBounds||!community.location.lat||!community.location.long){
return true
}
try {
const lat=parseFloat(community.location.lat)
const lng=parseFloat(community.location.long)
if(isNaN(lat)||isNaN(lng)) return true
return currentMapBounds.contains({ lat, lng })
} catch (e){
console.error('Error checking bounds for community:', e)
return true
}}
function filterCommunitiesByBounds(communities){
if(!boundsFilteringEnabled||!currentMapBounds){
return communities
}
return communities.filter(community=> isCommunityInBounds(community))
}
function updateListOnly(){
if(!isUserInteraction||!currentMapBounds) return
console.log('Updating list based on visible map area')
const text=$('#cgl-search').val().toLowerCase()
const city=$('#cgl-city-filter').val()
const state=$('#cgl-state-filter').val()
const zip=$('#cgl-zip-filter').val()
let filtered=communities.filter(c=> {
if(text&&!(
c.title.toLowerCase().includes(text) ||
c.location.address.toLowerCase().includes(text) ||
c.location.city.toLowerCase().includes(text) ||
c.location.state.toLowerCase().includes(text) ||
c.location.zip_code.toLowerCase().includes(text)
)) return false
if(city&&c.location.city!==city) return false
if(state&&c.location.state!==state) return false
return true
})
if(zipLatLng){
filtered.forEach(c=> {
if(c.location.lat&&c.location.long){
c._distance=getDistanceMiles(
parseFloat(c.location.lat),
parseFloat(c.location.long),
zipLatLng.lat,
zipLatLng.lng
)
}else{
c._distance=null
}})
filtered.sort((a, b)=> {
if(a._distance==null) return 1
if(b._distance==null) return -1
return a._distance - b._distance
})
}
const boundsFiltered=filterCommunitiesByBounds(filtered)
renderList(boundsFiltered)
}
const mapStyles=[
{
"featureType": "administrative",
"elementType": "labels.text.fill",
"stylers": [{"color": "#666666"}]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [{"color": "#0073aa"}]
},
{
"featureType": "administrative.province",
"elementType": "geometry.stroke",
"stylers": [{"color": "#cccccc"}, {"weight": 1}]
},
{
"featureType": "administrative.land_parcel",
"elementType": "geometry.stroke",
"stylers": [{"color": "#eeeeee"}]
},
{
"featureType": "landscape",
"elementType": "all",
"stylers": [{"color": "#f8f9fa"}]
},
{
"featureType": "poi",
"elementType": "all",
"stylers": [{"visibility": "off"}]
},
{
"featureType": "road",
"elementType": "all",
"stylers": [{"saturation": -100}, {"lightness": 45}]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [{"color": "#eeeeee"}]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [{"color": "#787878"}]
},
{
"featureType": "road.arterial",
"elementType": "geometry",
"stylers": [{"color": "#f1f1f1"}]
},
{
"featureType": "road.local",
"elementType": "geometry",
"stylers": [{"color": "#ffffff"}]
},
{
"featureType": "transit",
"elementType": "all",
"stylers": [{"visibility": "off"}]
},
{
"featureType": "water",
"elementType": "all",
"stylers": [{"color": "#e9f2f7"}, {"visibility": "on"}]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [{"color": "#6699cc"}]
}
]
function getUrlParams(){
const params={}
window.location.search.substring(1).split('&').forEach(function(pair){
if(!pair) return
const [key, value]=pair.split('=')
if(key) params[decodeURIComponent(key)]=decodeURIComponent(value||'')
})
return params
}
const urlParams=getUrlParams()
let cglDropdownsReady=false
let cglCommunitiesReady=false
function maybeApplyUrlFilters(){
if(cglDropdownsReady&&cglCommunitiesReady){
let hasUrlFilter=false
if(urlParams['location-search']){
$('#cgl-search').val(urlParams['location-search'])
hasUrlFilter=true
}
if(urlParams['location-state']){
$('#cgl-state-filter').val(urlParams['location-state'])
hasUrlFilter=true
}
if(urlParams['location-city']){
$('#cgl-city-filter').val(urlParams['location-city'])
hasUrlFilter=true
}
if(urlParams['location-zip']){
$('#cgl-zip-filter').val(urlParams['location-zip'])
hasUrlFilter=true
}
if(hasUrlFilter) applyFilters()
}}
function validateAndRefreshNonce(callback){
const isCached=CGL.nonce_cached||false
if(isCached){
console.log('🔄 Page cached - fetching fresh nonce...')
}
$.ajax({
url: CGL.ajax_url,
type: 'POST',
data: { action: 'cgl_refresh_nonce' },
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-WP-Nonce-Refresh': '1',
'Cache-Control': 'no-cache, no-store'
},
timeout: 5000,
success: function(res){
if(res.success&&res.data&&res.data.nonce){
const oldNonce=CGL.nonce ? CGL.nonce.substring(0, 10) + '...':'none'
CGL.nonce=res.data.nonce
if(isCached){
console.log('✓ Fresh nonce obtained (page was cached)')
}
if(callback) callback(true)
}else{
console.warn('⚠️ Nonce endpoint returned unexpected response, will try with existing nonce')
if(callback) callback(true)
}},
error: function(xhr, status, error){
console.error('❌ Failed to fetch fresh nonce on page load')
console.error('Status:', xhr.status, '|', status)
if(xhr.status===403){
console.error('⚠️ 403 Forbidden - Check Cloudflare Security Events')
}
if(callback) callback(true)
}})
}
$(document).ready(function(){
validateAndRefreshNonce(function(){
if(window.CGL_CURRENT_COMMUNITY&&window.CGL_CURRENT_COMMUNITY.lat&&window.CGL_CURRENT_COMMUNITY.long){
userLocation={
lat: parseFloat(window.CGL_CURRENT_COMMUNITY.lat),
long: parseFloat(window.CGL_CURRENT_COMMUNITY.long)
}
init()
}else{
init()
}
addResetButton()
})
})
function init(){
$('#cgl-container').html(`
<!-- Filter Bar -->
<div class="cgl-filter-container p-3 mb-3">
<div class="cgl-filter-row">
<input type="text" id="cgl-search" class="form-control" placeholder="Search communities...">
<select id="cgl-city-filter" class="form-select">
<option value="">All Cities</option>
</select>
<select id="cgl-state-filter" class="form-select">
<option value="">All States</option>
</select>
<input type="text" id="cgl-zip-filter" class="form-control" placeholder="Filter by zip code">
<div class="reset-btn-col"></div>
</div>
</div>
<!-- Main Content Area -->
<div class="cgl-content-area">
<div id="cgl-list-container">
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">Communities Near You</h5>
<button id="cgl-precise-location-btn" class="btn btn-outline-primary btn-sm" title="Use precise location">
<i class="bi bi-geo-alt"></i> <span class="d-none d-md-inline">Get Precise Location</span>
</button>
</div>
<div id="cgl-list"></div>
</div>
<div id="cgl-map-container"></div>
</div>
`)
$('#cgl-precise-location-btn').on('click', function(){
const $btn=$(this)
const originalHtml=$btn.html()
$btn.html('<i class="bi bi-arrow-clockwise spin"></i> <span class="d-none d-md-inline">Getting Location...</span>')
$btn.prop('disabled', true)
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(function(pos){
userLocation={ lat: pos.coords.latitude, long: pos.coords.longitude }
sortCommunitiesByDistance()
renderList()
renderMap()
$btn.html('<i class="bi bi-check-circle text-success"></i> <span class="d-none d-md-inline">Location Updated</span>')
setTimeout(()=> {
$btn.html(originalHtml)
$btn.prop('disabled', false)
}, 2000)
}, function(error){
console.error('Geolocation error:', error)
$btn.html('<i class="bi bi-exclamation-triangle text-warning"></i> <span class="d-none d-md-inline">Location Failed</span>')
setTimeout(()=> {
$btn.html(originalHtml)
$btn.prop('disabled', false)
}, 2000)
}, { timeout: 10000, enableHighAccuracy: true })
}else{
$btn.html('<i class="bi bi-x-circle text-danger"></i> <span class="d-none d-md-inline">Not Supported</span>')
setTimeout(()=> {
$btn.html(originalHtml)
$btn.prop('disabled', false)
}, 2000)
}})
$('#cgl-search, #cgl-zip-filter').on('input', debounce(applyFilters))
$('#cgl-city-filter, #cgl-state-filter').on('change', applyFilters)
$('#cgl-list-container').on('scroll', function(){
updateScrollClasses($(this))
})
console.log('⏱️  Staggering requests to avoid rate limiting...')
fetchFilterOptions()
setTimeout(function(){
if(userLocation&&userLocation.lat&&userLocation.long){
fetchCommunities()
}else{
fetchIpLocation(loc=> {
userLocation=loc
setTimeout(function(){
fetchCommunities()
}, 150)
})
}}, 150)
}
function updateScrollClasses($container){
if($container.scrollTop() <=5){
$container.addClass('at-top')
}else{
$container.removeClass('at-top')
}
const scrollHeight=$container[0].scrollHeight
const scrollTop=$container.scrollTop()
const containerHeight=$container.height()
if(scrollHeight - scrollTop - containerHeight <=5){
$container.addClass('at-bottom')
}else{
$container.removeClass('at-bottom')
}}
function fetchFilterOptions(){
ajaxWithRetry({
url: CGL.ajax_url,
type: 'POST',
data: { action: 'cgl_get_filter_options', nonce: CGL.nonce },
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-cache'
},
success: function(res){
if(res.success){
filterOptions=res.data
const citySelect=$('#cgl-city-filter')
filterOptions.cities.forEach(city=> {
citySelect.append(`<option value="${city}">${city}</option>`)
})
const stateSelect=$('#cgl-state-filter')
filterOptions.states.forEach(state=> {
stateSelect.append(`<option value="${state}">${state}</option>`)
})
cglDropdownsReady=true
maybeApplyUrlFilters()
}},
error: function(xhr, status, error){
console.error('Filter options error:', status, error)
}})
}
function showLoader(){
if(!$('#cgl-loader-overlay').length){
$('#cgl-list-container').append(`
<div id="cgl-loader-overlay">
<div class="spinner-border" role="status"></div>
<span>Loading communities...</span>
</div>
`)
}else{
$('#cgl-loader-overlay').show()
}}
function hideLoader(){
$('#cgl-loader-overlay').fadeOut(150)
}
function fetchCommunities(){
showLoader()
$('#cgl-list').html('')
ajaxWithRetry({
url: CGL.ajax_url,
type: 'POST',
data: { action: 'cgl_get_communities', nonce: CGL.nonce },
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-cache'
},
success: function(res){
hideLoader()
if(res.success){
communities=res.data.communities
console.log('Fetched communities:', communities.length)
console.log('User location:', userLocation)
if(userLocation&&userLocation.lat&&userLocation.long){
sortCommunitiesByDistance()
console.log('Communities sorted by distance')
}
renderList()
renderMap()
cglCommunitiesReady=true
maybeApplyUrlFilters()
}else{
console.error('Error fetching communities:', res)
$('#cgl-list').html('<div class="alert alert-danger">Error loading communities.</div>')
}},
error: function(xhr, status, error){
hideLoader()
console.error('AJAX error in fetchCommunities:', {
status: xhr.status,
statusText: status,
error: error,
responseText: xhr.responseText ? xhr.responseText.substring(0, 300):'none'
})
let errorMessage='Error loading communities. Please try again.'
let serverMessage=null
try {
const response=JSON.parse(xhr.responseText)
if(response&&response.data&&response.data.message){
serverMessage=response.data.message
}} catch (e){
}
if(xhr.status===403){
errorMessage=serverMessage||'Security check failed. Please refresh the page to continue.'
console.warn('403 Forbidden - nonce refresh should have already been attempted')
}else if(xhr.status===0){
errorMessage='Network connection error. Please check your internet connection or try refreshing the page.'
console.warn('Network error (status 0) - possibly blocked by browser, CORS issue, or server unreachable')
}else if(xhr.status >=500){
errorMessage='Server error. Please try again later or refresh the page.'
console.error('Server error (5xx) - backend issue')
}
$('#cgl-list').html(`
<div class="alert alert-danger">
<strong>Unable to load communities</strong>
<p class="mb-2">${errorMessage}</p>
<button class="btn btn-sm btn-outline-danger" onclick="window.location.reload()">
<i class="bi bi-arrow-clockwise"></i> Refresh Page
</button>
</div>
`)
}})
}
function getUserLocation(){
return new Promise(resolve=> {
if(navigator.geolocation){
navigator.geolocation.getCurrentPosition(function(pos){
resolve({ lat: pos.coords.latitude, long: pos.coords.longitude })
}, function(){
fetchIpLocation(resolve)
}, { timeout: 5000 })
} else fetchIpLocation(resolve)
})
}
function fetchIpLocation(cb){
console.log('Fetching IP location...')
ajaxWithRetry({
url: CGL.ajax_url,
type: 'POST',
data: { action: 'cgl_get_user_location', nonce: CGL.nonce },
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Cache-Control': 'no-cache'
},
success: function(res){
console.log('IP location response:', res)
if(res.success&&res.data&&res.data.lat&&res.data.long){
const location={
lat: parseFloat(res.data.lat),
long: parseFloat(res.data.long)
}
console.log('IP location found:', location)
cb(location)
}else{
console.log('No IP location found, using fallback')
cb({ lat: 39.8283, long: -98.5795 })
}},
error: function(xhr, status, error){
console.error('IP location AJAX error:', status, error, xhr.responseText)
if(xhr.status===403){
console.warn('403 Forbidden - possibly blocked by Cloudflare, using fallback location')
}else if(xhr.status===0){
console.warn('Network error - possibly blocked by browser or server, using fallback location')
}
cb({ lat: 39.8283, long: -98.5795 })
}})
}
function applyFilters(){
isUserInteraction=false
console.log('Filters applied - bounds filtering enabled')
const text=$('#cgl-search').val().toLowerCase()
const city=$('#cgl-city-filter').val()
const state=$('#cgl-state-filter').val()
const zip=$('#cgl-zip-filter').val()
let filtered=communities.filter(c=> {
if(text&&!(
c.title.toLowerCase().includes(text) ||
c.location.address.toLowerCase().includes(text) ||
c.location.city.toLowerCase().includes(text) ||
c.location.state.toLowerCase().includes(text) ||
c.location.zip_code.toLowerCase().includes(text)
)) return false
if(city&&c.location.city!==city) return false
if(state&&c.location.state!==state) return false
return true
})
if(zipLatLng){
filtered.forEach(c=> {
if(c.location.lat&&c.location.long){
c._distance=getDistanceMiles(
parseFloat(c.location.lat),
parseFloat(c.location.long),
zipLatLng.lat,
zipLatLng.lng
)
}else{
c._distance=null
}})
filtered.sort((a, b)=> {
if(a._distance==null) return 1
if(b._distance==null) return -1
return a._distance - b._distance
})
boundsFilteringEnabled=false
addZipMarker(zipLatLng, $('#cgl-zip-filter').val())
}else{
filtered.forEach(c=> { delete c._distance })
removeZipMarker()
boundsFilteringEnabled=true
}
let finalFiltered=filtered
renderList(finalFiltered)
renderMap(finalFiltered)
}
function sortCommunitiesByDistance(){
if(!userLocation) return
communities.forEach(c=> {
const distanceKm=getDistance(userLocation.lat, userLocation.long, c.location.lat, c.location.long)
c.distance=distanceKm ? distanceKm * 0.621371:null
})
communities.sort((a, b)=> (a.distance ?? 999999) - (b.distance ?? 999999))
}
function renderList(list=null){
let items=(list||communities)
if(window.CGL_CURRENT_COMMUNITY&&window.CGL_CURRENT_COMMUNITY.lat&&window.CGL_CURRENT_COMMUNITY.long){
items=items.filter(c=> {
return String(c.location.lat)!==String(window.CGL_CURRENT_COMMUNITY.lat)||String(c.location.long)!==String(window.CGL_CURRENT_COMMUNITY.long)
})
}
if(items.length===0){
$('#cgl-list').html('<div class="alert alert-info p-3 text-center">No communities found with current filters.</div>')
return
}
const html=items.map((c, index)=> `
<div class="cgl-card" data-id="${c.ID}">
<div class="cgl-card-content">
<h3 class="cgl-card-title">${c.title}</h3>
${c.phone_number ? `<div class="cgl-card-phone"><i class="bi bi-telephone-fill me-2"></i><a href="tel:${c.phone_number.replace(/[^0-9]/g, '')}">${c.phone_number}</a></div>`:''}
<address class="cgl-card-address">
${c.location.address}<br>
${c.location.city}, ${c.location.state} ${c.location.zip_code}
</address>
${(typeof c._distance==='number'||typeof c.distance==='number') ? `<div class="cgl-card-distance mb-2"><i class="bi bi-geo-alt-fill me-1"></i>${(c._distance||c.distance).toFixed(1)} miles away</div>`:''}
<div class="cgl-card-buttons">
<a href="${c.permalink}" class="cgl-card-btn">View Community</a>
<a href="${c.permalink}#contact-us" class="cgl-card-btn schedule-tour">Schedule Tour</a>
</div>
</div>
<div class="cgl-card-img" style="background-image: url('${c.thumbnail ? c.thumbnail:'https://via.placeholder.com/800x600?text=Community'}');">
</div>
</div>
`).join('')
$('#cgl-list').html(html)
setTimeout(function(){
updateScrollClasses($('#cgl-list-container'))
}, 100)
}
function renderMap(list=null){
isMapOperationInProgress=true
isUserInteraction=false
let data=list||communities
if(window.CGL_CURRENT_COMMUNITY&&window.CGL_CURRENT_COMMUNITY.lat&&window.CGL_CURRENT_COMMUNITY.long){
data=data.filter(c=> {
return String(c.location.lat)!==String(window.CGL_CURRENT_COMMUNITY.lat)||String(c.location.long)!==String(window.CGL_CURRENT_COMMUNITY.long)
})
}
const mapEl=document.getElementById('cgl-map-container')
if(!mapEl){
isMapOperationInProgress=false
return
}
if(!window.google||!window.google.maps){
isMapOperationInProgress=false
loadGoogleMaps(()=> renderMap(data))
return
}
if(markers&&markers.length){
markers.forEach(m=> {
if(m&&m.setMap) m.setMap(null)
})
}
markers=[]
if(infoWindow&&infoWindow.close) infoWindow.close()
infoWindow=new google.maps.InfoWindow()
if(!map){
try {
map=new google.maps.Map(mapEl, {
center: userLocation ? { lat: parseFloat(userLocation.lat), lng: parseFloat(userLocation.long) }:{ lat: 39.5, lng: -98.35 },
zoom: (window.CGL_CURRENT_COMMUNITY ? 12:5),
styles: mapStyles,
mapTypeControl: false,
fullscreenControl: true,
streetViewControl: false,
backgroundColor: '#e0e0e0'
})
google.maps.event.addListener(map, 'zoom_changed', function(){
isUserInteraction=true
currentMapBounds=map.getBounds()
console.log('User zoom detected - bounds filtering disabled')
})
google.maps.event.addListener(map, 'dragend', function(){
isUserInteraction=true
currentMapBounds=map.getBounds()
console.log('User pan detected - bounds filtering disabled')
})
google.maps.event.addListener(map, 'bounds_changed', debounce(function(){
if(isUserInteraction&&currentMapBounds){
currentMapBounds=map.getBounds()
updateListOnly()
}}, 500))
} catch (e){
console.error('Error initializing map:', e)
return
}}
const normalDot={
url: 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="10" fill="#0073aa" stroke="#ffffff" stroke-width="2" />
</svg>
`),
scaledSize: new google.maps.Size(24, 24),
origin: new google.maps.Point(0, 0),
anchor: new google.maps.Point(12, 12)
}
const selectedDot={
url: 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 34 34">
<circle cx="17" cy="17" r="10" fill="#0073aa" stroke="#ffffff" stroke-width="2">
<animate attributeName="r" values="10;15;10" dur="1.5s" repeatCount="indefinite" />
<animate attributeName="opacity" values="1;0.8;1" dur="1.5s" repeatCount="indefinite" />
</circle>
<circle cx="17" cy="17" r="15" fill="none" stroke="#0073aa" stroke-width="2" opacity="0.5">
<animate attributeName="r" values="15;20;15" dur="1.5s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.5;0;0.5" dur="1.5s" repeatCount="indefinite" />
</circle>
</svg>
`),
scaledSize: new google.maps.Size(34, 34),
origin: new google.maps.Point(0, 0),
anchor: new google.maps.Point(17, 17)
}
const bounds=new google.maps.LatLngBounds()
let boundsExtended=false
let validMarkers=0
data.forEach(c=> {
if(!c.location.lat||!c.location.long) return
try {
const position={
lat: parseFloat(c.location.lat),
lng: parseFloat(c.location.long)
}
if(isNaN(position.lat)||isNaN(position.lng)) return
const marker=new google.maps.Marker({
position,
map,
title: c.title,
icon: normalDot,
optimized: false
})
marker.addListener('click', function(){
markers.forEach(m=> {
if(m&&m.setIcon) m.setIcon(normalDot)
})
if(marker&&marker.setIcon) marker.setIcon(selectedDot)
$('.cgl-card').removeClass('active')
$(`.cgl-card[data-id="${c.ID}"]`).addClass('active')
const $listContainer=$('#cgl-list-container')
const $card=$(`.cgl-card[data-id="${c.ID}"]`)
if($card.length){
$listContainer.animate({
scrollTop: $card.position().top + $listContainer.scrollTop() - 20
}, 300)
}
if(infoWindow&&infoWindow.setContent){
infoWindow.setContent(`
<div class="map-info-content">
<div class="map-info-img" style="background-image: url('${c.thumbnail ? c.thumbnail:'https://via.placeholder.com/800x600?text=Community'}');">
</div>
<div class="map-info-body">
<h4>${c.title}</h4>
${c.phone_number ? `<div class="map-info-phone mb-2"><i class="bi bi-telephone-fill me-1"></i><a href="tel:${c.phone_number.replace(/[^0-9]/g, '')}">${c.phone_number}</a></div>`:''}
<p>${c.location.address}<br>${c.location.city}, ${c.location.state} ${c.location.zip_code}</p>
${(typeof c._distance==='number'||typeof c.distance==='number') ? `<div class="map-info-distance mb-2"><i class="bi bi-geo-alt-fill me-1"></i>${(c._distance||c.distance).toFixed(1)} miles away</div>`:''}
<div class="map-info-buttons">
<a href="${c.permalink}" class="map-info-btn">View Community</a>
<a href="${c.permalink}#contact-us" class="map-info-btn schedule-tour">Schedule Tour</a>
</div>
</div>
</div>
`)
infoWindow.open(map, marker)
}})
if(infoWindow&&infoWindow.addListener){
google.maps.event.addListener(infoWindow, 'closeclick', function(){
if(marker&&marker.setIcon) marker.setIcon(normalDot)
$('.cgl-card').removeClass('active')
})
}
bounds.extend(position)
boundsExtended=true
validMarkers++
markers.push(marker)
} catch (e){
console.error('Error creating marker:', e)
}})
if(validMarkers > 0){
try {
if(window.CGL_CURRENT_COMMUNITY&&communities.length > 3){
const sorted=[...communities].sort((a, b)=> (a.distance ?? 999999) - (b.distance ?? 999999))
const fitBounds=new google.maps.LatLngBounds()
let fitBoundsExtended=false
for (let i=0; i < 4&&i < sorted.length; i++){
if(sorted[i].location.lat&&sorted[i].location.long){
const lat=parseFloat(sorted[i].location.lat)
const lng=parseFloat(sorted[i].location.long)
if(!isNaN(lat)&&!isNaN(lng)){
fitBounds.extend({ lat, lng })
fitBoundsExtended=true
}}
}
if(fitBoundsExtended&&map&&map.fitBounds){
map.fitBounds(fitBounds)
}}else if(markers.length > 0&&boundsExtended&&map&&map.fitBounds){
if(markers.length===1&&markers[0]&&markers[0].getPosition){
const pos=markers[0].getPosition()
if(pos &&
typeof pos.lat==='function' &&
typeof pos.lng==='function' &&
!isNaN(pos.lat()) &&
!isNaN(pos.lng())
){
map.setCenter({ lat: pos.lat(), lng: pos.lng() })
map.setZoom(14)
}else{
console.warn('Invalid marker position for setCenter:', pos)
}}else{
map.fitBounds(bounds)
}}
} catch (e){
console.error('Error adjusting map viewport:', e)
}}
if(zipLatLng&&data&&data.length > 0){
setTimeout(()=> {
const closestThree=data.slice(0, 3)
zoomToZipAndClosest(zipLatLng, closestThree)
}, 200)
}
if(map&&boundsFilteringEnabled){
setTimeout(()=> {
currentMapBounds=map.getBounds()
console.log('Initial map bounds set for filtering')
isMapOperationInProgress=false
}, 100)
}else{
isMapOperationInProgress=false
}}
function loadGoogleMaps(cb){
if(window.google&&window.google.maps) return cb()
window.initGoogleMaps=cb
const script=document.createElement('script')
script.src=`https://maps.googleapis.com/maps/api/js?key=${CGL.google_maps_api_key}&loading=async&callback=initGoogleMaps`
script.async=true
script.defer=true
document.head.appendChild(script)
}
function getDistance(lat1, lon1, lat2, lon2){
if(!lat1||!lon1||!lat2||!lon2) return null
const R=6371 
const dLat=deg2rad(lat2 - lat1)
const dLon=deg2rad(lon2 - lon1)
const a=Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c=2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}
function deg2rad(deg){
return deg * (Math.PI/180)
}
function getDistanceMiles(lat1, lon1, lat2, lon2){
function toRad(x){ return x * Math.PI / 180 }
const R=3958.8 
const dLat=toRad(lat2 - lat1)
const dLon=toRad(lon2 - lon1)
const a=Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
Math.sin(dLon/2) * Math.sin(dLon/2)
const c=2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}
function geocodeZip(zip, cb){
if(!window.google||!window.google.maps){
loadGoogleMaps(()=> geocodeZip(zip, cb))
return
}
const geocoder=new google.maps.Geocoder()
geocoder.geocode({ address: zip }, function(results, status){
if(status==='OK'&&results[0]){
const loc=results[0].geometry.location
cb({ lat: loc.lat(), lng: loc.lng() })
}else{
cb(null)
}})
}
function addZipMarker(zipLatLng, zipCode){
if(!map||!window.google||!window.google.maps) return
removeZipMarker()
const zipIcon={
url: 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="40" viewBox="0 0 32 40">
<path d="M16 2C9.4 2 4 7.4 4 14c0 8.5 12 22 12 22s12-13.5 12-22c0-6.6-5.4-12-12-12z" fill="#28a745" stroke="#ffffff" stroke-width="2"/>
<circle cx="16" cy="14" r="6" fill="#ffffff"/>
<text x="16" y="36" text-anchor="middle" font-family="Arial, sans-serif" font-size="9" font-weight="bold" fill="#28a745">${zipCode}</text>
</svg>
`),
scaledSize: new google.maps.Size(32, 40),
origin: new google.maps.Point(0, 0),
anchor: new google.maps.Point(16, 32)
}
try {
zipMarker=new google.maps.Marker({
position: { lat: zipLatLng.lat, lng: zipLatLng.lng },
map: map,
title: `Your Location: ${zipCode}`,
icon: zipIcon,
zIndex: 1000,
optimized: false
})
zipMarker.addListener('click', function(){
if(infoWindow){
infoWindow.setContent(`
<div class="map-info-content">
<div class="map-info-body">
<h4>Your Search Location</h4>
<p><strong>Zip Code:</strong> ${zipCode}</p>
<p>Showing communities nearest to this location</p>
</div>
</div>
`)
infoWindow.open(map, zipMarker)
}})
} catch (e){
console.error('Error creating zip marker:', e)
}}
function removeZipMarker(){
if(zipMarker){
zipMarker.setMap(null)
zipMarker=null
}}
function zoomToZipAndClosest(zipLatLng, closestCommunities){
if(!map||!window.google||!window.google.maps) return
try {
const bounds=new google.maps.LatLngBounds()
let validLocations=0
bounds.extend({ lat: zipLatLng.lat, lng: zipLatLng.lng })
validLocations++
console.log('Zip location added to bounds:', zipLatLng)
let includedCommunities=0
closestCommunities.forEach((c, index)=> {
if(c.location.lat&&c.location.long&&includedCommunities < 3){
const lat=parseFloat(c.location.lat)
const lng=parseFloat(c.location.long)
if(!isNaN(lat)&&!isNaN(lng)){
bounds.extend({ lat, lng })
includedCommunities++
validLocations++
console.log(`Community ${index + 1} added to bounds:`, { lat, lng, title: c.title, distance: c._distance })
}}
})
console.log(`Total locations in bounds: ${validLocations}, Communities: ${includedCommunities}`)
if(validLocations > 0){
setTimeout(()=> {
try {
map.fitBounds(bounds)
console.log('Map bounds fitted')
google.maps.event.addListenerOnce(map, 'bounds_changed', function(){
const currentZoom=map.getZoom()
console.log('Current zoom after fitBounds:', currentZoom)
if(currentZoom > 15){
map.setZoom(15)
console.log('Zoom limited to 15')
}else if(currentZoom < 8){
map.setZoom(8)
console.log('Zoom increased to 8')
}})
} catch (boundsError){
console.error('Error fitting bounds:', boundsError)
map.setCenter({ lat: zipLatLng.lat, lng: zipLatLng.lng })
map.setZoom(10)
}}, 100)
}else{
map.setCenter({ lat: zipLatLng.lat, lng: zipLatLng.lng })
map.setZoom(10)
console.log('Fallback: centered on zip location')
}} catch (e){
console.error('Error zooming to zip and closest communities:', e)
}}
function addResetButton(){
if($('#cgl-reset-filters').length) return
const resetBtnHtml=`
<button id="cgl-reset-filters" class="btn btn-outline-secondary d-none d-md-inline-flex align-items-center justify-content-center" style="min-width:48px;">
<i class="bi bi-arrow-clockwise"></i>
<span class="ms-2 d-none d-lg-inline">Reset Filters</span>
</button>
<button id="cgl-reset-filters-mobile" class="btn btn-outline-secondary d-inline-flex d-md-none align-items-center justify-content-center" style="width:40px;height:40px;padding:0;">
<i class="bi bi-arrow-clockwise"></i>
</button>
`
$('.cgl-filter-row .reset-btn-col').html(resetBtnHtml)
$('#cgl-reset-filters, #cgl-reset-filters-mobile').on('click', function(){
$('#cgl-search').val('')
$('#cgl-city-filter').val('')
$('#cgl-state-filter').val('')
$('#cgl-zip-filter').val('')
zipLatLng=null
removeZipMarker()
boundsFilteringEnabled=true
applyFilters()
})
}
const onZipInput=debounce(function(){
let zip=$('#cgl-zip-filter').val().replace(/[^0-9]/g, '')
if(zip.length > 5) zip=zip.slice(0, 5)
$('#cgl-zip-filter').val(zip)
if(zip.length!==5){
zipLatLng=null
removeZipMarker()
applyFilters()
return
}
geocodeZip(zip, function(result){
zipLatLng=result
applyFilters()
})
}, 500)
$(document).on('input', '#cgl-zip-filter', function(){
let val=$(this).val().replace(/[^0-9]/g, '')
if(val.length > 5) val=val.slice(0, 5)
$(this).val(val)
})
$(document).on('input', '#cgl-zip-filter', onZipInput)
function showFullscreenMap(){
if(!map) return
const overlay=$(`
<div id="cgl-fullscreen-overlay" style="
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: white;
z-index: 9999;
display: flex;
flex-direction: column;
">
<div style="
background: #f8f9fa;
padding: 10px 20px;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
">
<h3 style="margin: 0; font-size: 18px;">Community Map</h3>
<button id="cgl-close-fullscreen" style="
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 5px;
">&times;</button>
</div>
<div id="cgl-fullscreen-map" style="flex: 1;"></div>
</div>
`)
$('body').append(overlay)
const fullscreenMap=new google.maps.Map(document.getElementById('cgl-fullscreen-map'), {
center: map.getCenter(),
zoom: map.getZoom(),
styles: mapStyles,
mapTypeControl: true,
fullscreenControl: false,
streetViewControl: true,
backgroundColor: '#e0e0e0'
})
markers.forEach(marker=> {
if(marker&&marker.getPosition){
const newMarker=new google.maps.Marker({
position: marker.getPosition(),
map: fullscreenMap,
title: marker.getTitle(),
icon: marker.getIcon()
})
newMarker.addListener('click', function(){
const position=newMarker.getPosition()
const community=communities.find(c=> {
return Math.abs(parseFloat(c.location.lat) - position.lat()) < 0.0001 &&
Math.abs(parseFloat(c.location.long) - position.lng()) < 0.0001
})
if(community&&infoWindow){
infoWindow.setContent(`
<div class="map-info-content">
<div class="map-info-img" style="background-image: url('${community.thumbnail ? community.thumbnail:'https://via.placeholder.com/800x600?text=Community'}');">
</div>
<div class="map-info-body">
<h4>${community.title}</h4>
${community.phone_number ? `<div class="map-info-phone mb-2"><i class="bi bi-telephone-fill me-1"></i><a href="tel:${community.phone_number.replace(/[^0-9]/g, '')}">${community.phone_number}</a></div>`:''}
<p>${community.location.address}<br>${community.location.city}, ${community.location.state} ${community.location.zip_code}</p>
${(typeof community._distance==='number'||typeof community.distance==='number') ? `<div class="map-info-distance mb-2"><i class="bi bi-geo-alt-fill me-1"></i>${(community._distance||community.distance).toFixed(1)} miles away</div>`:''}
<div class="map-info-buttons">
<a href="${community.permalink}" class="map-info-btn">View Community</a>
<a href="${community.permalink}#contact-us" class="map-info-btn schedule-tour">Schedule Tour</a>
</div>
</div>
</div>
`)
infoWindow.open(fullscreenMap, newMarker)
}})
}})
$('#cgl-close-fullscreen').on('click', function(){
$('#cgl-fullscreen-overlay').remove()
})
$(document).on('keydown.fullscreen', function(e){
if(e.keyCode===27){
$('#cgl-fullscreen-overlay').remove()
$(document).off('keydown.fullscreen')
}})
}})(jQuery);