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,
Moon,
Monitor,
Wind,
Droplets,
ThermometerSun,
ThermometerSnowflake,
Sunrise,
Sunset,
AlertTriangle,
Umbrella,
} from "lucide-react";
import { useScroll } from "@/contexts/ScrollContext";
import { useTheme } from "@/components/theme/ThemeProvider";
@@ -26,6 +34,12 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Alert, AlertDescription } from "@/components/ui/alert";
const CraftsIcon = () => (
<svg viewBox="0 0 2687 3338" className="w-6 h-6" aria-hidden="true">
@@ -43,6 +57,7 @@ const CraftsIcon = () => (
const Header = () => {
const [currentTime, setCurrentTime] = useState(new Date());
const [weather, setWeather] = useState(null);
const [forecast, setForecast] = useState(null);
const { isStuck } = useScroll();
const { theme, systemTheme, toggleTheme, setTheme } = useTheme();
@@ -54,69 +69,206 @@ const Header = () => {
}, []);
useEffect(() => {
const fetchWeather = async () => {
const fetchWeatherData = async () => {
try {
const API_KEY = import.meta.env.VITE_OPENWEATHER_API_KEY;
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?lat=43.63507&lon=-84.18995&appid=${API_KEY}&units=imperial`
);
const data = await response.json();
setWeather(data);
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`
),
fetch(
`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) {
console.error("Error fetching weather:", error);
}
};
fetchWeather();
const weatherTimer = setInterval(fetchWeather, 300000);
fetchWeatherData();
const weatherTimer = setInterval(fetchWeatherData, 300000);
return () => clearInterval(weatherTimer);
}, []);
const getWeatherIcon = (weatherCode, currentTime) => {
if (!weatherCode) return <CircleAlert className="w-6 h-6 text-red-500" />;
const getWeatherIcon = (weatherCode, currentTime, small = false) => {
if (!weatherCode) return <CircleAlert className={cn(small ? "w-6 h-6" : "w-7 h-7", "text-red-500")} />;
const code = parseInt(weatherCode, 10);
const iconProps = small ? "w-6 h-6" : "w-7 h-7";
switch (true) {
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:
return <CloudDrizzle className="w-7 h-7 text-blue-400" />;
return <CloudDrizzle className={cn(iconProps, "text-blue-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:
return <CloudSnow className="w-7 h-7 text-blue-200" />;
return <CloudSnow className={cn(iconProps, "text-blue-400")} />;
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:
return <Haze className="w-7 h-7 text-gray-500" />;
return <Haze className={cn(iconProps, "text-gray-700")} />;
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:
return <Tornado className="w-7 h-7 text-gray-500" />;
return <Tornado className={cn(iconProps, "text-gray-700")} />;
case code === 800:
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:
return <CloudSun className="w-7 h-7 text-gray-400" />;
return <CloudSun className={cn(iconProps, "text-gray-600")} />;
case code >= 803:
return <Cloud className="w-7 h-7 text-gray-400" />;
return <Cloud className={cn(iconProps, "text-gray-600")} />;
default:
return <CircleAlert className="w-6 h-6 text-red-500" />;
return <CircleAlert className={cn(iconProps, "text-red-500")} />;
}
};
const formatTime = (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}`;
const formatTime = (timestamp) => {
if (!timestamp) return '--:--';
const date = new Date(timestamp * 1000);
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
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) =>
date.toLocaleDateString("en-US", {
weekday: "short",
@@ -125,6 +277,15 @@ const Header = () => {
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 (
<Card
className={cn(
@@ -146,7 +307,7 @@ const Header = () => {
)}
>
<CraftsIcon />
</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">
@@ -158,14 +319,32 @@ const Header = () => {
{weather?.main && (
<>
<div className="flex-col items-center text-center">
<div className="items-center justify-center space-x-2 rounded-lg px-4 hidden sm:flex">
{getWeatherIcon(weather.weather[0]?.id, currentTime)}
<div>
<p className="text-xl font-bold tracking-tight dark:text-gray-100">
{Math.round(weather.main.temp)}° F
</p>
</div>
</div>
<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)}
<div>
<p className="text-xl font-bold tracking-tight dark:text-gray-100">
{Math.round(weather.main.temp)}° F
</p>
</div>
{weather.alerts && (
<AlertTriangle className="w-5 h-5 text-red-500 ml-1" />
)}
</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>
</>
)}
@@ -183,7 +362,7 @@ const Header = () => {
<Clock className="w-5 h-5 text-blue-500 shrink-0" />
<div>
<p className="text-md sm:text-xl font-bold tracking-tight tabular-nums dark:text-gray-100 mr-2">
{formatTime(currentTime)}
{formatTimeDisplay(currentTime)}
</p>
</div>
</div>