Add weather popover on click

This commit is contained in:
2024-12-27 13:15:00 -05:00
parent b4d3048b9b
commit 6cc74de9a1

View File

@@ -16,6 +16,14 @@ import {
Haze, Haze,
Moon, Moon,
Monitor, Monitor,
Wind,
Droplets,
ThermometerSun,
ThermometerSnowflake,
Sunrise,
Sunset,
AlertTriangle,
Umbrella,
} from "lucide-react"; } from "lucide-react";
import { useScroll } from "@/contexts/ScrollContext"; import { useScroll } from "@/contexts/ScrollContext";
import { useTheme } from "@/components/theme/ThemeProvider"; import { useTheme } from "@/components/theme/ThemeProvider";
@@ -26,6 +34,12 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from "@/components/ui/tooltip"; } from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Alert, AlertDescription } from "@/components/ui/alert";
const CraftsIcon = () => ( const CraftsIcon = () => (
<svg viewBox="0 0 2687 3338" className="w-6 h-6" aria-hidden="true"> <svg viewBox="0 0 2687 3338" className="w-6 h-6" aria-hidden="true">
@@ -43,6 +57,7 @@ const CraftsIcon = () => (
const Header = () => { const Header = () => {
const [currentTime, setCurrentTime] = useState(new Date()); const [currentTime, setCurrentTime] = useState(new Date());
const [weather, setWeather] = useState(null); const [weather, setWeather] = useState(null);
const [forecast, setForecast] = useState(null);
const { isStuck } = useScroll(); const { isStuck } = useScroll();
const { theme, systemTheme, toggleTheme, setTheme } = useTheme(); const { theme, systemTheme, toggleTheme, setTheme } = useTheme();
@@ -54,69 +69,206 @@ const Header = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const fetchWeather = async () => { const fetchWeatherData = async () => {
try { try {
const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY; const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY;
const response = await fetch( const [weatherResponse, forecastResponse] = await Promise.all([
fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial` `https://api.openweathermap.org/data/2.5/weather?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial`
); ),
const data = await response.json(); fetch(
setWeather(data); `https://api.openweathermap.org/data/2.5/forecast?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial`
)
]);
const weatherData = await weatherResponse.json();
const forecastData = await forecastResponse.json();
setWeather(weatherData);
// Process forecast data to get daily forecasts
const dailyForecasts = forecastData.list.reduce((acc, item) => {
const date = new Date(item.dt * 1000).toLocaleDateString();
if (!acc[date]) {
acc[date] = {
...item,
precipitation: item.rain?.['3h'] || item.snow?.['3h'] || 0,
pop: item.pop * 100
};
}
return acc;
}, {});
setForecast(Object.values(dailyForecasts).slice(0, 5));
} catch (error) { } catch (error) {
console.error("Error fetching weather:", error); console.error("Error fetching weather:", error);
} }
}; };
fetchWeather(); fetchWeatherData();
const weatherTimer = setInterval(fetchWeather, 300000); const weatherTimer = setInterval(fetchWeatherData, 300000);
return () => clearInterval(weatherTimer); return () => clearInterval(weatherTimer);
}, []); }, []);
const getWeatherIcon = (weatherCode, currentTime) => { const getWeatherIcon = (weatherCode, currentTime, small = false) => {
if (!weatherCode) return <CircleAlert className="w-6 h-6 text-red-500" />; if (!weatherCode) return <CircleAlert className={cn(small ? "w-6 h-6" : "w-7 h-7", "text-red-500")} />;
const code = parseInt(weatherCode, 10); const code = parseInt(weatherCode, 10);
const iconProps = small ? "w-6 h-6" : "w-7 h-7";
switch (true) { switch (true) {
case code >= 200 && code < 300: case code >= 200 && code < 300:
return <CloudLightning className="w-7 h-7 text-gray-500" />; return <CloudLightning className={cn(iconProps, "text-gray-700")} />;
case code >= 300 && code < 500: case code >= 300 && code < 500:
return <CloudDrizzle className="w-7 h-7 text-blue-400" />; return <CloudDrizzle className={cn(iconProps, "text-blue-600")} />;
case code >= 500 && code < 600: case code >= 500 && code < 600:
return <CloudRain className="w-7 h-7 text-blue-400" />; return <CloudRain className={cn(iconProps, "text-blue-600")} />;
case code >= 600 && code < 700: case code >= 600 && code < 700:
return <CloudSnow className="w-7 h-7 text-blue-200" />; return <CloudSnow className={cn(iconProps, "text-blue-400")} />;
case code >= 700 && code < 721: case code >= 700 && code < 721:
return <CloudFog className="w-7 h-7 text-gray-400" />; return <CloudFog className={cn(iconProps, "text-gray-600")} />;
case code === 721: case code === 721:
return <Haze className="w-7 h-7 text-gray-500" />; return <Haze className={cn(iconProps, "text-gray-700")} />;
case code >= 722 && code < 781: case code >= 722 && code < 781:
return <CloudFog className="w-7 h-7 text-gray-400" />; return <CloudFog className={cn(iconProps, "text-gray-600")} />;
case code === 781: case code === 781:
return <Tornado className="w-7 h-7 text-gray-500" />; return <Tornado className={cn(iconProps, "text-gray-700")} />;
case code === 800: case code === 800:
return currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? ( return currentTime.getHours() >= 6 && currentTime.getHours() < 18 ? (
<Sun className="w-7 h-7 text-yellow-500" /> <Sun className={cn(iconProps, "text-yellow-500")} />
) : ( ) : (
<Moon className="w-7 h-7 text-gray-300" /> <Moon className={cn(iconProps, "text-gray-300")} />
); );
case code >= 800 && code < 803: case code >= 800 && code < 803:
return <CloudSun className="w-7 h-7 text-gray-400" />; return <CloudSun className={cn(iconProps, "text-gray-600")} />;
case code >= 803: case code >= 803:
return <Cloud className="w-7 h-7 text-gray-400" />; return <Cloud className={cn(iconProps, "text-gray-600")} />;
default: default:
return <CircleAlert className="w-6 h-6 text-red-500" />; return <CircleAlert className={cn(iconProps, "text-red-500")} />;
} }
}; };
const formatTime = (date) => { const formatTime = (timestamp) => {
const hours = date.getHours(); if (!timestamp) return '--:--';
const minutes = String(date.getMinutes()).padStart(2, "0"); const date = new Date(timestamp * 1000);
const seconds = String(date.getSeconds()).padStart(2, "0"); return date.toLocaleTimeString('en-US', {
const period = hours >= 12 ? "PM" : "AM"; hour: 'numeric',
const displayHours = hours % 12 || 12; minute: '2-digit',
return `${displayHours}:${minutes}:${seconds} ${period}`; hour12: true
});
}; };
const WeatherDetails = () => (
<div className="space-y-4 p-3">
<div className="grid grid-cols-3 gap-2">
<Card className="p-2">
<div className="flex items-center gap-1">
<ThermometerSun className="w-5 h-5 text-orange-500" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">High</span>
<span className="text-sm font-bold">{Math.round(weather.main.temp_max)}°F</span>
</div>
</div>
</Card>
<Card className="p-2">
<div className="flex items-center gap-1">
<ThermometerSnowflake className="w-5 h-5 text-blue-500" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">Low</span>
<span className="text-sm font-bold">{Math.round(weather.main.temp_min)}°F</span>
</div>
</div>
</Card>
<Card className="p-2">
<div className="flex items-center gap-1">
<Droplets className="w-5 h-5 text-blue-400" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">Humidity</span>
<span className="text-sm font-bold">{weather.main.humidity}%</span>
</div>
</div>
</Card>
<Card className="p-2">
<div className="flex items-center gap-1">
<Wind className="w-5 h-5 text-gray-500" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">Wind</span>
<span className="text-sm font-bold">{Math.round(weather.wind.speed)} mph</span>
</div>
</div>
</Card>
<Card className="p-2">
<div className="flex items-center gap-1">
<Sunrise className="w-5 h-5 text-yellow-500" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">Sunrise</span>
<span className="text-sm font-bold">{formatTime(weather.sys?.sunrise)}</span>
</div>
</div>
</Card>
<Card className="p-2">
<div className="flex items-center gap-1">
<Sunset className="w-5 h-5 text-orange-400" />
<div className="flex flex-col">
<span className="text-xs text-muted-foreground">Sunset</span>
<span className="text-sm font-bold">{formatTime(weather.sys?.sunset)}</span>
</div>
</div>
</Card>
</div>
{forecast && (
<div>
<div className="grid grid-cols-5 gap-2">
{forecast.map((day, index) => (
<Card key={index} className="p-2">
<div className="flex flex-col items-center gap-1">
<span className="text-sm font-medium">
{new Date(day.dt * 1000).toLocaleDateString('en-US', { weekday: 'short' })}
</span>
{getWeatherIcon(day.weather[0].id, new Date(day.dt * 1000), true)}
<div className="flex justify-center gap-1 items-baseline w-full">
<span className="text-sm font-bold">
{Math.round(day.main.temp_max)}°
</span>
<span className="text-xs text-muted-foreground">
{Math.round(day.main.temp_min)}°
</span>
</div>
<div className="flex flex-col items-center gap-1 w-full pt-1">
{day.rain?.['3h'] > 0 && (
<div className="flex items-center gap-1">
<CloudRain className="w-3 h-3 text-blue-400" />
<span className="text-xs">{day.rain['3h'].toFixed(2)}"</span>
</div>
)}
{day.snow?.['3h'] > 0 && (
<div className="flex items-center gap-1">
<CloudSnow className="w-3 h-3 text-blue-400" />
<span className="text-xs">{day.snow['3h'].toFixed(2)}"</span>
</div>
)}
{!day.rain?.['3h'] && !day.snow?.['3h'] && (
<div className="flex items-center gap-1">
<Umbrella className="w-3 h-3 text-gray-400" />
<span className="text-xs">0"</span>
</div>
)}
</div>
</div>
</Card>
))}
</div>
</div>
)}
</div>
);
const formatDate = (date) => const formatDate = (date) =>
date.toLocaleDateString("en-US", { date.toLocaleDateString("en-US", {
weekday: "short", weekday: "short",
@@ -125,6 +277,15 @@ const Header = () => {
day: "numeric", day: "numeric",
}); });
const formatTimeDisplay = (date) => {
const hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
const seconds = String(date.getSeconds()).padStart(2, "0");
const period = hours >= 12 ? "PM" : "AM";
const displayHours = hours % 12 || 12;
return `${displayHours}:${minutes}:${seconds} ${period}`;
};
return ( return (
<Card <Card
className={cn( className={cn(
@@ -146,7 +307,7 @@ const Header = () => {
)} )}
> >
<CraftsIcon /> <CraftsIcon />
</div>{" "} </div>
</div> </div>
<div> <div>
<h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-blue-400 bg-clip-text text-transparent"> <h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-blue-400 bg-clip-text text-transparent">
@@ -158,14 +319,32 @@ const Header = () => {
{weather?.main && ( {weather?.main && (
<> <>
<div className="flex-col items-center text-center"> <div className="flex-col items-center text-center">
<div className="items-center justify-center space-x-2 rounded-lg px-4 hidden sm:flex"> <Popover>
<PopoverTrigger asChild>
<div className="items-center justify-center space-x-2 rounded-lg px-4 hidden sm:flex cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors p-2">
{getWeatherIcon(weather.weather[0]?.id, currentTime)} {getWeatherIcon(weather.weather[0]?.id, currentTime)}
<div> <div>
<p className="text-xl font-bold tracking-tight dark:text-gray-100"> <p className="text-xl font-bold tracking-tight dark:text-gray-100">
{Math.round(weather.main.temp)}° F {Math.round(weather.main.temp)}° F
</p> </p>
</div> </div>
{weather.alerts && (
<AlertTriangle className="w-5 h-5 text-red-500 ml-1" />
)}
</div> </div>
</PopoverTrigger>
<PopoverContent className="w-[450px]" align="end" side="bottom" sideOffset={5}>
{weather.alerts && (
<Alert variant="warning" className="mb-3">
<AlertTriangle className="h-3 w-3" />
<AlertDescription className="text-xs">
{weather.alerts[0].event}
</AlertDescription>
</Alert>
)}
<WeatherDetails />
</PopoverContent>
</Popover>
</div> </div>
</> </>
)} )}
@@ -183,7 +362,7 @@ const Header = () => {
<Clock className="w-5 h-5 text-blue-500 shrink-0" /> <Clock className="w-5 h-5 text-blue-500 shrink-0" />
<div> <div>
<p className="text-md sm:text-xl font-bold tracking-tight tabular-nums dark:text-gray-100 mr-2"> <p className="text-md sm:text-xl font-bold tracking-tight tabular-nums dark:text-gray-100 mr-2">
{formatTime(currentTime)} {formatTimeDisplay(currentTime)}
</p> </p>
</div> </div>
</div> </div>