| 1 |
// src/App.js - Complete working app with fixes |
| 2 |
import React, { useState } from 'react'; |
| 3 |
import './App.css'; |
| 4 |
import { getRoastsForLocation } from './services/roastService'; |
| 5 |
|
| 6 |
function App() { |
| 7 |
const [location, setLocation] = useState(null); |
| 8 |
const [locationObj, setLocationObj] = useState(null); // Store the structured object separately |
| 9 |
const [currentRoast, setCurrentRoast] = useState(null); |
| 10 |
const [loading, setLoading] = useState(false); |
| 11 |
const [error, setError] = useState(null); |
| 12 |
const [manualInput, setManualInput] = useState(''); |
| 13 |
|
| 14 |
const detectLocation = async () => { |
| 15 |
setLoading(true); |
| 16 |
setError(null); |
| 17 |
|
| 18 |
try { |
| 19 |
// Try IP geolocation first (more reliable for getting city/state names) |
| 20 |
try { |
| 21 |
const response = await fetch('https://ipapi.co/json/'); |
| 22 |
const data = await response.json(); |
| 23 |
|
| 24 |
// Build location object from API response |
| 25 |
const locationObj = { |
| 26 |
city: data.city || null, |
| 27 |
state: data.region || null, |
| 28 |
country: data.country_name || null |
| 29 |
}; |
| 30 |
|
| 31 |
// Create display string |
| 32 |
let locationStr = ''; |
| 33 |
if (data.city) { |
| 34 |
locationStr = data.city; |
| 35 |
if (data.region) { |
| 36 |
locationStr += `, ${data.region}`; |
| 37 |
} |
| 38 |
if (data.country_name) { |
| 39 |
locationStr += `, ${data.country_name}`; |
| 40 |
} |
| 41 |
} else if (data.region) { |
| 42 |
locationStr = data.region; |
| 43 |
if (data.country_name) { |
| 44 |
locationStr += `, ${data.country_name}`; |
| 45 |
} |
| 46 |
} else if (data.country_name) { |
| 47 |
locationStr = data.country_name; |
| 48 |
} |
| 49 |
|
| 50 |
setLocation(locationStr || 'Unknown'); |
| 51 |
setLocationObj(locationObj); // Store the structured object |
| 52 |
handleLocationFound(locationObj); |
| 53 |
} catch (err) { |
| 54 |
setError("Couldn't detect location. Try entering manually!"); |
| 55 |
setLoading(false); |
| 56 |
} |
| 57 |
} catch (err) { |
| 58 |
setError("Couldn't detect location. Try entering manually!"); |
| 59 |
setLoading(false); |
| 60 |
} |
| 61 |
}; |
| 62 |
|
| 63 |
const handleLocationFound = (loc) => { |
| 64 |
// Parse the location string if needed |
| 65 |
let parsedLocationObj = { city: null, state: null, country: null }; |
| 66 |
|
| 67 |
if (typeof loc === 'string') { |
| 68 |
// If it's a string, try to parse it |
| 69 |
const parts = loc.split(',').map(p => p.trim()); |
| 70 |
|
| 71 |
if (parts.length === 1) { |
| 72 |
// Could be city, state, or country |
| 73 |
parsedLocationObj.city = parts[0]; |
| 74 |
} else if (parts.length === 2) { |
| 75 |
// Likely "City, State" or "City, Country" |
| 76 |
parsedLocationObj.city = parts[0]; |
| 77 |
parsedLocationObj.state = parts[1]; |
| 78 |
} else if (parts.length >= 3) { |
| 79 |
// "City, State, Country" format |
| 80 |
parsedLocationObj.city = parts[0]; |
| 81 |
parsedLocationObj.state = parts[1]; |
| 82 |
parsedLocationObj.country = parts[2]; |
| 83 |
} |
| 84 |
} else if (typeof loc === 'object') { |
| 85 |
// If it's already an object, use it directly |
| 86 |
parsedLocationObj = loc; |
| 87 |
} |
| 88 |
|
| 89 |
// Store the parsed object for reuse |
| 90 |
setLocationObj(parsedLocationObj); |
| 91 |
|
| 92 |
// Get roasts using the service |
| 93 |
const roasts = getRoastsForLocation(parsedLocationObj); |
| 94 |
|
| 95 |
if (roasts.length > 0) { |
| 96 |
const randomRoast = roasts[Math.floor(Math.random() * roasts.length)]; |
| 97 |
setCurrentRoast(randomRoast); |
| 98 |
} else { |
| 99 |
setCurrentRoast("Your location is so irrelevant, even our roast database gave up."); |
| 100 |
} |
| 101 |
|
| 102 |
setLoading(false); |
| 103 |
}; |
| 104 |
|
| 105 |
const handleManualSubmit = (e) => { |
| 106 |
e.preventDefault(); |
| 107 |
if (manualInput.trim()) { |
| 108 |
const inputLocation = manualInput.trim(); |
| 109 |
setLocation(inputLocation); |
| 110 |
handleLocationFound(inputLocation); |
| 111 |
setManualInput(''); |
| 112 |
} |
| 113 |
}; |
| 114 |
|
| 115 |
const getAnotherRoast = () => { |
| 116 |
if (locationObj) { |
| 117 |
// Use the stored location object instead of parsing the display string |
| 118 |
const roasts = getRoastsForLocation(locationObj); |
| 119 |
if (roasts.length > 0) { |
| 120 |
const randomRoast = roasts[Math.floor(Math.random() * roasts.length)]; |
| 121 |
setCurrentRoast(randomRoast); |
| 122 |
} |
| 123 |
} |
| 124 |
}; |
| 125 |
|
| 126 |
return ( |
| 127 |
<div className="App"> |
| 128 |
<div className="container"> |
| 129 |
<div className="logo">🔥</div> |
| 130 |
<h1>LocalRoast</h1> |
| 131 |
<p className="subtitle">Get absolutely torched based on where you're from</p> |
| 132 |
|
| 133 |
<div className="roast-box"> |
| 134 |
{loading ? ( |
| 135 |
<div className="loading"></div> |
| 136 |
) : currentRoast ? ( |
| 137 |
<p className="roast-text">{currentRoast}</p> |
| 138 |
) : ( |
| 139 |
<p className="roast-text"> |
| 140 |
Ready to get roasted? Hit the button if you can handle it... |
| 141 |
</p> |
| 142 |
)} |
| 143 |
</div> |
| 144 |
|
| 145 |
{location && ( |
| 146 |
<div className="location-display">📍 {location}</div> |
| 147 |
)} |
| 148 |
|
| 149 |
{!currentRoast && !loading && ( |
| 150 |
<button onClick={detectLocation} className="primary-button"> |
| 151 |
Roast Me! 🔥 |
| 152 |
</button> |
| 153 |
)} |
| 154 |
|
| 155 |
{currentRoast && ( |
| 156 |
<div className="button-group"> |
| 157 |
<button onClick={getAnotherRoast} className="primary-button"> |
| 158 |
Hit me again! 😤 |
| 159 |
</button> |
| 160 |
</div> |
| 161 |
)} |
| 162 |
|
| 163 |
<div className="manual-input"> |
| 164 |
<p>Can't detect location? Enter it manually:</p> |
| 165 |
<form onSubmit={handleManualSubmit}> |
| 166 |
<input |
| 167 |
type="text" |
| 168 |
value={manualInput} |
| 169 |
onChange={(e) => setManualInput(e.target.value)} |
| 170 |
placeholder="Enter city or region" |
| 171 |
disabled={loading} |
| 172 |
/> |
| 173 |
<button type="submit" disabled={loading || !manualInput.trim()}> |
| 174 |
Roast! 🎯 |
| 175 |
</button> |
| 176 |
</form> |
| 177 |
</div> |
| 178 |
|
| 179 |
{error && <div className="error">{error}</div>} |
| 180 |
|
| 181 |
<div className="footer"> |
| 182 |
<p>Made with 🔥 and tears | Not responsible for hurt feelings</p> |
| 183 |
</div> |
| 184 |
</div> |
| 185 |
</div> |
| 186 |
); |
| 187 |
} |
| 188 |
|
| 189 |
export default App; |