Add weather popover on click
This commit is contained in:
@@ -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(
|
||||
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`
|
||||
);
|
||||
const data = await response.json();
|
||||
setWeather(data);
|
||||
),
|
||||
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">
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user