Files
wtr/components/warnings/dashboard-warnings.tsx

142 lines
5.7 KiB
TypeScript

"use client";
import { useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { motion } from "framer-motion";
import { AlertTriangle, ArrowRight, CalendarClock } from "lucide-react";
import { useWarnings } from "@/hooks/use-warnings";
import { DEFAULT_STATION_ID } from "@/lib/constants";
import { useI18n } from "@/lib/i18n";
import { formatProvinceName, getProvinceForSelection } from "@/lib/provinces";
import { useWeatherStore } from "@/lib/store";
import { formatDateTime } from "@/lib/weather-utils";
import type { WeatherWarning } from "@/types/imgw";
const MAX_VISIBLE_WARNINGS = 2;
function getTimestamp(value: string | null) {
if (!value) return null;
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? null : timestamp;
}
function isWarningActive(warning: WeatherWarning, now: number) {
const validFrom = getTimestamp(warning.validFrom);
return validFrom === null || validFrom <= now;
}
function compareDashboardWarnings(a: WeatherWarning, b: WeatherWarning, now: number) {
const activeDifference = Number(isWarningActive(b, now)) - Number(isWarningActive(a, now));
if (activeDifference) return activeDifference;
const levelDifference = (b.level ?? 0) - (a.level ?? 0);
if (levelDifference) return levelDifference;
return (getTimestamp(a.validFrom) ?? 0) - (getTimestamp(b.validFrom) ?? 0);
}
export function DashboardWarnings() {
const { data: warnings } = useWarnings();
const { language, t } = useI18n();
const selectedStationId = useWeatherStore((state) => state.selectedStationId);
const selectedLocation = useWeatherStore((state) => state.selectedLocation);
const [now, setNow] = useState<number | null>(null);
useEffect(() => {
const timeoutId = window.setTimeout(() => setNow(Date.now()), 0);
const intervalId = window.setInterval(() => setNow(Date.now()), 30_000);
return () => {
window.clearTimeout(timeoutId);
window.clearInterval(intervalId);
};
}, []);
const province = getProvinceForSelection(selectedLocation?.province, selectedStationId ?? DEFAULT_STATION_ID);
const relevantWarnings = useMemo(() => {
if (!warnings || !province || now === null) return [];
return warnings
.filter((warning) => {
const validTo = getTimestamp(warning.validTo);
return warning.kind === "meteo"
&& warning.provinces.includes(province)
&& (validTo === null || validTo > now);
})
.sort((a, b) => compareDashboardWarnings(a, b, now));
}, [now, province, warnings]);
if (!province || !relevantWarnings.length || now === null) return null;
const visibleWarnings = relevantWarnings.slice(0, MAX_VISIBLE_WARNINGS);
const hiddenWarningsCount = relevantWarnings.length - visibleWarnings.length;
return (
<motion.section
aria-label={t("warnings.dashboard.title")}
className="rounded-panel border border-warning/25 bg-warning/10 p-4 shadow-soft sm:p-5"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, ease: "easeOut" }}
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-start gap-3">
<div className="rounded-control border border-warning/30 bg-warning/10 p-2 text-warning">
<AlertTriangle className="size-4" aria-hidden="true" />
</div>
<div>
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.2em] text-warning">
IMGW · {formatProvinceName(province, language)}
</p>
<h2 className="mt-1 text-base font-semibold text-foreground">
{t("warnings.dashboard.title")}
</h2>
</div>
</div>
<Link
href="/warnings"
className="inline-flex w-fit items-center gap-1.5 rounded-control px-2 py-1 text-xs font-semibold text-warning transition hover:bg-warning/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-warning"
>
{t("warnings.dashboard.viewAll")}
<ArrowRight className="size-3.5" aria-hidden="true" />
</Link>
</div>
<div className="mt-4 grid gap-2 md:grid-cols-2">
{visibleWarnings.map((warning) => {
const active = isWarningActive(warning, now);
const validityLabel = active
? warning.validTo && t("warnings.dashboard.validUntil", { date: formatDateTime(warning.validTo, language) })
: warning.validFrom && t("warnings.dashboard.validFrom", { date: formatDateTime(warning.validFrom, language) });
return (
<article
key={warning.id}
className="rounded-card border border-warning/20 bg-surface px-3.5 py-3"
>
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.16em] text-warning">
{t(active ? "warnings.dashboard.active" : "warnings.dashboard.upcoming")}
{warning.level !== null && ` · ${t("warnings.level", { level: warning.level })}`}
</p>
<p className="mt-1 text-sm font-semibold text-foreground">
{warning.title || t("warnings.genericMeteo")}
</p>
{validityLabel && (
<p className="mt-1.5 flex items-center gap-1.5 text-xs text-muted">
<CalendarClock className="size-3.5" aria-hidden="true" />
{validityLabel}
</p>
)}
</article>
);
})}
</div>
{hiddenWarningsCount > 0 && (
<p className="mt-3 text-xs font-medium text-warning">
{t("warnings.dashboard.more", { count: hiddenWarningsCount })}
</p>
)}
</motion.section>
);
}