Files
smart-home-tablet/components/cards/WeatherCard.tsx
Cosmo 088cd35ea6
All checks were successful
Deploy to Coolify / deploy (push) Successful in 5s
feat: new layout, rooms row, fix weather+HA, fix BottomNav overflow
- Remove TasksCard and SavingsCard from home tab
- New grid layout: lights+thermostat row 1, purifier+weather row 2
- Add RoomsRow component with room navigation
- Fix HA entity mapping: fan.zhimi_rmb1_9528_air_purifier → fan.air_purifier
- Add real entity aliases for HA route
- Fix weather route: add timeout, better error handling
- Fix BottomNav: use 100dvh + flex-shrink-0
- TopBar: accept isDemo prop, show Demo badge in header
- WeatherCard: compact prop, better loading/error states
- globals.css: add no-scrollbar utility
2026-04-22 10:33:20 +00:00

175 lines
5.3 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 { motion } from "framer-motion";
import { Droplets, Wind } from "lucide-react";
function getWeatherEmoji(code: string): string {
const c = parseInt(code);
if (c === 113) return "☀️";
if (c === 116) return "⛅";
if (c === 119 || c === 122) return "☁️";
if (c >= 176 && c <= 182) return "🌦️";
if (c >= 185 && c <= 200) return "🌧️";
if (c >= 200 && c <= 210) return "⛈️";
if (c >= 210 && c <= 260) return "❄️";
if (c >= 260 && c <= 300) return "🌨️";
if (c >= 300 && c <= 400) return "🌧️";
return "🌤️";
}
function formatDate(dateStr: string): string {
const d = new Date(dateStr + "T12:00:00");
return d.toLocaleDateString("ru-RU", { weekday: "short", day: "numeric" });
}
interface Props {
weather: any;
compact?: boolean;
}
export default function WeatherCard({ weather, compact }: Props) {
if (!weather) {
return (
<motion.div
className="glass-card p-6 h-full flex flex-col items-center justify-center gap-3"
style={{
background: "rgba(59,130,246,0.03)",
border: "1px solid rgba(59,130,246,0.1)",
}}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="text-3xl animate-pulse">🌤</div>
<div className="text-sm" style={{ color: "var(--text-secondary)" }}>
Загрузка погоды...
</div>
</motion.div>
);
}
const hasData = weather.temp && weather.temp !== "—";
if (!hasData) {
return (
<motion.div
className="glass-card p-6 h-full flex flex-col items-center justify-center gap-2"
style={{
background: "rgba(59,130,246,0.03)",
border: "1px solid rgba(59,130,246,0.1)",
}}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div className="text-3xl">🌧</div>
<div className="text-sm text-center" style={{ color: "var(--text-secondary)" }}>
Нет данных о погоде
</div>
</motion.div>
);
}
return (
<motion.div
className="glass-card p-5 h-full flex flex-col"
style={{
background: "rgba(59,130,246,0.05)",
border: "1px solid rgba(59,130,246,0.15)",
}}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.35, delay: 0.21 }}
whileHover={{ scale: 1.01 }}
>
{/* Location */}
<div
className="text-xs font-medium mb-2"
style={{ color: "var(--text-secondary)" }}
>
📍 Санкт-Петербург
</div>
{/* Current weather */}
<div className="flex items-center gap-3 mb-3">
<div className={compact ? "text-3xl leading-none" : "text-4xl leading-none"}>
{getWeatherEmoji(weather.weatherCode)}
</div>
<div>
<div
className="font-black leading-none"
style={{
fontSize: compact ? "36px" : "42px",
color: "var(--text-primary)",
}}
>
{weather.temp}°
</div>
<div
className="text-xs mt-0.5"
style={{ color: "var(--text-secondary)" }}
>
{weather.desc}
</div>
</div>
</div>
{/* Stats */}
<div className="flex gap-3 mb-3 flex-wrap">
<div className="flex items-center gap-1">
<Droplets size={12} color="#3b82f6" />
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
{weather.humidity}%
</span>
</div>
<div className="flex items-center gap-1">
<Wind size={12} color="#8b5cf6" />
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>
{weather.windSpeed} км/ч
</span>
</div>
<div className="text-xs" style={{ color: "var(--text-secondary)" }}>
Ощущ. {weather.feelsLike}°
</div>
</div>
{/* Forecast */}
<div className="flex gap-2 mt-auto">
{(weather.forecast || []).slice(0, 3).map((day: any, i: number) => (
<motion.div
key={day.date}
className="flex-1 rounded-xl p-2 text-center"
style={{
background:
i === 0
? "rgba(59,130,246,0.14)"
: "rgba(255,255,255,0.04)",
border:
i === 0
? "1px solid rgba(59,130,246,0.28)"
: "1px solid rgba(255,255,255,0.06)",
}}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 + i * 0.08 }}
>
<div
className="text-xs mb-0.5 font-medium"
style={{ color: "var(--text-secondary)" }}
>
{i === 0 ? "Сег." : formatDate(day.date)}
</div>
<div className="text-base mb-0.5">
{getWeatherEmoji(day.weatherCode)}
</div>
<div
className="text-xs font-bold"
style={{ color: "var(--text-primary)" }}
>
{day.maxTemp}°/{day.minTemp}°
</div>
</motion.div>
))}
</div>
</motion.div>
);
}