|
|
|
import React, { useState, useEffect } from 'react'; |
|
import { UmbrellaIcon, SnowflakeIcon, SunIcon, WindIcon, AlertTriangleIcon } from '../components/icons'; |
|
import { useLocalStorage } from '../hooks/useLocalStorage'; |
|
import type { BonsaiTree } from '../types'; |
|
import Spinner from '../components/Spinner'; |
|
|
|
interface WeatherData { |
|
daily: { |
|
time: string[]; |
|
weathercode: number[]; |
|
temperature_2m_max: number[]; |
|
temperature_2m_min: number[]; |
|
windspeed_10m_max: number[]; |
|
}; |
|
} |
|
|
|
const WeatherShieldView: React.FC = () => { |
|
const [trees] = useLocalStorage<BonsaiTree[]>('bonsai-diary-trees', []); |
|
const [weather, setWeather] = useState<WeatherData | null>(null); |
|
const [isLoading, setIsLoading] = useState(true); |
|
const [error, setError] = useState<string>(''); |
|
|
|
useEffect(() => { |
|
const fetchWeather = async () => { |
|
if (trees.length === 0) { |
|
setIsLoading(false); |
|
return; |
|
} |
|
|
|
|
|
const primaryLocation = trees[0].location; |
|
if (!primaryLocation) { |
|
setError("No location set for your trees. Please add a location in 'My Garden'."); |
|
setIsLoading(false); |
|
return; |
|
} |
|
|
|
try { |
|
|
|
const geoResponse = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(primaryLocation)}&count=1`); |
|
const geoData = await geoResponse.json(); |
|
|
|
if (!geoData.results || geoData.results.length === 0) { |
|
throw new Error(`Could not find location: ${primaryLocation}`); |
|
} |
|
const { latitude, longitude } = geoData.results[0]; |
|
|
|
|
|
const weatherResponse = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min,windspeed_10m_max&timezone=auto`); |
|
const weatherData = await weatherResponse.json(); |
|
setWeather(weatherData); |
|
|
|
} catch (err) { |
|
if (err instanceof Error) { |
|
setError(`Failed to fetch weather data: ${err.message}`); |
|
} else { |
|
setError("An unknown error occurred while fetching weather data."); |
|
} |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
fetchWeather(); |
|
}, [trees]); |
|
|
|
const getWeatherIcon = (code: number) => { |
|
if (code >= 200 && code < 600) return <UmbrellaIcon className="w-8 h-8 text-blue-500" />; |
|
if (code >= 600 && code < 700) return <SnowflakeIcon className="w-8 h-8 text-cyan-400" />; |
|
if (code === 800) return <SunIcon className="w-8 h-8 text-yellow-500" />; |
|
return <SunIcon className="w-8 h-8 text-stone-500" />; |
|
}; |
|
|
|
const generateAlertsForTree = (tree: BonsaiTree) => { |
|
if (!weather || !tree.protectionProfile || !tree.protectionProfile.alertsEnabled) return []; |
|
|
|
const alerts: string[] = []; |
|
weather.daily.time.slice(0, 5).forEach((_, i) => { |
|
const dayMinTemp = weather.daily.temperature_2m_min[i]; |
|
const dayMaxTemp = weather.daily.temperature_2m_max[i]; |
|
const dayMaxWind = weather.daily.windspeed_10m_max[i]; |
|
|
|
if (dayMinTemp < tree.protectionProfile!.minTempC) { |
|
alerts.push(`Frost Alert for ${new Date(weather.daily.time[i]).toLocaleDateString([], {weekday: 'short'})}: Low of ${dayMinTemp.toFixed(0)}°C`); |
|
} |
|
if (dayMaxTemp > tree.protectionProfile!.maxTempC) { |
|
alerts.push(`Heat Alert for ${new Date(weather.daily.time[i]).toLocaleDateString([], {weekday: 'short'})}: High of ${dayMaxTemp.toFixed(0)}°C`); |
|
} |
|
if (dayMaxWind > tree.protectionProfile!.maxWindKph) { |
|
alerts.push(`Wind Alert for ${new Date(weather.daily.time[i]).toLocaleDateString([], {weekday: 'short'})}: Gusts up to ${dayMaxWind.toFixed(0)} km/h`); |
|
} |
|
}); |
|
return alerts; |
|
} |
|
|
|
|
|
return ( |
|
<div className="space-y-8 max-w-6xl mx-auto"> |
|
<header className="text-center"> |
|
<h2 className="text-3xl font-bold tracking-tight text-stone-900 sm:text-4xl flex items-center justify-center gap-3"> |
|
<UmbrellaIcon className="w-8 h-8 text-blue-600" /> |
|
Weather Shield |
|
</h2> |
|
<p className="mt-4 text-lg leading-8 text-stone-600"> |
|
Proactive weather alerts for your bonsai collection, based on their individual needs. |
|
</p> |
|
</header> |
|
|
|
{isLoading ? <Spinner text="Fetching local forecast..."/> : |
|
error ? <p className="text-center text-red-600 p-4 bg-red-50 rounded-md">{error}</p> : |
|
!weather ? <p className="text-center text-stone-600">Add trees to your garden to see weather alerts.</p> : |
|
( |
|
<> |
|
<div className="bg-white p-6 rounded-xl shadow-lg border border-stone-200"> |
|
<h3 className="text-xl font-bold text-stone-800 mb-4">5-Day Forecast for {trees[0]?.location || 'Your Area'}</h3> |
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-center"> |
|
{weather.daily.time.slice(0, 5).map((time, i) => ( |
|
<div key={time} className="bg-stone-50 p-4 rounded-lg border border-stone-100"> |
|
<p className="font-bold text-stone-900">{new Date(time).toLocaleDateString([], { weekday: 'short', day: 'numeric'})}</p> |
|
<div className="my-2">{getWeatherIcon(weather.daily.weathercode[i])}</div> |
|
<p className="font-semibold text-blue-600">{weather.daily.temperature_2m_min[i].toFixed(0)}°C</p> |
|
<p className="font-semibold text-red-600">{weather.daily.temperature_2m_max[i].toFixed(0)}°C</p> |
|
<p className="text-sm text-stone-500 mt-1 flex items-center justify-center gap-1"><WindIcon className="w-4 h-4" />{weather.daily.windspeed_10m_max[i].toFixed(0)} km/h</p> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
|
|
<div className="space-y-4"> |
|
<h3 className="text-xl font-bold text-stone-800">Tree Protection Status</h3> |
|
{trees.map(tree => { |
|
const alerts = generateAlertsForTree(tree); |
|
const hasAlerts = alerts.length > 0; |
|
return ( |
|
<div key={tree.id} className={`bg-white p-4 rounded-lg shadow-md border-l-4 ${hasAlerts ? 'border-red-500' : 'border-green-500'}`}> |
|
<h4 className="font-bold text-lg text-stone-900">{tree.name} <span className="text-sm font-normal text-stone-500">({tree.species})</span></h4> |
|
{hasAlerts ? ( |
|
<div className="mt-2 space-y-1"> |
|
{alerts.map((alert, i) => ( |
|
<div key={i} className="flex items-center gap-2 text-sm text-red-700 font-semibold"> |
|
<AlertTriangleIcon className="w-4 h-4 flex-shrink-0" /> |
|
<p>{alert}</p> |
|
</div> |
|
))} |
|
</div> |
|
) : ( |
|
<p className="mt-1 text-sm text-green-700 font-medium">No weather threats detected in the next 5 days.</p> |
|
)} |
|
</div> |
|
); |
|
})} |
|
</div> |
|
</> |
|
) |
|
} |
|
</div> |
|
); |
|
}; |
|
|
|
export default WeatherShieldView; |