fix: show night icons for hourly forecast
All checks were successful
CI / Lint, typecheck and build (push) Successful in 10m2s
All checks were successful
CI / Lint, typecheck and build (push) Successful in 10m2s
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
formatForecastTemperature,
|
||||
formatForecastWind,
|
||||
getForecastCondition,
|
||||
isForecastNight,
|
||||
} from "@/lib/forecast-utils";
|
||||
import { useWeatherStore } from "@/lib/store";
|
||||
import type { DailyForecast, ForecastSource, HourlyForecast } from "@/types/forecast";
|
||||
@@ -156,7 +157,7 @@ export function DayForecastModal({
|
||||
title={getForecastCondition(hour.weatherCode, language)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted">{formatHour(hour.time)}</p>
|
||||
<ForecastIcon code={hour.weatherCode} className="mx-auto my-3 size-6 text-accent" />
|
||||
<ForecastIcon code={hour.weatherCode} isNight={isForecastNight(hour.time, day)} className="mx-auto my-3 size-6 text-accent" />
|
||||
<p className="text-lg font-semibold tracking-tight">{formatForecastTemperature(hour.temperature, language, temperatureUnit)}</p>
|
||||
<p className="mt-2 flex items-center justify-center gap-1 text-[0.66rem] text-accent">
|
||||
<Droplets className="size-3" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Cloud, CloudDrizzle, CloudFog, CloudLightning, CloudRain, CloudSnow, CloudSun, Sun } from "lucide-react";
|
||||
import { Cloud, CloudDrizzle, CloudFog, CloudLightning, CloudMoon, CloudRain, CloudSnow, CloudSun, MoonStar, Sun } from "lucide-react";
|
||||
|
||||
export function ForecastIcon({ code, className = "" }: { code: number | null; className?: string }) {
|
||||
const Icon = code === 0 ? Sun
|
||||
: code === 1 || code === 2 ? CloudSun
|
||||
export function ForecastIcon({ code, className = "", isNight = false }: { code: number | null; className?: string; isNight?: boolean }) {
|
||||
const Icon = code === 0 ? isNight ? MoonStar : Sun
|
||||
: code === 1 || code === 2 ? isNight ? CloudMoon : CloudSun
|
||||
: code === 45 || code === 48 ? CloudFog
|
||||
: code !== null && code >= 51 && code <= 57 ? CloudDrizzle
|
||||
: code !== null && ((code >= 61 && code <= 67) || (code >= 80 && code <= 82)) ? CloudRain
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
formatForecastRainfall,
|
||||
formatForecastTemperature,
|
||||
formatForecastWind,
|
||||
getDailyForecastForHour,
|
||||
getForecastCondition,
|
||||
getHourlyForecastForDay,
|
||||
getUpcomingHourlyForecast,
|
||||
isForecastNight,
|
||||
} from "@/lib/forecast-utils";
|
||||
import { useWeatherStore } from "@/lib/store";
|
||||
import type { DailyForecast, HourlyForecast } from "@/types/forecast";
|
||||
@@ -153,35 +155,38 @@ export function ForecastPanel({ latitude, longitude, locationName }: { latitude?
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold"><Clock3 className="size-4 text-accent" />{t("forecast.hourly")}</h3>
|
||||
<div className="weather-scrollbar -mx-4 mt-4 overflow-x-auto px-4 pb-2 sm:-mx-5 sm:px-5 lg:mt-5">
|
||||
<ul className="flex min-w-max gap-2">
|
||||
{upcomingHours.map((hour, index) => (
|
||||
<motion.li
|
||||
key={hour.time}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: Math.min(index * 0.018, 0.3), duration: 0.25 }}
|
||||
className="w-[4.6rem] rounded-card border border-border/60 bg-surface-muted/55 px-2 py-3 text-center lg:w-[5.5rem] lg:py-4"
|
||||
title={getForecastCondition(hour.weatherCode, language)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted">{formatHour(hour.time)}</p>
|
||||
<ForecastIcon code={hour.weatherCode} className="mx-auto my-3 size-6 text-accent" />
|
||||
<p className="text-lg font-semibold tracking-tight">{formatForecastTemperature(hour.temperature, language, temperatureUnit)}</p>
|
||||
<p className="mt-2 flex items-center justify-center gap-1 text-[0.66rem] text-accent"><Droplets className="size-3" />{hour.precipitationProbability === null ? "—" : `${hour.precipitationProbability}%`}</p>
|
||||
<div className="mt-3 hidden space-y-1.5 border-t border-border/65 pt-3 text-[0.66rem] text-muted lg:block">
|
||||
<p className="flex items-center justify-center gap-1" title={t("forecast.apparentTemperature")}>
|
||||
<ThermometerSun className="size-3 text-accent" />
|
||||
{formatForecastTemperature(hour.feelsLike, language, temperatureUnit)}
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1" title={t("weather.wind")}>
|
||||
<Wind className="size-3 text-accent" />
|
||||
{formatForecastWind(hour.windSpeed, language, windSpeedUnit)}
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1" title={t("forecast.precipitation")}>
|
||||
<CloudRain className="size-3 text-accent" />
|
||||
{formatForecastRainfall(hour.precipitation, language)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.li>
|
||||
))}
|
||||
{upcomingHours.map((hour, index) => {
|
||||
const day = getDailyForecastForHour(forecast.daily, hour.time);
|
||||
return (
|
||||
<motion.li
|
||||
key={hour.time}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: Math.min(index * 0.018, 0.3), duration: 0.25 }}
|
||||
className="w-[4.6rem] rounded-card border border-border/60 bg-surface-muted/55 px-2 py-3 text-center lg:w-[5.5rem] lg:py-4"
|
||||
title={getForecastCondition(hour.weatherCode, language)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted">{formatHour(hour.time)}</p>
|
||||
<ForecastIcon code={hour.weatherCode} isNight={isForecastNight(hour.time, day)} className="mx-auto my-3 size-6 text-accent" />
|
||||
<p className="text-lg font-semibold tracking-tight">{formatForecastTemperature(hour.temperature, language, temperatureUnit)}</p>
|
||||
<p className="mt-2 flex items-center justify-center gap-1 text-[0.66rem] text-accent"><Droplets className="size-3" />{hour.precipitationProbability === null ? "—" : `${hour.precipitationProbability}%`}</p>
|
||||
<div className="mt-3 hidden space-y-1.5 border-t border-border/65 pt-3 text-[0.66rem] text-muted lg:block">
|
||||
<p className="flex items-center justify-center gap-1" title={t("forecast.apparentTemperature")}>
|
||||
<ThermometerSun className="size-3 text-accent" />
|
||||
{formatForecastTemperature(hour.feelsLike, language, temperatureUnit)}
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1" title={t("weather.wind")}>
|
||||
<Wind className="size-3 text-accent" />
|
||||
{formatForecastWind(hour.windSpeed, language, windSpeedUnit)}
|
||||
</p>
|
||||
<p className="flex items-center justify-center gap-1" title={t("forecast.precipitation")}>
|
||||
<CloudRain className="size-3 text-accent" />
|
||||
{formatForecastRainfall(hour.precipitation, language)}
|
||||
</p>
|
||||
</div>
|
||||
</motion.li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<HourlyForecastSummary hours={upcomingHours} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Language, TranslationKey } from "@/lib/i18n";
|
||||
import { translate } from "@/lib/i18n";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, formatTemperature, formatWindSpeed } from "@/lib/weather-utils";
|
||||
import type { HourlyForecast } from "@/types/forecast";
|
||||
import type { DailyForecast, HourlyForecast } from "@/types/forecast";
|
||||
import type { TemperatureUnit, WindSpeedUnit } from "@/types/units";
|
||||
|
||||
export function getForecastConditionKey(code: number | null): TranslationKey {
|
||||
@@ -89,6 +89,23 @@ export function getHourlyForecastForDay(hours: HourlyForecast[], date: string) {
|
||||
return hours.filter((hour) => hour.time.startsWith(`${date}T`));
|
||||
}
|
||||
|
||||
export function getDailyForecastForHour(days: DailyForecast[], time: string) {
|
||||
return days.find((day) => time.startsWith(`${day.date}T`)) ?? null;
|
||||
}
|
||||
|
||||
export function isForecastNight(time: string, day: Pick<DailyForecast, "sunrise" | "sunset"> | null | undefined) {
|
||||
const hourTimestamp = parseForecastHour(time);
|
||||
const sunriseTimestamp = day?.sunrise ? parseForecastHour(day.sunrise) : null;
|
||||
const sunsetTimestamp = day?.sunset ? parseForecastHour(day.sunset) : null;
|
||||
|
||||
if (hourTimestamp !== null && sunriseTimestamp !== null && sunsetTimestamp !== null) {
|
||||
return hourTimestamp < sunriseTimestamp || hourTimestamp >= sunsetTimestamp;
|
||||
}
|
||||
|
||||
const hour = getForecastHourOfDay(time);
|
||||
return hour !== null && (hour < 6 || hour >= 21);
|
||||
}
|
||||
|
||||
export function isForecastHourPast(time: string) {
|
||||
const currentHour = getWarsawForecastHour();
|
||||
const hourTimestamp = parseForecastHour(time);
|
||||
|
||||
Reference in New Issue
Block a user