Yuki / views /WeatherShieldView.tsx
Severian's picture
Upload 43 files
be02369 verified
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;