| 1 |
// src/App.js - Complete working app in one file for quick testing |
| 2 |
import React, { useState } from 'react'; |
| 3 |
import './App.css'; |
| 4 |
|
| 5 |
// Mini roast database for testing |
| 6 |
const roastDatabase = { |
| 7 |
'New York': [ |
| 8 |
"NYC? Let me guess, you've mentioned you're from New York within 5 minutes of every conversation you've ever had.", |
| 9 |
"From NYC? Cool, how's that superiority complex and vitamin D deficiency working out?", |
| 10 |
"New York City: Where everyone walks fast to nowhere important and calls it ambition." |
| 11 |
], |
| 12 |
'California': [ |
| 13 |
"California? How's that $8 gas and $15 avocado toast treating you?", |
| 14 |
"Oh, you're from California? Which wellness trend are you pretending changed your life this week?", |
| 15 |
"California: Come for the weather, stay because you can't afford to leave." |
| 16 |
], |
| 17 |
'Texas': [ |
| 18 |
"Texas? Everything's bigger there, especially the egos and the power grid failures.", |
| 19 |
"From Texas? Let me guess, you've already mentioned how big your state is three times today.", |
| 20 |
"Texas: Where 105°F is 'nice weather' and a light dusting of snow shuts down civilization." |
| 21 |
], |
| 22 |
'Virginia': [ |
| 23 |
"Virginia? The state that can't decide if it's the South or just South of Maryland.", |
| 24 |
"From Virginia? Home of 'Virginia is for Lovers' - because you need a slogan when you have no personality.", |
| 25 |
"Virginia: Where Northern Virginia pretends it's DC and the rest pretends it's still 1865.", |
| 26 |
"Oh, Virginia? The state whose biggest achievement is being close to somewhere important.", |
| 27 |
"Virginia: Where everyone works for the government but swears they're a 'small government conservative'." |
| 28 |
], |
| 29 |
'default': [ |
| 30 |
"Your location is so irrelevant, even Google Maps just shrugs.", |
| 31 |
"From there? I'd roast your hometown but it would require me to care about it first.", |
| 32 |
"Your area is so forgettable, even this roast generator had nothing prepared." |
| 33 |
] |
| 34 |
}; |
| 35 |
|
| 36 |
function App() { |
| 37 |
const [location, setLocation] = useState(null); |
| 38 |
const [currentRoast, setCurrentRoast] = useState(null); |
| 39 |
const [loading, setLoading] = useState(false); |
| 40 |
const [error, setError] = useState(null); |
| 41 |
const [manualInput, setManualInput] = useState(''); |
| 42 |
|
| 43 |
const detectLocation = async () => { |
| 44 |
setLoading(true); |
| 45 |
setError(null); |
| 46 |
|
| 47 |
try { |
| 48 |
// Try IP geolocation first (more reliable for getting city/state names) |
| 49 |
try { |
| 50 |
const response = await fetch('https://ipapi.co/json/'); |
| 51 |
const data = await response.json(); |
| 52 |
|
| 53 |
// Build location string from most specific to least specific |
| 54 |
let locationStr = ''; |
| 55 |
if (data.city) { |
| 56 |
locationStr = data.city; |
| 57 |
if (data.region) { |
| 58 |
locationStr = data.region; // Use state/region as primary identifier |
| 59 |
} |
| 60 |
} else if (data.region) { |
| 61 |
locationStr = data.region; |
| 62 |
} else if (data.country_name) { |
| 63 |
locationStr = data.country_name; |
| 64 |
} |
| 65 |
|
| 66 |
handleLocationFound(locationStr || 'Unknown'); |
| 67 |
} catch (err) { |
| 68 |
setError("Couldn't detect location. Try entering manually!"); |
| 69 |
setLoading(false); |
| 70 |
} |
| 71 |
} catch (err) { |
| 72 |
setError("Couldn't detect location. Try entering manually!"); |
| 73 |
setLoading(false); |
| 74 |
} |
| 75 |
}; |
| 76 |
|
| 77 |
const handleLocationFound = (loc) => { |
| 78 |
setLocation(loc); |
| 79 |
|
| 80 |
// Normalize the location for database lookup |
| 81 |
const normalizedLoc = loc.trim() |
| 82 |
.split(' ') |
| 83 |
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) |
| 84 |
.join(' '); |
| 85 |
|
| 86 |
// Try exact match first, then try just the first word (for "Los Angeles, California" -> "California") |
| 87 |
let roasts = roastDatabase[normalizedLoc]; |
| 88 |
|
| 89 |
if (!roasts && loc.includes(',')) { |
| 90 |
// Try the state/country part after the comma |
| 91 |
const parts = loc.split(','); |
| 92 |
const statePart = parts[1].trim() |
| 93 |
.split(' ') |
| 94 |
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) |
| 95 |
.join(' '); |
| 96 |
roasts = roastDatabase[statePart]; |
| 97 |
} |
| 98 |
|
| 99 |
if (!roasts) { |
| 100 |
// Try some common variations |
| 101 |
const variations = { |
| 102 |
'Ny': 'New York', |
| 103 |
'Nyc': 'New York', |
| 104 |
'La': 'California', |
| 105 |
'Los Angeles': 'California', |
| 106 |
'San Francisco': 'California', |
| 107 |
'Sf': 'California', |
| 108 |
'Tx': 'Texas', |
| 109 |
'Va': 'Virginia', |
| 110 |
'Cali': 'California' |
| 111 |
}; |
| 112 |
|
| 113 |
const variation = variations[normalizedLoc]; |
| 114 |
if (variation) { |
| 115 |
roasts = roastDatabase[variation]; |
| 116 |
} |
| 117 |
} |
| 118 |
|
| 119 |
// Default to generic roasts if nothing found |
| 120 |
roasts = roasts || roastDatabase.default; |
| 121 |
|
| 122 |
const randomRoast = roasts[Math.floor(Math.random() * roasts.length)]; |
| 123 |
setCurrentRoast(randomRoast); |
| 124 |
setLoading(false); |
| 125 |
}; |
| 126 |
|
| 127 |
const handleManualSubmit = (e) => { |
| 128 |
e.preventDefault(); |
| 129 |
if (manualInput.trim()) { |
| 130 |
handleLocationFound(manualInput.trim()); |
| 131 |
setManualInput(''); |
| 132 |
} |
| 133 |
}; |
| 134 |
|
| 135 |
const getAnotherRoast = () => { |
| 136 |
if (location) { |
| 137 |
const roasts = roastDatabase[location] || roastDatabase.default; |
| 138 |
const randomRoast = roasts[Math.floor(Math.random() * roasts.length)]; |
| 139 |
setCurrentRoast(randomRoast); |
| 140 |
} |
| 141 |
}; |
| 142 |
|
| 143 |
return ( |
| 144 |
<div className="App"> |
| 145 |
<div className="container"> |
| 146 |
<div className="logo">🔥</div> |
| 147 |
<h1>LocalRoast</h1> |
| 148 |
<p className="subtitle">Get absolutely torched based on where you're from</p> |
| 149 |
|
| 150 |
<div className="roast-box"> |
| 151 |
{loading ? ( |
| 152 |
<div className="loading"></div> |
| 153 |
) : currentRoast ? ( |
| 154 |
<p className="roast-text">{currentRoast}</p> |
| 155 |
) : ( |
| 156 |
<p className="roast-text"> |
| 157 |
Ready to get roasted? Hit the button if you can handle it... |
| 158 |
</p> |
| 159 |
)} |
| 160 |
</div> |
| 161 |
|
| 162 |
{location && ( |
| 163 |
<div className="location-display">📍 {location}</div> |
| 164 |
)} |
| 165 |
|
| 166 |
{!currentRoast && !loading && ( |
| 167 |
<button onClick={detectLocation} className="primary-button"> |
| 168 |
Roast Me! 🔥 |
| 169 |
</button> |
| 170 |
)} |
| 171 |
|
| 172 |
{currentRoast && ( |
| 173 |
<div className="button-group"> |
| 174 |
<button onClick={getAnotherRoast} className="primary-button"> |
| 175 |
Hit me again! 😤 |
| 176 |
</button> |
| 177 |
</div> |
| 178 |
)} |
| 179 |
|
| 180 |
<div className="manual-input"> |
| 181 |
<p>Can't detect location? Enter it manually:</p> |
| 182 |
<form onSubmit={handleManualSubmit}> |
| 183 |
<input |
| 184 |
type="text" |
| 185 |
value={manualInput} |
| 186 |
onChange={(e) => setManualInput(e.target.value)} |
| 187 |
placeholder="Enter city or region" |
| 188 |
disabled={loading} |
| 189 |
/> |
| 190 |
<button type="submit" disabled={loading || !manualInput.trim()}> |
| 191 |
Roast! 🎯 |
| 192 |
</button> |
| 193 |
</form> |
| 194 |
</div> |
| 195 |
|
| 196 |
{error && <div className="error">{error}</div>} |
| 197 |
|
| 198 |
<div className="footer"> |
| 199 |
<p>Made with 🔥 and tears | Not responsible for hurt feelings</p> |
| 200 |
</div> |
| 201 |
</div> |
| 202 |
</div> |
| 203 |
); |
| 204 |
} |
| 205 |
|
| 206 |
export default App; |