chore: add prettier formatting
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
This commit is contained in:
@@ -82,7 +82,9 @@ export function DayForecastModal({
|
||||
}, [day, onClose]);
|
||||
|
||||
const formattedDate = day
|
||||
? new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", timeZone: "UTC" }).format(new Date(`${day.date}T12:00:00Z`))
|
||||
? new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", timeZone: "UTC" }).format(
|
||||
new Date(`${day.date}T12:00:00Z`),
|
||||
)
|
||||
: "";
|
||||
|
||||
const modal = (
|
||||
@@ -114,7 +116,9 @@ export function DayForecastModal({
|
||||
<CloudSun className="size-4" />
|
||||
{t("forecast.dayDetails")}
|
||||
</p>
|
||||
<h2 id="day-forecast-title" className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">{locationName}</h2>
|
||||
<h2 id="day-forecast-title" className="mt-2 text-2xl font-semibold tracking-tight sm:text-3xl">
|
||||
{locationName}
|
||||
</h2>
|
||||
<p className="mt-1 capitalize text-muted">{formattedDate}</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -134,13 +138,25 @@ export function DayForecastModal({
|
||||
<p className="text-sm text-muted">{getForecastCondition(day.weatherCode, language)}</p>
|
||||
<div className="mt-2 flex items-end gap-4">
|
||||
<ForecastIcon code={day.weatherCode} className="mb-2 size-14 text-accent" />
|
||||
<p className="text-6xl font-semibold tracking-[-0.08em] sm:text-7xl">{formatForecastTemperature(day.temperatureMax, language, temperatureUnit)}</p>
|
||||
<p className="mb-2 text-2xl text-muted">{formatForecastTemperature(day.temperatureMin, language, temperatureUnit)}</p>
|
||||
<p className="text-6xl font-semibold tracking-[-0.08em] sm:text-7xl">
|
||||
{formatForecastTemperature(day.temperatureMax, language, temperatureUnit)}
|
||||
</p>
|
||||
<p className="mb-2 text-2xl text-muted">
|
||||
{formatForecastTemperature(day.temperatureMin, language, temperatureUnit)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 sm:min-w-[22rem]">
|
||||
<DayMetric icon={Droplets} label={t("forecast.precipitation")} value={formatForecastRainfall(day.precipitation, language)} />
|
||||
<DayMetric icon={Wind} label={t("forecast.maxWind")} value={formatForecastWind(maximumWind, language, windSpeedUnit)} />
|
||||
<DayMetric
|
||||
icon={Droplets}
|
||||
label={t("forecast.precipitation")}
|
||||
value={formatForecastRainfall(day.precipitation, language)}
|
||||
/>
|
||||
<DayMetric
|
||||
icon={Wind}
|
||||
label={t("forecast.maxWind")}
|
||||
value={formatForecastWind(maximumWind, language, windSpeedUnit)}
|
||||
/>
|
||||
<DayMetric icon={Sunrise} label={t("forecast.sunrise")} value={formatHour(day.sunrise)} />
|
||||
<DayMetric icon={Sunset} label={t("forecast.sunset")} value={formatHour(day.sunset)} />
|
||||
</div>
|
||||
@@ -160,8 +176,14 @@ export function DayForecastModal({
|
||||
title={getForecastCondition(weatherCode, language)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted">{formatHour(hour.time)}</p>
|
||||
<ForecastIcon code={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>
|
||||
<ForecastIcon
|
||||
code={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}%`}
|
||||
|
||||
@@ -1,13 +1,44 @@
|
||||
import { Cloud, CloudDrizzle, CloudFog, CloudLightning, CloudMoon, CloudRain, CloudSnow, CloudSun, MoonStar, Sun } from "lucide-react";
|
||||
import {
|
||||
Cloud,
|
||||
CloudDrizzle,
|
||||
CloudFog,
|
||||
CloudLightning,
|
||||
CloudMoon,
|
||||
CloudRain,
|
||||
CloudSnow,
|
||||
CloudSun,
|
||||
MoonStar,
|
||||
Sun,
|
||||
} from "lucide-react";
|
||||
|
||||
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
|
||||
: code !== null && ((code >= 71 && code <= 77) || code === 85 || code === 86) ? CloudSnow
|
||||
: code !== null && code >= 95 ? CloudLightning
|
||||
: Cloud;
|
||||
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
|
||||
: code !== null && ((code >= 71 && code <= 77) || code === 85 || code === 86)
|
||||
? CloudSnow
|
||||
: code !== null && code >= 95
|
||||
? CloudLightning
|
||||
: Cloud;
|
||||
return <Icon className={className} strokeWidth={1.45} />;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { CalendarDays, ChevronRight, Clock3, CloudRain, CloudSun, Droplets, RefreshCw, ThermometerSun, Wind, type LucideIcon } from "lucide-react";
|
||||
import {
|
||||
CalendarDays,
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
CloudRain,
|
||||
CloudSun,
|
||||
Droplets,
|
||||
RefreshCw,
|
||||
ThermometerSun,
|
||||
Wind,
|
||||
type LucideIcon,
|
||||
} from "lucide-react";
|
||||
import { DayForecastCharts } from "@/components/charts/day-forecast-charts";
|
||||
import { DayForecastModal } from "@/components/forecast/day-forecast-modal";
|
||||
import { ForecastIcon } from "@/components/forecast/forecast-icon";
|
||||
@@ -71,24 +82,47 @@ function HourlyForecastSummary({ hours }: { hours: HourlyForecast[] }) {
|
||||
const maximumWind = getMaximum(hours.map((hour) => hour.windSpeed));
|
||||
const rainfallTotal = getTotal(hours.map((hour) => hour.precipitation));
|
||||
const maximumRainfallProbability = getMaximum(hours.map((hour) => hour.precipitationProbability));
|
||||
const temperatureRange = minimumTemperature === null || maximumTemperature === null
|
||||
? "—"
|
||||
: `${formatForecastTemperature(minimumTemperature, language, temperatureUnit)} / ${formatForecastTemperature(maximumTemperature, language, temperatureUnit)}`;
|
||||
const temperatureRange =
|
||||
minimumTemperature === null || maximumTemperature === null
|
||||
? "—"
|
||||
: `${formatForecastTemperature(minimumTemperature, language, temperatureUnit)} / ${formatForecastTemperature(maximumTemperature, language, temperatureUnit)}`;
|
||||
|
||||
return (
|
||||
<div className="mt-auto hidden border-t border-border/70 pt-4 lg:block">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.14em] text-muted">{t("forecast.nextHoursOverview")}</p>
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.14em] text-muted">
|
||||
{t("forecast.nextHoursOverview")}
|
||||
</p>
|
||||
<div className="mt-3 grid grid-cols-4 gap-2">
|
||||
<HourlySummaryMetric icon={ThermometerSun} label={t("forecast.temperatureRange")} value={temperatureRange} />
|
||||
<HourlySummaryMetric icon={Wind} label={t("forecast.maxWind")} value={formatForecastWind(maximumWind, language, windSpeedUnit)} />
|
||||
<HourlySummaryMetric icon={CloudRain} label={t("forecast.rainfallTotal")} value={formatForecastRainfall(rainfallTotal, language)} />
|
||||
<HourlySummaryMetric icon={Droplets} label={t("forecast.maxProbability")} value={maximumRainfallProbability === null ? "—" : `${maximumRainfallProbability}%`} />
|
||||
<HourlySummaryMetric
|
||||
icon={Wind}
|
||||
label={t("forecast.maxWind")}
|
||||
value={formatForecastWind(maximumWind, language, windSpeedUnit)}
|
||||
/>
|
||||
<HourlySummaryMetric
|
||||
icon={CloudRain}
|
||||
label={t("forecast.rainfallTotal")}
|
||||
value={formatForecastRainfall(rainfallTotal, language)}
|
||||
/>
|
||||
<HourlySummaryMetric
|
||||
icon={Droplets}
|
||||
label={t("forecast.maxProbability")}
|
||||
value={maximumRainfallProbability === null ? "—" : `${maximumRainfallProbability}%`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DailyForecastRow({ day, index, onSelect }: { day: DailyForecast; index: number; onSelect: (day: DailyForecast) => void }) {
|
||||
function DailyForecastRow({
|
||||
day,
|
||||
index,
|
||||
onSelect,
|
||||
}: {
|
||||
day: DailyForecast;
|
||||
index: number;
|
||||
onSelect: (day: DailyForecast) => void;
|
||||
}) {
|
||||
const { language, locale, t } = useI18n();
|
||||
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
|
||||
const label = formatDay(day.date, locale, t("forecast.today"), index);
|
||||
@@ -109,17 +143,37 @@ function DailyForecastRow({ day, index, onSelect }: { day: DailyForecast; index:
|
||||
<p className="text-sm font-semibold capitalize">{label}</p>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<ForecastIcon code={day.weatherCode} className="size-6 shrink-0 text-accent" />
|
||||
<span className="truncate text-xs text-muted">{getForecastCondition(day.weatherCode, language)} · {formatForecastRainfall(day.precipitation, language)}</span>
|
||||
<span className="truncate text-xs text-muted">
|
||||
{getForecastCondition(day.weatherCode, language)} · {formatForecastRainfall(day.precipitation, language)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="hidden items-center gap-1 text-xs text-accent sm:flex"><Droplets className="size-3" />{day.precipitationProbability === null ? "—" : `${day.precipitationProbability}%`}</span>
|
||||
<p className="whitespace-nowrap text-sm"><strong>{formatForecastTemperature(day.temperatureMax, language, temperatureUnit)}</strong><span className="ml-2 text-muted">{formatForecastTemperature(day.temperatureMin, language, temperatureUnit)}</span></p>
|
||||
<span className="hidden items-center gap-1 text-xs text-accent sm:flex">
|
||||
<Droplets className="size-3" />
|
||||
{day.precipitationProbability === null ? "—" : `${day.precipitationProbability}%`}
|
||||
</span>
|
||||
<p className="whitespace-nowrap text-sm">
|
||||
<strong>{formatForecastTemperature(day.temperatureMax, language, temperatureUnit)}</strong>
|
||||
<span className="ml-2 text-muted">
|
||||
{formatForecastTemperature(day.temperatureMin, language, temperatureUnit)}
|
||||
</span>
|
||||
</p>
|
||||
<ChevronRight className="hidden size-4 text-muted sm:block" />
|
||||
</motion.button>
|
||||
</motion.li>
|
||||
);
|
||||
}
|
||||
|
||||
export function ForecastPanel({ latitude, longitude, region = "PL", locationName }: { latitude?: number; longitude?: number; region?: WeatherRegion; locationName: string }) {
|
||||
export function ForecastPanel({
|
||||
latitude,
|
||||
longitude,
|
||||
region = "PL",
|
||||
locationName,
|
||||
}: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
region?: WeatherRegion;
|
||||
locationName: string;
|
||||
}) {
|
||||
const { language, t } = useI18n();
|
||||
const temperatureUnit = useWeatherStore((state) => state.temperatureUnit);
|
||||
const windSpeedUnit = useWeatherStore((state) => state.windSpeedUnit);
|
||||
@@ -133,9 +187,14 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div>
|
||||
<p className="section-kicker flex items-center gap-2"><CloudSun className="size-4" />{t("forecast.label")}</p>
|
||||
<p className="section-kicker flex items-center gap-2">
|
||||
<CloudSun className="size-4" />
|
||||
{t("forecast.label")}
|
||||
</p>
|
||||
<h2 className="mt-2 text-xl font-semibold tracking-tight">{t("forecast.title")}</h2>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-muted">{t("forecast.description", { location: locationName })}</p>
|
||||
<p className="mt-1 max-w-3xl text-sm leading-6 text-muted">
|
||||
{t("forecast.description", { location: locationName })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!Number.isFinite(latitude) || !Number.isFinite(longitude) || isPending ? (
|
||||
@@ -146,7 +205,10 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
) : isError || !forecast ? (
|
||||
<Card className="flex min-h-40 flex-col items-center justify-center p-6 text-center">
|
||||
<p className="text-sm text-muted">{t("forecast.error")}</p>
|
||||
<Button variant="glass" className="mt-4" onClick={() => refetch()}><RefreshCw className="size-4" />{t("common.retry")}</Button>
|
||||
<Button variant="glass" className="mt-4" onClick={() => refetch()}>
|
||||
<RefreshCw className="size-4" />
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</Card>
|
||||
) : !forecast.hourly.length || !forecast.daily.length ? (
|
||||
<EmptyState title={t("forecast.emptyTitle")} description={t("forecast.emptyDescription")} />
|
||||
@@ -154,7 +216,10 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
<div className="space-y-3">
|
||||
<div className="grid items-start gap-3 lg:grid-cols-[1.35fr_1fr] lg:items-stretch">
|
||||
<Card className="flex flex-col overflow-hidden p-4 sm:p-5 lg:h-full">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold"><Clock3 className="size-4 text-accent" />{t("forecast.hourly")}</h3>
|
||||
<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) => {
|
||||
@@ -170,11 +235,23 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
title={getForecastCondition(weatherCode, language)}
|
||||
>
|
||||
<p className="text-xs font-medium text-muted">{formatHour(hour.time)}</p>
|
||||
<ForecastIcon code={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>
|
||||
<ForecastIcon
|
||||
code={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")}>
|
||||
<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>
|
||||
@@ -195,8 +272,15 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
<HourlyForecastSummary hours={upcomingHours} />
|
||||
</Card>
|
||||
<Card className="p-4 sm:p-5">
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold"><CalendarDays className="size-4 text-accent" />{t("forecast.daily")}</h3>
|
||||
<ul className="mt-2">{forecast.daily.map((day, index) => <DailyForecastRow day={day} index={index} key={day.date} onSelect={setSelectedDay} />)}</ul>
|
||||
<h3 className="flex items-center gap-2 text-sm font-semibold">
|
||||
<CalendarDays className="size-4 text-accent" />
|
||||
{t("forecast.daily")}
|
||||
</h3>
|
||||
<ul className="mt-2">
|
||||
{forecast.daily.map((day, index) => (
|
||||
<DailyForecastRow day={day} index={index} key={day.date} onSelect={setSelectedDay} />
|
||||
))}
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
<DayForecastCharts hours={todayHours} />
|
||||
@@ -205,7 +289,13 @@ export function ForecastPanel({ latitude, longitude, region = "PL", locationName
|
||||
|
||||
{forecast && <ForecastSources sources={forecast.sources} />}
|
||||
|
||||
<DayForecastModal day={selectedDay} hours={selectedDayHours} locationName={locationName} sources={forecast?.sources ?? []} onClose={closeDayDetails} />
|
||||
<DayForecastModal
|
||||
day={selectedDay}
|
||||
hours={selectedDayHours}
|
||||
locationName={locationName}
|
||||
sources={forecast?.sources ?? []}
|
||||
onClose={closeDayDetails}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,13 +13,23 @@ export function ForecastSources({ sources }: { sources: ForecastSource[] }) {
|
||||
{t("forecast.source")}{" "}
|
||||
{hasImgw && (
|
||||
<>
|
||||
<a href="https://meteo.imgw.pl/pogoda/" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 underline decoration-muted/60 underline-offset-2 transition hover:text-accent">
|
||||
<a
|
||||
href="https://meteo.imgw.pl/pogoda/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 underline decoration-muted/60 underline-offset-2 transition hover:text-accent"
|
||||
>
|
||||
IMGW ALARO <ExternalLink className="size-3" />
|
||||
</a>
|
||||
{", "}
|
||||
</>
|
||||
)}
|
||||
<a href="https://open-meteo.com/" target="_blank" rel="noreferrer" className="inline-flex items-center gap-1 underline decoration-muted/60 underline-offset-2 transition hover:text-accent">
|
||||
<a
|
||||
href="https://open-meteo.com/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex items-center gap-1 underline decoration-muted/60 underline-offset-2 transition hover:text-accent"
|
||||
>
|
||||
Open-Meteo <ExternalLink className="size-3" />
|
||||
</a>
|
||||
. {t(hasImgw ? "forecast.sourceCombinedDescription" : "forecast.sourceFallbackDescription")}
|
||||
|
||||
Reference in New Issue
Block a user