import React, {useState} from 'react';
// FishTime.jsx
// Single-file React component (Tailwind-ready) that checks weather for a ZIP code
// and gives a simple "Fishing Score" (0-100) and recommendation.
// Requirements (developer / user):
// - This example uses OpenWeatherMap's Geocoding + One Call API.
// - Create an account at https://openweathermap.org/ and get an API key.
// - Add your API key to the REACT_APP_OWM_KEY environment variable or paste into the UI (demo only).
// How it works (brief):
// 1. Geocode ZIP -> lat/lon using OWM Geocoding API
// 2. Fetch 7-day daily forecast (One Call) for lat/lon
// 3. Compute a fishing score from weather factors (precipitation chance, wind, temp, cloud cover, moon phase)
// 4. Display scores and short tips
export default function FishTime() {
const [zip, setZip] = useState('');
const [apiKey, setApiKey] = useState(process.env.REACT_APP_OWM_KEY || '');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [forecast, setForecast] = useState(null);
// scoring weights (tweakable)
const WEIGHTS = {
precip: 0.30, // probability of precipitation (POP)
wind: 0.25, // wind speed
temp: 0.20, // temperature suitability
clouds: 0.15, // cloud cover
moon: 0.10 // moon phase
};
async function handleCheck(e) {
e.preventDefault();
setError(null);
setForecast(null);
if (!zip || !/^[0-9]{5}$/.test(zip)) return setError('Please enter a valid 5-digit ZIP code.');
if (!apiKey) return setError('Please add your OpenWeatherMap API key (top-right).');
setLoading(true);
try {
// 1) Geocode ZIP -> lat/lon
const geoRes = await fetch(`https://api.openweathermap.org/geo/1.0/zip?zip=${zip},US&appid=${apiKey}`);
if (!geoRes.ok) throw new Error('Failed to geocode ZIP.');
const geo = await geoRes.json();
const {lat, lon, name} = geo;
// 2) One Call (daily) - use exclude to save bandwidth
const onecall = await fetch(`https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&exclude=minutely,hourly,alerts&units=imperial&appid=${apiKey}`);
if (!onecall.ok) throw new Error('Failed to fetch forecast.');
const data = await onecall.json();
// We'll analyze the daily array (today + 7 days)
const daily = data.daily || [];
setForecast({place: name || `${lat.toFixed(2)},${lon.toFixed(2)}`, daily});
} catch (err) {
console.error(err);
setError(err.message || 'Unknown error');
} finally {
setLoading(false);
}
}
function scoreDay(day) {
// day is an object from One Call daily
// Values: day.pop (probability of precipitation 0-1), day.wind_speed (mph), day.temp.day (F), day.clouds (0-100), day.moon_phase (0-1)
// Convert each to a 0-1 where 1 is perfect fishing conditions, then build weighted sum.
// precipitation: best when pop is low
const pop = day.pop ?? 0; // 0..1
const precipScore = Math.max(0, 1 - pop); // 1 when 0 pop, 0 when 1 pop
// wind: best when calm. We'll assume <8 mph ideal, 8-20 okay, >20 bad
const w = day.wind_speed ?? 0;
let windScore = 1;
if (w <= 8) windScore = 1;
else if (w <= 20) windScore = 1 - (w - 8) / 12 * 0.75; // degrade to 0.25
else windScore = 0.1;
// temp: fish species differ; we'll assume comfortable 50-75 F ideal.
const t = day.temp?.day ?? (day.temp || {}).day ?? 60;
let tempScore = 0;
if (t >= 50 && t <= 75) tempScore = 1;
else if (t >= 40 && t < 50) tempScore = 0.7;
else if (t > 75 && t <= 85) tempScore = 0.6;
else tempScore = 0.3;
// clouds: some anglers like overcast (fish feed more). We'll give higher score for partial/overcast.
const c = (day.clouds ?? 50) / 100; // 0..1
// ideal: 0.4 - 0.9 (40% - 90%). Very clear or very dark not ideal.
let cloudScore = 0.5;
if (c >= 0.4 && c <= 0.9) cloudScore = 1;
else cloudScore = Math.max(0.2, 1 - Math.abs(c - 0.65) * 1.5);
// moon phase: 0 new, 0.5 full. Some anglers prefer new or full (feeding). We'll treat 0 or 0.5 as slightly better.
const m = day.moon_phase ?? 0.5; // 0..1
// score peaks at 0 and 0.5. compute distance to nearest of {0,0.5,1}
const distToNew = Math.min(Math.abs(m - 0), Math.abs(m - 1));
const distToFull = Math.abs(m - 0.5);
const moonFavor = Math.min(distToNew, distToFull); // 0 is best
const moonScore = 1 - Math.min(1, moonFavor * 2.0); // when moonFavor=0 =>1, when 0.5=>0
// combine
const combined = (precipScore * WEIGHTS.precip) + (windScore * WEIGHTS.wind) + (tempScore * WEIGHTS.temp) + (cloudScore * WEIGHTS.clouds) + (moonScore * WEIGHTS.moon);
// normalize 0..1 -> 0..100
return Math.round(combined * 100);
}
function niceTip(score) {
if (score >= 80) return 'Great time to fish — calm, comfortable, low chance of rain.';
if (score >= 60) return 'Good conditions — bring light layers and polarized sunglasses.';
if (score >= 40) return 'Mixed conditions — consider sheltered spots or slower presentations.';
return 'Poor conditions — heavy wind/precipitation or extreme temps. Consider rescheduling.';
}
return (
{error &&
)}
);
}
/* README (short)
1) Create a new React app (Vite or CRA). Place this component as src/FishTime.jsx and import it in App.jsx.
2) Install Tailwind or adjust classes to your CSS system.
3) Set REACT_APP_OWM_KEY in your environment, or paste your key in the UI for demo.
4) Run the app and enter a US ZIP code to see the 7-day fishing outlook.
Notes & improvements:
- Add caching of geocode results and daily barometer trends for better accuracy.
- Add species presets (bass, trout, walleye) with different temp/wind preferences.
- Add a small backend to securely store the API key and handle rate limits.
*/
FishTime — Is it a good time to fish?
Demo uses OpenWeatherMap • add your API key
{error}
}
{!forecast && (
Enter a ZIP and API key then press Check to see a 7-day fishing outlook.
)}
{forecast && (
Location: {forecast.place}
{forecast.daily.slice(0, 7).map((d, i) => {
const score = scoreDay(d);
const date = new Date((d.dt || 0) * 1000);
const dayLabel = date.toLocaleDateString(undefined, {weekday: 'short', month: 'short', day: 'numeric'});
return (
);
})}
{dayLabel}
{Math.round(d.temp.day)}°F • {Math.round((d.wind_speed||0))} mph
= 70 ? 'text-green-600' : score >= 50 ? 'text-amber-600' : 'text-red-600'}`}>{score}%
Fishing Score
= 70 ? 'bg-green-500' : score >= 50 ? 'bg-amber-500' : 'bg-red-500'}`}>
{niceTip(score)}
POP: {Math.round((d.pop||0)*100)}% • Clouds: {d.clouds || '--'}% • Moon phase: {d.moon_phase?.toFixed(2) || '--'}
How score is calculated: precipitation chance, wind, temperature, cloud cover, and moon phase are combined into a 0–100 score. You can customize weights in the code.