feat: build production-ready wtr weather PWA

This commit is contained in:
zv
2026-06-01 18:43:56 +02:00
commit 840555f4f5
60 changed files with 9052 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
"use client";
import { motion } from "framer-motion";
import { CalendarClock, MapPinned, Waves, CloudLightning } from "lucide-react";
import type { WeatherWarning } from "@/types/imgw";
import { formatDateTime } from "@/lib/weather-utils";
import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
export function WarningCard({ warning, index = 0 }: { warning: WeatherWarning; index?: number }) {
const Icon = warning.kind === "hydro" ? Waves : CloudLightning;
const level = warning.level;
const levelLabel = level === -1 ? "Susza hydrologiczna" : level === null ? "Poziom nieokreślony" : `Stopień ${level}`;
const areasLabel = warning.areas.length > 8
? `${warning.areas.slice(0, 8).join(", ")} i ${warning.areas.length - 8} więcej`
: warning.areas.join("; ");
return (
<motion.article initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: Math.min(index * 0.04, 0.4), duration: 0.35 }}>
<Card className="h-full overflow-hidden p-5">
<div className="flex items-start justify-between gap-3">
<div className="rounded-2xl bg-amber-500/15 p-2.5 text-amber-700 dark:text-amber-300"><Icon className="size-5" /></div>
<span className={cn("rounded-full border px-2.5 py-1 text-xs font-semibold", level === -1 ? "border-orange-300/40 bg-orange-400/15 text-orange-800 dark:text-orange-200" : "border-amber-300/40 bg-amber-400/15 text-amber-800 dark:text-amber-200")}>{levelLabel}</span>
</div>
<p className="mt-5 text-xs font-semibold uppercase tracking-[0.15em] text-slate-500 dark:text-slate-400">{warning.kind === "hydro" ? "Hydrologiczne" : "Meteorologiczne"}</p>
<h2 className="mt-2 text-lg font-semibold tracking-tight">{warning.title}</h2>
{warning.description && <p className="mt-3 line-clamp-5 text-sm leading-6 text-slate-600 dark:text-slate-300">{warning.description}</p>}
<div className="mt-5 space-y-2 text-xs text-slate-500 dark:text-slate-400">
<p className="flex items-start gap-2"><CalendarClock className="mt-0.5 size-3.5 shrink-0" />{formatDateTime(warning.validFrom)} {warning.validTo ? formatDateTime(warning.validTo) : "do odwołania"}</p>
<p className="flex items-start gap-2"><MapPinned className="mt-0.5 size-3.5 shrink-0" />{areasLabel || "Obszar nieokreślony"}</p>
</div>
{warning.probability !== null && <p className="mt-4 text-xs font-medium text-slate-600 dark:text-slate-300">Prawdopodobieństwo: {warning.probability}%</p>}
</Card>
</motion.article>
);
}