style: fill desktop hourly forecast card
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { CalendarDays, ChevronRight, Clock3, CloudRain, CloudSun, Droplets, ExternalLink, RefreshCw, ThermometerSun, Wind } from "lucide-react";
|
||||
import { CalendarDays, ChevronRight, Clock3, CloudRain, CloudSun, Droplets, ExternalLink, 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";
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
getHourlyForecastForDay,
|
||||
getUpcomingHourlyForecast,
|
||||
} from "@/lib/forecast-utils";
|
||||
import type { DailyForecast } from "@/types/forecast";
|
||||
import type { DailyForecast, HourlyForecast } from "@/types/forecast";
|
||||
|
||||
function formatHour(value: string) {
|
||||
return value.slice(11, 16);
|
||||
@@ -31,6 +31,55 @@ function formatDay(value: string, locale: string, todayLabel: string, index: num
|
||||
return new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(new Date(`${value}T12:00:00Z`));
|
||||
}
|
||||
|
||||
function getMaximum(values: Array<number | null>) {
|
||||
const availableValues = values.filter((value): value is number => value !== null);
|
||||
return availableValues.length ? Math.max(...availableValues) : null;
|
||||
}
|
||||
|
||||
function getMinimum(values: Array<number | null>) {
|
||||
const availableValues = values.filter((value): value is number => value !== null);
|
||||
return availableValues.length ? Math.min(...availableValues) : null;
|
||||
}
|
||||
|
||||
function getTotal(values: Array<number | null>) {
|
||||
const availableValues = values.filter((value): value is number => value !== null);
|
||||
return availableValues.length ? availableValues.reduce((total, value) => total + value, 0) : null;
|
||||
}
|
||||
|
||||
function HourlySummaryMetric({ icon: Icon, label, value }: { icon: LucideIcon; label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-white/30 bg-white/20 p-3 dark:border-white/10 dark:bg-white/5">
|
||||
<Icon className="size-4 text-sky-700 dark:text-sky-300" />
|
||||
<p className="mt-2 text-[0.62rem] font-semibold uppercase tracking-[0.12em] text-slate-500 dark:text-slate-400">{label}</p>
|
||||
<p className="mt-1 text-sm font-semibold">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function HourlyForecastSummary({ hours }: { hours: HourlyForecast[] }) {
|
||||
const { language, t } = useI18n();
|
||||
const minimumTemperature = getMinimum(hours.map((hour) => hour.temperature));
|
||||
const maximumTemperature = getMaximum(hours.map((hour) => hour.temperature));
|
||||
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)} / ${formatForecastTemperature(maximumTemperature, language)}`;
|
||||
|
||||
return (
|
||||
<div className="mt-auto hidden border-t border-white/30 pt-4 dark:border-white/10 lg:block">
|
||||
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.14em] text-slate-500 dark:text-slate-400">{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)} />
|
||||
<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 }) {
|
||||
const { language, locale, t } = useI18n();
|
||||
const label = formatDay(day.date, locale, t("forecast.today"), index);
|
||||
@@ -95,7 +144,7 @@ export function ForecastPanel({ latitude, longitude, locationName }: { latitude?
|
||||
<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-sky-600 dark:text-sky-300" />{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:my-auto lg:pt-4">
|
||||
<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
|
||||
@@ -128,6 +177,7 @@ export function ForecastPanel({ latitude, longitude, locationName }: { latitude?
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<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-sky-600 dark:text-sky-300" />{t("forecast.daily")}</h3>
|
||||
|
||||
Reference in New Issue
Block a user