Add weather popover on click
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user