redesign: modern dark dashboard WOW effect - gradients, animations, new layout

This commit is contained in:
Cosmo
2026-04-16 08:06:13 +00:00
parent cd126135ef
commit 7aa02290d9
13 changed files with 296 additions and 315 deletions

View File

@@ -21,7 +21,7 @@ export function WeatherWidget() {
const [error, setError] = useState(false);
const [selected, setSelected] = useState(0);
const fetchData = async () => {
const fetch_ = async () => {
setLoading(true); setError(false);
try {
const res = await fetch("/api/weather");
@@ -31,38 +31,16 @@ export function WeatherWidget() {
finally { setLoading(false); }
};
useEffect(() => { fetchData(); }, []);
const c = data?.current;
const day = data?.forecast?.[selected];
const formatDay = (dateStr: string, idx: number) => {
if (idx === 0) return "Сег";
const d = new Date(dateStr + "T12:00:00");
return DAY_NAMES[d.getDay()];
};
useEffect(() => { fetch_(); }, []);
return (
<div className="card card-accent-blue relative overflow-hidden">
{/* Decorative background */}
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/20 via-transparent to-violet-900/10 pointer-events-none" />
<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">
{/* Header */}
<div className="flex items-center justify-between mb-5">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-blue-400 uppercase tracking-wider">Погода</span>
<span className="text-xs text-slate-500">· Санкт-Петербург</span>
</div>
<button onClick={fetchData} className="text-slate-600 hover:text-slate-300 transition-colors p-1 rounded-lg hover:bg-white/5">
<RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
{error ? (
<div className="text-slate-500 text-sm text-center py-8">Нет данных о погоде</div>
) : loading ? (
<div className="space-y-4 animate-pulse">
{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">
@@ -70,62 +48,78 @@ export function WeatherWidget() {
<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-xl" />)}</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>
) : c && day ? (
<>
{/* Current weather */}
<div className="flex items-start justify-between mb-6">
) : 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>
<div className="flex items-baseline gap-3 mb-1">
<span className="text-7xl font-extralight text-white leading-none">
{selected === 0 ? c.temp : day.maxTemp}°
<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>
<span className="text-4xl">{selected === 0 ? c.icon : day.icon}</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 mt-2">{selected === 0 ? c.desc : day.desc}</p>
{selected === 0 && (
<p className="text-slate-500 text-xs mt-0.5">Ощущается как {c.feelsLike}°</p>
<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="text-right space-y-2 mt-2">
<div className="flex items-center gap-2 justify-end text-sm">
<span className="text-slate-500 text-xs">Влажность</span>
<span className="text-blue-400 font-medium">💧 {c.humidity}%</span>
<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-2 justify-end text-sm">
<span className="text-slate-500 text-xs">Ветер</span>
<span className="text-cyan-400 font-medium">💨 {c.windKmh} км/ч</span>
<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((f, i) => (
<button
key={f.date}
onClick={() => setSelected(i)}
className={`flex flex-col items-center gap-1.5 p-2 rounded-xl transition-all ${
selected === i
? "bg-white/10 ring-1 ring-white/20"
: "hover:bg-white/5"
}`}
>
<span className="text-[10px] font-medium text-slate-400">{formatDay(f.date, i)}</span>
<span className="text-xl">{f.icon}</span>
<span className="text-xs font-semibold text-white">{f.maxTemp}°</span>
<span className="text-[10px] text-slate-600">{f.minTemp}°</span>
{f.precipProb > 20 && (
<span className="text-[9px] text-blue-400 font-medium">{f.precipProb}%</span>
)}
</button>
))}
{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>
</>
) : null}
</div>
)}
</div>
</div>
);