File size: 8,371 Bytes
be02369 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
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;
}
// For simplicity, use the location of the first tree as the primary location
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 {
// Simple geocoding using a public API
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];
// Fetch weather forecast
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" />; // Rain
if (code >= 600 && code < 700) return <SnowflakeIcon className="w-8 h-8 text-cyan-400" />; // Snow
if (code === 800) return <SunIcon className="w-8 h-8 text-yellow-500" />; // Clear
return <SunIcon className="w-8 h-8 text-stone-500" />; // Default/Clouds
};
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; |