JavaScript · 8820 bytes Raw Blame History
1 // src/services/locationService.js
2 import axios from 'axios';
3
4 // API keys from environment variables (optional)
5 const IPAPI_KEY = process.env.REACT_APP_IPAPI_KEY;
6 const OPENCAGE_KEY = process.env.REACT_APP_OPENCAGE_KEY;
7
8 /**
9 * Detect user's location using IP geolocation
10 * Uses ipapi.co free tier (no API key required for limited requests)
11 */
12 export const detectLocationByIP = async () => {
13 try {
14 // ipapi.co free tier - 1000 requests per day without API key
15 const response = await axios.get('https://ipapi.co/json/');
16
17 return {
18 city: response.data.city,
19 state: response.data.region,
20 country: response.data.country_name,
21 countryCode: response.data.country_code,
22 latitude: response.data.latitude,
23 longitude: response.data.longitude,
24 ip: response.data.ip
25 };
26 } catch (error) {
27 console.error('IP geolocation failed:', error);
28
29 // Fallback to another free service
30 try {
31 const fallbackResponse = await axios.get('https://api.ipify.org?format=json');
32 const ip = fallbackResponse.data.ip;
33
34 // Try ip-api.com as fallback (free, no key needed)
35 const geoResponse = await axios.get(`http://ip-api.com/json/${ip}`);
36
37 return {
38 city: geoResponse.data.city,
39 state: geoResponse.data.regionName,
40 country: geoResponse.data.country,
41 countryCode: geoResponse.data.countryCode,
42 latitude: geoResponse.data.lat,
43 longitude: geoResponse.data.lon,
44 ip: ip
45 };
46 } catch (fallbackError) {
47 console.error('Fallback IP geolocation also failed:', fallbackError);
48 throw new Error('Unable to detect location by IP');
49 }
50 }
51 };
52
53 /**
54 * Reverse geocode coordinates to get location details
55 * @param {number} latitude
56 * @param {number} longitude
57 */
58 export const reverseGeocode = async (latitude, longitude) => {
59 try {
60 // If we have an OpenCage API key, use it for better accuracy
61 if (OPENCAGE_KEY) {
62 const response = await axios.get(
63 `https://api.opencagedata.com/geocode/v1/json?q=${latitude}+${longitude}&key=${OPENCAGE_KEY}`
64 );
65
66 if (response.data.results && response.data.results.length > 0) {
67 const result = response.data.results[0];
68 const components = result.components;
69
70 return {
71 city: components.city || components.town || components.village || components.municipality,
72 state: components.state || components.county || components.state_district,
73 country: components.country,
74 countryCode: components.country_code?.toUpperCase(),
75 latitude,
76 longitude,
77 formatted: result.formatted
78 };
79 }
80 }
81
82 // Fallback: Use Nominatim (OpenStreetMap) - free, no key required
83 const response = await axios.get(
84 `https://nominatim.openstreetmap.org/reverse?format=json&lat=${latitude}&lon=${longitude}`,
85 {
86 headers: {
87 'User-Agent': 'LocalRoast/1.0' // Required by Nominatim
88 }
89 }
90 );
91
92 const address = response.data.address;
93
94 return {
95 city: address.city || address.town || address.village || address.municipality,
96 state: address.state || address.county || address.state_district,
97 country: address.country,
98 countryCode: address.country_code?.toUpperCase(),
99 latitude,
100 longitude,
101 formatted: response.data.display_name
102 };
103
104 } catch (error) {
105 console.error('Reverse geocoding failed:', error);
106 // If reverse geocoding fails, fall back to IP location
107 return detectLocationByIP();
108 }
109 };
110
111 /**
112 * Main function to detect user location
113 * Tries browser geolocation first, then falls back to IP
114 */
115 export const detectLocation = async () => {
116 return new Promise((resolve, reject) => {
117 // Check if geolocation is available
118 if ('geolocation' in navigator) {
119 navigator.geolocation.getCurrentPosition(
120 async (position) => {
121 try {
122 // Got coordinates, now reverse geocode them
123 const location = await reverseGeocode(
124 position.coords.latitude,
125 position.coords.longitude
126 );
127 resolve(location);
128 } catch (error) {
129 // Reverse geocoding failed, try IP location
130 try {
131 const ipLocation = await detectLocationByIP();
132 resolve(ipLocation);
133 } catch (ipError) {
134 reject(new Error('Unable to detect location'));
135 }
136 }
137 },
138 async (error) => {
139 // User denied geolocation or it failed
140 console.log('Browser geolocation failed:', error.message);
141
142 // Fall back to IP geolocation
143 try {
144 const ipLocation = await detectLocationByIP();
145 resolve(ipLocation);
146 } catch (ipError) {
147 reject(new Error('Unable to detect location'));
148 }
149 },
150 {
151 timeout: 5000, // 5 second timeout
152 enableHighAccuracy: false // Don't need high accuracy for roasting
153 }
154 );
155 } else {
156 // Browser doesn't support geolocation, use IP
157 detectLocationByIP()
158 .then(resolve)
159 .catch(() => reject(new Error('Unable to detect location')));
160 }
161 });
162 };
163
164 /**
165 * Parse manual location input into structured format
166 * @param {string} input - User's manual location input
167 */
168 export const parseManualLocation = (input) => {
169 if (!input || typeof input !== 'string') {
170 return null;
171 }
172
173 // Split by commas and trim whitespace
174 const parts = input.split(',').map(part => part.trim()).filter(Boolean);
175
176 if (parts.length === 0) {
177 return null;
178 }
179
180 // Try to intelligently parse the input
181 let city, state, country;
182
183 if (parts.length === 1) {
184 // Could be city, state, or country
185 const location = parts[0];
186
187 // Check if it's a known state/country
188 if (isKnownState(location) || isKnownCountry(location)) {
189 if (isKnownCountry(location)) {
190 country = location;
191 } else {
192 state = location;
193 country = 'United States'; // Assume US for state names
194 }
195 } else {
196 // Assume it's a city
197 city = location;
198 }
199 } else if (parts.length === 2) {
200 // Likely "City, State" or "City, Country"
201 city = parts[0];
202
203 if (isKnownCountry(parts[1])) {
204 country = parts[1];
205 } else {
206 state = parts[1];
207 // Assume US if state is provided without country
208 country = 'United States';
209 }
210 } else if (parts.length >= 3) {
211 // "City, State, Country" format
212 city = parts[0];
213 state = parts[1];
214 country = parts[2];
215 }
216
217 return {
218 city,
219 state,
220 country,
221 formatted: input
222 };
223 };
224
225 // Helper functions to identify known locations
226 const isKnownState = (location) => {
227 const states = [
228 'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado',
229 'Connecticut', 'Delaware', 'Florida', 'Georgia', 'Hawaii', 'Idaho',
230 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana',
231 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota',
232 'Mississippi', 'Missouri', 'Montana', 'Nebraska', 'Nevada',
233 'New Hampshire', 'New Jersey', 'New Mexico', 'New York',
234 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon',
235 'Pennsylvania', 'Rhode Island', 'South Carolina', 'South Dakota',
236 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virginia', 'Washington',
237 'West Virginia', 'Wisconsin', 'Wyoming',
238 // Common abbreviations
239 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'FL', 'GA',
240 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD',
241 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ',
242 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC',
243 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY',
244 // Canadian provinces
245 'Ontario', 'Quebec', 'British Columbia', 'Alberta', 'Manitoba',
246 'Saskatchewan', 'Nova Scotia', 'New Brunswick', 'Newfoundland',
247 'Prince Edward Island', 'Northwest Territories', 'Yukon', 'Nunavut'
248 ];
249
250 return states.some(state =>
251 state.toLowerCase() === location.toLowerCase()
252 );
253 };
254
255 const isKnownCountry = (location) => {
256 const countries = [
257 'United States', 'USA', 'US', 'America',
258 'Canada', 'United Kingdom', 'UK', 'Britain', 'England',
259 'Australia', 'New Zealand', 'Ireland', 'Scotland', 'Wales',
260 'France', 'Germany', 'Spain', 'Italy', 'Netherlands',
261 'Mexico', 'Brazil', 'Argentina', 'Japan', 'China', 'India'
262 ];
263
264 return countries.some(country =>
265 country.toLowerCase() === location.toLowerCase()
266 );
267 };
268
269 // Export all functions
270 export default {
271 detectLocation,
272 detectLocationByIP,
273 reverseGeocode,
274 parseManualLocation
275 };