fix: stop treating rainfall total as current rain
This commit is contained in:
@@ -37,6 +37,7 @@ Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani for
|
|||||||
- Dodawaj `"use client"` tylko tam, gdzie komponent lub moduł korzysta z hooków, stanu przeglądarki albo interakcji.
|
- Dodawaj `"use client"` tylko tam, gdzie komponent lub moduł korzysta z hooków, stanu przeglądarki albo interakcji.
|
||||||
- Dane zewnętrzne pobieraj przez route handlery Next.js. Nie omijaj allowlisty w `app/api/imgw/[...path]/route.ts`.
|
- Dane zewnętrzne pobieraj przez route handlery Next.js. Nie omijaj allowlisty w `app/api/imgw/[...path]/route.ts`.
|
||||||
- Traktuj IMGW jako źródło bieżących pomiarów, hydro i ostrzeżeń. Prognozę Open-Meteo pokazuj oddzielnie jako prognozę modelową. Nie generuj fikcyjnych danych ani nie przedstawiaj prognozy jako pomiaru IMGW.
|
- Traktuj IMGW jako źródło bieżących pomiarów, hydro i ostrzeżeń. Prognozę Open-Meteo pokazuj oddzielnie jako prognozę modelową. Nie generuj fikcyjnych danych ani nie przedstawiaj prognozy jako pomiaru IMGW.
|
||||||
|
- `synop.suma_opadu` jest akumulowaną sumą opadu. Nie używaj jej jako sygnału, że pada w tej chwili, ani do sterowania animacją deszczu.
|
||||||
- GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim.
|
- GPS wymaga świadomej zgody użytkownika i HTTPS. Zaokrąglaj współrzędne przed użyciem i utrzymuj widoczną atrybucję OpenStreetMap dla reverse geocodingu Nominatim.
|
||||||
- Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją.
|
- Normalizuj zewnętrzne odpowiedzi i obsługuj `null`, puste pola oraz błędne wartości. Brak danych pokazuj jawnie zamiast uzupełniać estymacją.
|
||||||
- Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states.
|
- Dla pobierania danych używaj TanStack Query z sensownym `queryKey`, cache i retry. W UI zachowuj loading, error, retry oraz empty states.
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ Przeglądarka pobiera dane przez route handlery Next.js. Proxy IMGW w `app/api/i
|
|||||||
|
|
||||||
Publiczny endpoint synoptyczny IMGW udostępnia najnowszy pomiar, a nie pełną prognozę godzinową lub wielodniową i nie historię odczytów. Dlatego prognoza pochodzi z Open-Meteo i jest wyraźnie oddzielona od pomiarów IMGW. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`.
|
Publiczny endpoint synoptyczny IMGW udostępnia najnowszy pomiar, a nie pełną prognozę godzinową lub wielodniową i nie historię odczytów. Dlatego prognoza pochodzi z Open-Meteo i jest wyraźnie oddzielona od pomiarów IMGW. `wtr.` nie generuje fikcyjnych prognoz. Widok stacji prezentuje aktualne parametry i jawnie opisany snapshot pomiarowy. Brakujące wartości są oznaczane jako `Brak danych`.
|
||||||
|
|
||||||
|
Pole `suma_opadu` z endpointu synoptycznego jest prezentowane jako akumulowana suma opadu. Nie służy do wnioskowania, że w danej chwili pada, ani do uruchamiania animacji deszczu.
|
||||||
|
|
||||||
Czas aktualizacji parametrów hydrologicznych może się różnić. Interfejs pokazuje czas pomiaru, aby starsze odczyty nie wyglądały na bieżące.
|
Czas aktualizacji parametrów hydrologicznych może się różnić. Interfejs pokazuje czas pomiaru, aby starsze odczyty nie wyglądały na bieżące.
|
||||||
|
|
||||||
## Struktura projektu
|
## Struktura projektu
|
||||||
|
|||||||
@@ -3,15 +3,6 @@
|
|||||||
import { motion, useReducedMotion } from "framer-motion";
|
import { motion, useReducedMotion } from "framer-motion";
|
||||||
import type { SynopStation, WeatherMood } from "@/types/imgw";
|
import type { SynopStation, WeatherMood } from "@/types/imgw";
|
||||||
|
|
||||||
const rainDrops = Array.from({ length: 58 }, (_, index) => ({
|
|
||||||
left: `${(index * 29 + 7) % 100}%`,
|
|
||||||
delay: (index % 11) * 0.13,
|
|
||||||
duration: 0.72 + (index % 6) * 0.1,
|
|
||||||
height: 15 + (index % 5) * 5,
|
|
||||||
opacity: 0.48 + (index % 4) * 0.1,
|
|
||||||
width: index % 5 === 0 ? 2 : 1,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const windLines = Array.from({ length: 7 }, (_, index) => ({
|
const windLines = Array.from({ length: 7 }, (_, index) => ({
|
||||||
top: `${18 + index * 11}%`,
|
top: `${18 + index * 11}%`,
|
||||||
delay: index * 0.22,
|
delay: index * 0.22,
|
||||||
@@ -26,14 +17,11 @@ const stars = Array.from({ length: 16 }, (_, index) => ({
|
|||||||
|
|
||||||
export function WeatherEffects({ station, mood }: { station: SynopStation; mood: WeatherMood }) {
|
export function WeatherEffects({ station, mood }: { station: SynopStation; mood: WeatherMood }) {
|
||||||
const reduceMotion = useReducedMotion();
|
const reduceMotion = useReducedMotion();
|
||||||
const rainfall = station.rainfall ?? 0;
|
|
||||||
const isRaining = rainfall >= 0.1;
|
|
||||||
const visibleDrops = rainfall >= 5 ? rainDrops : rainfall >= 1 ? rainDrops.slice(0, 44) : rainDrops.slice(0, 30);
|
|
||||||
const isWindy = (station.windSpeed ?? 0) >= 8;
|
const isWindy = (station.windSpeed ?? 0) >= 8;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-[1] overflow-hidden">
|
<div aria-hidden="true" className="pointer-events-none absolute inset-0 z-[1] overflow-hidden">
|
||||||
{(mood === "cloudy" || mood === "rain") && (
|
{mood === "cloudy" && (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={reduceMotion ? undefined : { x: ["-4%", "5%", "-4%"], y: [0, 8, 0], scale: [1, 1.05, 1] }}
|
animate={reduceMotion ? undefined : { x: ["-4%", "5%", "-4%"], y: [0, 8, 0], scale: [1, 1.05, 1] }}
|
||||||
@@ -75,16 +63,6 @@ export function WeatherEffects({ station, mood }: { station: SynopStation; mood:
|
|||||||
style={{ top: line.top, width: line.width }}
|
style={{ top: line.top, width: line.width }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{isRaining && !reduceMotion && visibleDrops.map((drop, index) => (
|
|
||||||
<motion.span
|
|
||||||
key={index}
|
|
||||||
initial={{ y: "-8vh", opacity: 0 }}
|
|
||||||
animate={{ y: ["-8vh", "85vh"], x: [0, -16], opacity: [0, drop.opacity, 0.18] }}
|
|
||||||
transition={{ duration: drop.duration, delay: drop.delay, repeat: Infinity, ease: "linear" }}
|
|
||||||
className="absolute -top-10 rounded-full bg-slate-100/90 shadow-[0_0_6px_rgba(226,232,240,0.7)] blur-[0.25px]"
|
|
||||||
style={{ height: drop.height, left: drop.left, width: drop.width, transform: "rotate(14deg)" }}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{mood === "cold" && (
|
{mood === "cold" && (
|
||||||
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-cyan-100/20 to-transparent" />
|
<div className="absolute inset-x-0 bottom-0 h-28 bg-gradient-to-t from-cyan-100/20 to-transparent" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function WeatherHero({ station, locationName, distanceKm }: { station: Sy
|
|||||||
const metrics = [
|
const metrics = [
|
||||||
{ icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) },
|
{ icon: Droplets, label: t("weather.humidity"), value: formatHumidity(station.humidity, language) },
|
||||||
{ icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) },
|
{ icon: Wind, label: t("weather.wind"), value: formatWind(station.windSpeed, null, language) },
|
||||||
{ icon: Umbrella, label: t("weather.rainfall"), value: formatRainfall(station.rainfall, language) },
|
{ icon: Umbrella, label: t("weather.rainfallTotal"), value: formatRainfall(station.rainfall, language) },
|
||||||
{ icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) },
|
{ icon: Gauge, label: t("weather.pressure"), value: formatPressure(station.pressure, language) },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Cloud, CloudRain, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
|
import { Cloud, CloudSun, MoonStar, Snowflake, ThermometerSun, Wind } from "lucide-react";
|
||||||
import type { WeatherMood } from "@/types/imgw";
|
import type { WeatherMood } from "@/types/imgw";
|
||||||
|
|
||||||
export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) {
|
export function WeatherIcon({ mood, className = "" }: { mood: WeatherMood; className?: string }) {
|
||||||
const Icon = {
|
const Icon = {
|
||||||
warm: ThermometerSun,
|
warm: ThermometerSun,
|
||||||
cloudy: Cloud,
|
cloudy: Cloud,
|
||||||
rain: CloudRain,
|
|
||||||
wind: Wind,
|
wind: Wind,
|
||||||
cold: Snowflake,
|
cold: Snowflake,
|
||||||
night: MoonStar,
|
night: MoonStar,
|
||||||
|
|||||||
12
lib/i18n.tsx
12
lib/i18n.tsx
@@ -61,7 +61,7 @@ const translations = {
|
|||||||
"favorites.remove": "Usuń z ulubionych",
|
"favorites.remove": "Usuń z ulubionych",
|
||||||
"weather.humidity": "Wilgotność",
|
"weather.humidity": "Wilgotność",
|
||||||
"weather.wind": "Wiatr",
|
"weather.wind": "Wiatr",
|
||||||
"weather.rainfall": "Opad",
|
"weather.rainfall": "Suma opadu",
|
||||||
"weather.pressure": "Ciśnienie",
|
"weather.pressure": "Ciśnienie",
|
||||||
"weather.feelsLike": "Odczuwalna",
|
"weather.feelsLike": "Odczuwalna",
|
||||||
"weather.measurement": "pomiar",
|
"weather.measurement": "pomiar",
|
||||||
@@ -69,8 +69,6 @@ const translations = {
|
|||||||
"weather.calm": "Spokojne warunki",
|
"weather.calm": "Spokojne warunki",
|
||||||
"weather.humid": "Wilgotno",
|
"weather.humid": "Wilgotno",
|
||||||
"weather.strongWind": "Silny wiatr",
|
"weather.strongWind": "Silny wiatr",
|
||||||
"weather.rain": "Opady",
|
|
||||||
"weather.heavyRain": "Wyraźne opady",
|
|
||||||
"weather.airTemperature": "Temperatura",
|
"weather.airTemperature": "Temperatura",
|
||||||
"weather.windSpeed": "Prędkość wiatru",
|
"weather.windSpeed": "Prędkość wiatru",
|
||||||
"weather.rainfallTotal": "Suma opadu",
|
"weather.rainfallTotal": "Suma opadu",
|
||||||
@@ -79,7 +77,7 @@ const translations = {
|
|||||||
"weather.pressureDetail": "Ciśnienie atmosferyczne",
|
"weather.pressureDetail": "Ciśnienie atmosferyczne",
|
||||||
"weather.windSpeedDetail": "Bieżący odczyt IMGW",
|
"weather.windSpeedDetail": "Bieżący odczyt IMGW",
|
||||||
"weather.windDirectionDetail": "Kierunek napływu wiatru",
|
"weather.windDirectionDetail": "Kierunek napływu wiatru",
|
||||||
"weather.rainfallDetail": "Suma opadu z pomiaru IMGW",
|
"weather.rainfallDetail": "Akumulowana suma opadu z pomiaru IMGW. Nie oznacza, że pada w tej chwili.",
|
||||||
"weather.temperatureDetail": "Temperatura powietrza",
|
"weather.temperatureDetail": "Temperatura powietrza",
|
||||||
"forecast.label": "Prognoza modelowa",
|
"forecast.label": "Prognoza modelowa",
|
||||||
"forecast.title": "Najbliższe godziny i dni",
|
"forecast.title": "Najbliższe godziny i dni",
|
||||||
@@ -205,7 +203,7 @@ const translations = {
|
|||||||
"favorites.remove": "Remove from favourites",
|
"favorites.remove": "Remove from favourites",
|
||||||
"weather.humidity": "Humidity",
|
"weather.humidity": "Humidity",
|
||||||
"weather.wind": "Wind",
|
"weather.wind": "Wind",
|
||||||
"weather.rainfall": "Rainfall",
|
"weather.rainfall": "Rainfall total",
|
||||||
"weather.pressure": "Pressure",
|
"weather.pressure": "Pressure",
|
||||||
"weather.feelsLike": "Feels like",
|
"weather.feelsLike": "Feels like",
|
||||||
"weather.measurement": "measurement",
|
"weather.measurement": "measurement",
|
||||||
@@ -213,8 +211,6 @@ const translations = {
|
|||||||
"weather.calm": "Calm conditions",
|
"weather.calm": "Calm conditions",
|
||||||
"weather.humid": "Humid",
|
"weather.humid": "Humid",
|
||||||
"weather.strongWind": "Strong wind",
|
"weather.strongWind": "Strong wind",
|
||||||
"weather.rain": "Rainfall",
|
|
||||||
"weather.heavyRain": "Heavy rainfall",
|
|
||||||
"weather.airTemperature": "Temperature",
|
"weather.airTemperature": "Temperature",
|
||||||
"weather.windSpeed": "Wind speed",
|
"weather.windSpeed": "Wind speed",
|
||||||
"weather.rainfallTotal": "Rainfall total",
|
"weather.rainfallTotal": "Rainfall total",
|
||||||
@@ -223,7 +219,7 @@ const translations = {
|
|||||||
"weather.pressureDetail": "Atmospheric pressure",
|
"weather.pressureDetail": "Atmospheric pressure",
|
||||||
"weather.windSpeedDetail": "Current IMGW reading",
|
"weather.windSpeedDetail": "Current IMGW reading",
|
||||||
"weather.windDirectionDetail": "Direction the wind is coming from",
|
"weather.windDirectionDetail": "Direction the wind is coming from",
|
||||||
"weather.rainfallDetail": "Total rainfall from the IMGW reading",
|
"weather.rainfallDetail": "Accumulated rainfall total from the IMGW reading. It does not mean that it is raining right now.",
|
||||||
"weather.temperatureDetail": "Air temperature",
|
"weather.temperatureDetail": "Air temperature",
|
||||||
"forecast.label": "Model forecast",
|
"forecast.label": "Model forecast",
|
||||||
"forecast.title": "Upcoming hours and days",
|
"forecast.title": "Upcoming hours and days",
|
||||||
|
|||||||
@@ -160,7 +160,6 @@ export function getWindDirection(degrees: number) {
|
|||||||
|
|
||||||
export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood {
|
export function getWeatherMoodFromData(station: SynopStation, date = new Date()): WeatherMood {
|
||||||
const hour = date.getHours();
|
const hour = date.getHours();
|
||||||
if ((station.rainfall ?? 0) >= 0.1) return "rain";
|
|
||||||
if (hour < 6 || hour >= 21) return "night";
|
if (hour < 6 || hour >= 21) return "night";
|
||||||
if ((station.windSpeed ?? 0) >= 8) return "wind";
|
if ((station.windSpeed ?? 0) >= 8) return "wind";
|
||||||
if ((station.temperature ?? 15) <= 3) return "cold";
|
if ((station.temperature ?? 15) <= 3) return "cold";
|
||||||
@@ -170,8 +169,6 @@ export function getWeatherMoodFromData(station: SynopStation, date = new Date())
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWeatherDescription(station: SynopStation, language: Language = "pl") {
|
export function getWeatherDescription(station: SynopStation, language: Language = "pl") {
|
||||||
if ((station.rainfall ?? 0) >= 5) return translate(language, "weather.heavyRain");
|
|
||||||
if ((station.rainfall ?? 0) >= 0.1) return translate(language, "weather.rain");
|
|
||||||
if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind");
|
if ((station.windSpeed ?? 0) >= 8) return translate(language, "weather.strongWind");
|
||||||
if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid");
|
if ((station.humidity ?? 0) >= 90) return translate(language, "weather.humid");
|
||||||
return translate(language, "weather.calm");
|
return translate(language, "weather.calm");
|
||||||
@@ -181,7 +178,6 @@ export function moodGradient(mood: WeatherMood) {
|
|||||||
return {
|
return {
|
||||||
warm: "from-sky-400 via-blue-500 to-indigo-700",
|
warm: "from-sky-400 via-blue-500 to-indigo-700",
|
||||||
cloudy: "from-slate-600 via-slate-700 to-slate-900",
|
cloudy: "from-slate-600 via-slate-700 to-slate-900",
|
||||||
rain: "from-slate-600 via-blue-900 to-slate-950",
|
|
||||||
wind: "from-cyan-600 via-slate-600 to-blue-950",
|
wind: "from-cyan-600 via-slate-600 to-blue-950",
|
||||||
cold: "from-cyan-300 via-blue-500 to-indigo-900",
|
cold: "from-cyan-300 via-blue-500 to-indigo-900",
|
||||||
night: "from-slate-800 via-indigo-950 to-slate-950",
|
night: "from-slate-800 via-indigo-950 to-slate-950",
|
||||||
|
|||||||
@@ -115,4 +115,4 @@ export interface WeatherWarning {
|
|||||||
office: string | null;
|
office: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WeatherMood = "warm" | "cloudy" | "rain" | "wind" | "cold" | "night" | "mild";
|
export type WeatherMood = "warm" | "cloudy" | "wind" | "cold" | "night" | "mild";
|
||||||
|
|||||||
Reference in New Issue
Block a user