/** * Store Finder Component * * Modern store locator with advanced features: * - Real-time search with autocomplete * - Distance-based filtering * - Interactive map view * - List/Map toggle * - Current location detection */ import { useState, useEffect, useCallback, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { Search, MapPin, Navigation, Phone, Mail, Globe, Clock, Filter, ChevronRight, Map, List, Star, Car, Wifi, Zap, Coffee } from 'lucide-react'; interface Location { id: string; name: string; address: string; city: string; state: string; postalCode: string; country: string; phone: string; email: string; website: string; latitude: number; longitude: number; distance?: number; hours?: any; amenities?: { wifi?: boolean; parking?: boolean; wheelchair?: boolean; evCharging?: boolean; delivery?: boolean; }; rating?: number; reviewCount?: number; } interface StoreFinderProps { maxResults?: number; defaultRadius?: number; showMap?: boolean; showFilters?: boolean; categories?: string[]; mapHeight?: string; } const StoreFinder: React.FC = ({ maxResults = 10, defaultRadius = 25, showMap = true, showFilters = true, categories = [], mapHeight = '400px' }) => { const [locations, setLocations] = useState([]); const [filteredLocations, setFilteredLocations] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [selectedLocation, setSelectedLocation] = useState(null); const [viewMode, setViewMode] = useState<'list' | 'map'>(showMap ? 'map' : 'list'); const [loading, setLoading] = useState(false); const [userLocation, setUserLocation] = useState<{ lat: number; lng: number } | null>(null); const [radius, setRadius] = useState(defaultRadius); const [filters, setFilters] = useState({ openNow: false, hasParking: false, hasWifi: false, hasEvCharging: false, wheelchairAccessible: false }); const [suggestions, setSuggestions] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const searchInputRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef([]); useEffect(() => { loadLocations(); detectUserLocation(); }, []); useEffect(() => { filterLocations(); }, [searchQuery, radius, filters, locations, userLocation]); const loadLocations = async () => { setLoading(true); try { const response = await apiFetch({ path: '/prorank-seo/v1/local-seo/locations', method: 'GET' }); setLocations(response.data || []); } catch (error) { console.error('Failed to load locations:', error); } finally { setLoading(false); } }; const detectUserLocation = () => { if ('geolocation' in navigator) { navigator.geolocation.getCurrentPosition( (position) => { setUserLocation({ lat: position.coords.latitude, lng: position.coords.longitude }); }, () => { // Ignore geolocation failures (user may deny permission). } ); } }; const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { const R = 3959; // Earth's radius in miles const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; }; const filterLocations = () => { let filtered = [...locations]; // Search filter if (searchQuery) { const query = searchQuery.toLowerCase(); filtered = filtered.filter(loc => loc.name.toLowerCase().includes(query) || loc.address.toLowerCase().includes(query) || loc.city.toLowerCase().includes(query) || loc.state.toLowerCase().includes(query) || loc.postalCode.includes(query) ); } // Distance filter if (userLocation) { filtered = filtered.map(loc => ({ ...loc, distance: calculateDistance( userLocation.lat, userLocation.lng, loc.latitude || 0, loc.longitude || 0 ) })); filtered = filtered.filter(loc => (loc.distance || 0) <= radius); filtered.sort((a, b) => (a.distance || 0) - (b.distance || 0)); } // Amenity filters if (filters.hasParking) { filtered = filtered.filter(loc => loc.amenities?.parking); } if (filters.hasWifi) { filtered = filtered.filter(loc => loc.amenities?.wifi); } if (filters.hasEvCharging) { filtered = filtered.filter(loc => loc.amenities?.evCharging); } if (filters.wheelchairAccessible) { filtered = filtered.filter(loc => loc.amenities?.wheelchair); } // Open now filter if (filters.openNow) { filtered = filtered.filter(loc => isOpenNow(loc)); } // Limit results filtered = filtered.slice(0, maxResults); setFilteredLocations(filtered); }; const isOpenNow = (location: Location): boolean => { if (!location.hours) return false; const now = new Date(); const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const currentDay = dayNames[now.getDay()]; const currentTime = now.getHours() * 60 + now.getMinutes(); const todayHours = location.hours[currentDay]; if (!todayHours || todayHours === 'Closed') return false; // Parse hours like "9:00 AM - 5:00 PM" const match = todayHours.match(/(\d{1,2}):(\d{2})\s*(AM|PM)\s*-\s*(\d{1,2}):(\d{2})\s*(AM|PM)/i); if (!match) return false; let openHour = parseInt(match[1]); const openMin = parseInt(match[2]); if (match[3].toUpperCase() === 'PM' && openHour !== 12) openHour += 12; if (match[3].toUpperCase() === 'AM' && openHour === 12) openHour = 0; let closeHour = parseInt(match[4]); const closeMin = parseInt(match[5]); if (match[6].toUpperCase() === 'PM' && closeHour !== 12) closeHour += 12; if (match[6].toUpperCase() === 'AM' && closeHour === 12) closeHour = 0; const openTime = openHour * 60 + openMin; const closeTime = closeHour * 60 + closeMin; return currentTime >= openTime && currentTime <= closeTime; }; const handleSearchChange = (e: React.ChangeEvent) => { const value = e.target.value; setSearchQuery(value); // Generate suggestions if (value.length > 2) { const cities = [...new Set(locations.map(l => l.city))]; const states = [...new Set(locations.map(l => l.state))]; const allSuggestions = [...cities, ...states].filter(s => s.toLowerCase().includes(value.toLowerCase()) ); setSuggestions(allSuggestions.slice(0, 5)); setShowSuggestions(true); } else { setShowSuggestions(false); } }; const handleSuggestionClick = (suggestion: string) => { setSearchQuery(suggestion); setShowSuggestions(false); }; const getDirections = (location: Location) => { const destination = encodeURIComponent( `${location.address}, ${location.city}, ${location.state} ${location.postalCode}` ); if (userLocation) { const origin = `${userLocation.lat},${userLocation.lng}`; window.open( `https://www.google.com/maps/dir/${origin}/${destination}`, '_blank' ); } else { window.open( `https://www.google.com/maps/search/${destination}`, '_blank' ); } }; const LocationCard = ({ location }: { location: Location }) => (
setSelectedLocation(location)} >

{location.name}

{location.distance && ( {location.distance.toFixed(1)} mi )}

{location.address}

{location.city}, {location.state} {location.postalCode}

{location.phone && ( )} {location.rating && (
{location.rating.toFixed(1)} ({location.reviewCount} reviews)
)}
{location.amenities?.wifi && ( )} {location.amenities?.parking && ( )} {location.amenities?.evCharging && ( )}
{location.website && ( e.stopPropagation()} > Website )}
); return (
{/* Search Bar */}
{userLocation && ( )}
{showSuggestions && suggestions.length > 0 && (
{suggestions.map((suggestion, index) => (
handleSuggestionClick(suggestion)} > {suggestion}
))}
)}
{/* Filters */} {showFilters && (
)} {/* View Mode Toggle */} {showMap && (
)} {/* Results */}
{loading ? (
{__('Loading locations...', 'prorank-seo')}
) : filteredLocations.length === 0 ? (

{__('No locations found matching your criteria', 'prorank-seo')}

) : viewMode === 'list' ? (
{filteredLocations.map((location) => ( ))}
) : (
{selectedLocation && (
)}
)}
); }; export default StoreFinder;