import { useState, useEffect, useCallback } from 'react'; // Default location: Phoenix, AZ (fallback if all else fails) const DEFAULT_LOCATION = { lat: 33.4484, lng: -112.0740, city: 'Phoenix', state: 'AZ' }; const LOCATION_STORAGE_KEY = 'findagram_location'; const SESSION_ID_KEY = 'findagram_session_id'; const API_BASE_URL = process.env.REACT_APP_API_URL || ''; /** * Get or create session ID */ function getSessionId() { let sessionId = sessionStorage.getItem(SESSION_ID_KEY); if (!sessionId) { sessionId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; sessionStorage.setItem(SESSION_ID_KEY, sessionId); } return sessionId; } /** * Get cached location from sessionStorage */ function getCachedLocation() { try { const cached = sessionStorage.getItem(LOCATION_STORAGE_KEY); if (cached) { return JSON.parse(cached); } } catch (err) { console.error('Error reading cached location:', err); } return null; } /** * Save location to sessionStorage */ function cacheLocation(location) { try { sessionStorage.setItem(LOCATION_STORAGE_KEY, JSON.stringify(location)); } catch (err) { console.error('Error caching location:', err); } } /** * Track visitor and get location from our backend API * This logs the visit for analytics and returns location from IP */ async function trackVisitorAndGetLocation() { try { const response = await fetch(`${API_BASE_URL}/api/v1/visitor/track`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ domain: 'findagram.co', page_path: window.location.pathname, session_id: getSessionId(), referrer: document.referrer || null, }), }); const data = await response.json(); if (data.success && data.location) { return { lat: data.location.lat, lng: data.location.lng, city: data.location.city, state: data.location.state, stateCode: data.location.stateCode, source: 'api' }; } } catch (err) { console.error('Visitor tracking error:', err); } return null; } /** * Custom hook for getting user's geolocation * * @param {Object} options * @param {boolean} options.autoRequest - Whether to request location automatically on mount * @param {boolean} options.useIPFallback - Whether to use IP geolocation as fallback (default: true) * @param {Object} options.defaultLocation - Default location if all methods fail * @returns {Object} { location, loading, error, requestLocation, hasPermission } */ export function useGeolocation(options = {}) { const { autoRequest = false, useIPFallback = true, defaultLocation = DEFAULT_LOCATION } = options; const [location, setLocation] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [hasPermission, setHasPermission] = useState(null); const [locationSource, setLocationSource] = useState(null); // 'gps', 'ip', or 'default' // Try IP geolocation first (no permission needed) const getIPLocation = useCallback(async () => { if (!useIPFallback) return null; const ipLoc = await getLocationFromIP(); if (ipLoc) { setLocation(ipLoc); setLocationSource('ip'); return ipLoc; } return null; }, [useIPFallback]); // Request precise GPS location (requires permission) const requestLocation = useCallback(async () => { setLoading(true); setError(null); // First try browser geolocation if (navigator.geolocation) { return new Promise(async (resolve) => { navigator.geolocation.getCurrentPosition( (position) => { const { latitude, longitude } = position.coords; const loc = { lat: latitude, lng: longitude, source: 'gps' }; setLocation(loc); setLocationSource('gps'); setHasPermission(true); setLoading(false); resolve(loc); }, async (err) => { console.error('Geolocation error:', err); if (err.code === err.PERMISSION_DENIED) { setHasPermission(false); } // Fall back to IP geolocation if (useIPFallback) { const ipLoc = await getLocationFromIP(); if (ipLoc) { setLocation(ipLoc); setLocationSource('ip'); setLoading(false); resolve(ipLoc); return; } } // Last resort: default location setError('Unable to determine location'); setLocation(defaultLocation); setLocationSource('default'); setLoading(false); resolve(defaultLocation); }, { enableHighAccuracy: false, timeout: 5000, maximumAge: 600000 // Cache for 10 minutes } ); }); } // No browser geolocation, try IP if (useIPFallback) { const ipLoc = await getLocationFromIP(); if (ipLoc) { setLocation(ipLoc); setLocationSource('ip'); setLoading(false); return ipLoc; } } // Fallback to default setLocation(defaultLocation); setLocationSource('default'); setLoading(false); return defaultLocation; }, [defaultLocation, useIPFallback]); // Auto-request location on mount if enabled useEffect(() => { if (autoRequest) { const init = async () => { // Check for cached location first const cached = getCachedLocation(); if (cached) { setLocation(cached); setLocationSource(cached.source || 'api'); setLoading(false); return; } setLoading(true); // Track visitor and get location from our backend API const apiLoc = await trackVisitorAndGetLocation(); if (apiLoc) { setLocation(apiLoc); setLocationSource('api'); cacheLocation(apiLoc); // Save for session } else { // Fallback to default setLocation(defaultLocation); setLocationSource('default'); } setLoading(false); }; init(); } }, [autoRequest, defaultLocation]); return { location, loading, error, requestLocation, hasPermission, locationSource, isDefault: locationSource === 'default', isFromIP: locationSource === 'ip', isFromGPS: locationSource === 'gps' }; } /** * Calculate distance between two points using Haversine formula * * @param {number} lat1 - First point latitude * @param {number} lng1 - First point longitude * @param {number} lat2 - Second point latitude * @param {number} lng2 - Second point longitude * @returns {number} Distance in miles */ export function calculateDistance(lat1, lng1, lat2, lng2) { const R = 3959; // Earth's radius in miles const dLat = toRad(lat2 - lat1); const dLng = toRad(lng2 - lng1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } function toRad(deg) { return deg * (Math.PI / 180); } /** * Sort items by distance from a location * * @param {Array} items - Array of items with location data * @param {Object} userLocation - User's location { lat, lng } * @param {Function} getItemLocation - Function to extract lat/lng from item * @returns {Array} Items sorted by distance with distance property added */ export function sortByDistance(items, userLocation, getItemLocation = (item) => item.location) { if (!userLocation || !items?.length) return items; return items .map(item => { const itemLoc = getItemLocation(item); if (!itemLoc?.latitude || !itemLoc?.longitude) { return { ...item, distance: null }; } const distance = calculateDistance( userLocation.lat, userLocation.lng, itemLoc.latitude, itemLoc.longitude ); return { ...item, distance: Math.round(distance * 10) / 10 }; }) .sort((a, b) => { if (a.distance === null) return 1; if (b.distance === null) return -1; return a.distance - b.distance; }); } /** * Filter items within a radius from user location * * @param {Array} items - Array of items with location data * @param {Object} userLocation - User's location { lat, lng } * @param {number} radiusMiles - Max distance in miles * @param {Function} getItemLocation - Function to extract lat/lng from item * @returns {Array} Items within radius, sorted by distance */ export function filterByRadius(items, userLocation, radiusMiles = 50, getItemLocation = (item) => item.location) { const sorted = sortByDistance(items, userLocation, getItemLocation); return sorted.filter(item => item.distance !== null && item.distance <= radiusMiles); } export default useGeolocation;