Files
digital-home-dashboard/src/components/widgets/WeatherWidget.tsx

127 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { RefreshCw } from "lucide-react";
interface CurrentWeather {
temp: number; feelsLike: number; humidity: number; windKmh: number; desc: string; icon: string;
}
interface DayForecast {
date: string; maxTemp: number; minTemp: number; desc: string; icon: string; precipProb: number;
}
interface WeatherData {
current: CurrentWeather;
forecast: DayForecast[];
}
const DAY_NAMES = ["Вс","Пн","Вт","Ср","Чт","Пт","Сб"];
export function WeatherWidget() {
const [data, setData] = useState<WeatherData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [selected, setSelected] = useState(0);
const fetch_ = async () => {
setLoading(true); setError(false);
try {
const res = await fetch("/api/weather");
if (!res.ok) throw new Error();
setData(await res.json());
} catch { setError(true); }
finally { setLoading(false); }
};
useEffect(() => { fetch_(); }, []);
return (
<div className="card card-blue relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-blue-950/40 via-transparent to-indigo-950/30 pointer-events-none" />
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-500/5 rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
<div className="relative p-6">
{loading ? (
<div className="animate-pulse space-y-4">
<div className="flex items-end gap-4">
<div className="h-20 w-20 bg-white/5 rounded-2xl" />
<div className="space-y-2">
<div className="h-12 w-28 bg-white/5 rounded-lg" />
<div className="h-4 w-40 bg-white/5 rounded" />
</div>
</div>
<div className="flex gap-2">{[...Array(7)].map((_,i) => <div key={i} className="flex-1 h-24 bg-white/5 rounded-2xl" />)}</div>
</div>
) : error ? (
<div className="text-slate-500 text-sm py-8 text-center">Нет данных о погоде</div>
) : data && (
<div className="space-y-5">
{/* Current */}
<div className="flex items-start justify-between">
<div>
<p className="text-slate-400 text-xs font-medium uppercase tracking-wider mb-2">Санкт-Петербург</p>
<div className="flex items-start gap-3">
<span className="text-6xl font-extralight text-white leading-none">
{selected === 0 ? data.current.temp : data.forecast[selected].maxTemp}°
</span>
<div className="mt-1">
<span className="text-3xl">{selected === 0 ? data.current.icon : data.forecast[selected].icon}</span>
</div>
</div>
<p className="text-slate-300 text-sm mt-2">{selected === 0 ? data.current.desc : data.forecast[selected].desc}</p>
{selected !== 0 && (
<p className="text-slate-500 text-xs mt-1">мин {data.forecast[selected].minTemp}°</p>
)}
</div>
{selected === 0 && (
<div className="flex flex-col gap-2 text-xs text-slate-400">
<div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span>🌡</span>
<span>Ощущ. {data.current.feelsLike}°</span>
</div>
<div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span>💧</span>
<span>{data.current.humidity}%</span>
</div>
<div className="flex items-center gap-1.5 bg-white/5 rounded-lg px-2.5 py-1.5">
<span>💨</span>
<span>{data.current.windKmh} км/ч</span>
</div>
<button onClick={fetch_} className="flex items-center gap-1.5 bg-white/5 hover:bg-white/10 rounded-lg px-2.5 py-1.5 transition-colors">
<RefreshCw className={`w-3 h-3 ${loading ? "animate-spin" : ""}`} />
<span>Обновить</span>
</button>
</div>
)}
</div>
{/* 7-day forecast */}
<div className="grid grid-cols-7 gap-1.5">
{data.forecast.map((day: DayForecast, i: number) => {
const d = new Date(day.date + "T12:00:00");
return (
<button
key={day.date}
onClick={() => setSelected(i)}
className={`flex flex-col items-center gap-1.5 py-3 px-1 rounded-2xl transition-all ${
selected === i
? "bg-white/10 ring-1 ring-white/20 shadow-lg"
: "hover:bg-white/5"
}`}
>
<span className="text-[10px] font-medium text-slate-400">{i === 0 ? "Сег" : DAY_NAMES[d.getDay()]}</span>
<span className="text-xl">{day.icon}</span>
<span className="text-xs font-semibold text-white">{day.maxTemp}°</span>
<span className="text-[10px] text-slate-600">{day.minTemp}°</span>
{day.precipProb > 20 && (
<span className="text-[9px] text-blue-400 font-medium">{day.precipProb}%</span>
)}
</button>
);
})}
</div>
</div>
)}
</div>
</div>
);
}