import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; import * as dataSync from '../api/dataSync'; // UUID generator that works in non-secure contexts (HTTP) function generateUUID(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } export interface SavedLocation { id: string; name: string; latitude: number; longitude: number; } export interface LocationState { latitude: number; longitude: number; name?: string; } interface LocationContextType { location: LocationState | null; savedLocations: SavedLocation[]; setLocation: (location: LocationState | null) => void; saveLocation: (name: string, lat: number, lng: number) => void; deleteLocation: (id: string) => void; clearLocation: () => void; } const LocationContext = createContext(null); const STORAGE_KEY = 'feeld_locations'; const CURRENT_LOCATION_KEY = 'feeld_current_location'; export function LocationProvider({ children }: { children: ReactNode }) { const [location, setLocationState] = useState(() => { const saved = localStorage.getItem(CURRENT_LOCATION_KEY); return saved ? JSON.parse(saved) : null; }); const [savedLocations, setSavedLocations] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY); return saved ? JSON.parse(saved) : []; }); const [hasSyncedFromServer, setHasSyncedFromServer] = useState(false); // Fetch from server on mount and merge with localStorage useEffect(() => { const syncFromServer = async () => { try { const serverData = await dataSync.getAllFromServer(); if (serverData) { // Merge saved locations from server if (serverData.savedLocations && serverData.savedLocations.length > 0) { setSavedLocations(prev => { const merged = [...serverData.savedLocations]; for (const local of prev) { if (!merged.some((s: SavedLocation) => s.id === local.id)) { merged.push(local); } } // Update localStorage with merged data localStorage.setItem(STORAGE_KEY, JSON.stringify(merged)); return merged; }); } // Restore current location if we don't have one if (serverData.currentLocation && !location) { setLocationState(serverData.currentLocation); localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(serverData.currentLocation)); } // Restore custom location if we don't have one if (serverData.customLocation && !location) { setLocationState(serverData.customLocation); localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(serverData.customLocation)); } } } catch (e) { console.error('Failed to sync locations from server:', e); } finally { setHasSyncedFromServer(true); } }; syncFromServer(); }, []); useEffect(() => { if (location) { localStorage.setItem(CURRENT_LOCATION_KEY, JSON.stringify(location)); dataSync.setData('currentLocation', location); } else { localStorage.removeItem(CURRENT_LOCATION_KEY); } }, [location]); // Only sync to server after initial load from server is complete useEffect(() => { if (!hasSyncedFromServer) return; localStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations)); // Sync to server dataSync.setData('savedLocations', savedLocations); // Also sync to dedicated saved-locations endpoint for rotation cron fetch('/api/saved-locations', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ locations: savedLocations }), }).catch(() => {}); // Best-effort }, [savedLocations, hasSyncedFromServer]); const setLocation = (loc: LocationState | null) => { setLocationState(loc); // Sync to server if (loc) { dataSync.setData('customLocation', loc); } }; const saveLocation = (name: string, latitude: number, longitude: number) => { const newLocation: SavedLocation = { id: generateUUID(), name, latitude, longitude, }; setSavedLocations((prev) => [...prev, newLocation]); }; const deleteLocation = (id: string) => { setSavedLocations((prev) => prev.filter((loc) => loc.id !== id)); }; const clearLocation = () => { setLocationState(null); }; return ( {children} ); } export function useLocation() { const context = useContext(LocationContext); if (!context) { throw new Error('useLocation must be used within a LocationProvider'); } return context; } // Geocoding helper using OpenStreetMap Nominatim (free, no API key needed) export async function geocodeAddress(address: string): Promise<{ lat: number; lng: number; displayName: string } | null> { try { const response = await fetch( `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}&limit=1`, { headers: { 'User-Agent': 'FeeldWebApp/1.0', }, } ); const data = await response.json(); if (data.length > 0) { return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon), displayName: data[0].display_name, }; } return null; } catch (error) { console.error('Geocoding error:', error); return null; } }