Files
smart-home-tablet/components/TopBar.tsx
Cosmo 98fdcafb73
All checks were successful
Deploy to Coolify / deploy (push) Successful in 3s
fix: weather modal, remove tasks/savings, fix HA controls, safe-area BottomNav
2026-04-22 10:42:41 +00:00

188 lines
8.1 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 { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import ThemeToggle from "./ThemeToggle";
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 <= 300) return "🌧️";
if (c >= 200 && c <= 210) return "⛈️";
if (c >= 210 && c <= 260) return "❄️";
return "🌤️";
}
function getDayName(dateStr: string): string {
const d = new Date(dateStr + "T12:00:00");
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
if (d.toDateString() === today.toDateString()) return "Сег";
if (d.toDateString() === tomorrow.toDateString()) return "Завт";
return d.toLocaleDateString("ru-RU", { weekday: "short" });
}
interface Props {
isDark: boolean;
onToggleTheme: () => void;
weather: any;
isDemo?: boolean;
}
export default function TopBar({ isDark, onToggleTheme, weather, isDemo }: Props) {
const [time, setTime] = useState("");
const [date, setDate] = useState("");
const [showModal, setShowModal] = useState(false);
useEffect(() => {
const update = () => {
const now = new Date();
setTime(now.toLocaleTimeString("ru-RU", { hour: "2-digit", minute: "2-digit" }));
setDate(now.toLocaleDateString("ru-RU", { weekday: "short", day: "numeric", month: "long" }));
};
update();
const id = setInterval(update, 1000);
return () => clearInterval(id);
}, []);
const hasWeather = weather && weather.temp && weather.temp !== "—";
return (
<>
<motion.div
className="glass-card px-6 py-3 flex items-center justify-between no-select flex-shrink-0"
style={{ borderRadius: "20px" }}
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{/* Time & Date */}
<div className="flex items-baseline gap-4">
<span className="font-black tracking-tight leading-none" style={{ fontSize: "42px", color: "var(--text-primary)" }}>
{time}
</span>
<span className="text-sm font-medium capitalize" style={{ color: "var(--text-secondary)" }}>
{date}
</span>
</div>
{/* Weather pill — clickable */}
{hasWeather ? (
<motion.button
className="flex items-center gap-3 px-5 py-2 rounded-2xl cursor-pointer"
style={{ background: "rgba(59,130,246,0.1)", border: "1px solid rgba(59,130,246,0.22)" }}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.97 }}
onClick={() => setShowModal(true)}
>
<span className="text-2xl leading-none">{getWeatherEmoji(weather.weatherCode)}</span>
<div>
<div className="text-xl font-black leading-tight" style={{ color: "var(--text-primary)" }}>{weather.temp}°C</div>
<div className="text-xs leading-tight" style={{ color: "var(--text-secondary)" }}>{weather.desc}</div>
</div>
<span className="text-xs ml-1" style={{ color: "var(--text-secondary)", opacity: 0.6 }}></span>
</motion.button>
) : (
<div className="text-sm px-4 py-2 rounded-2xl" style={{ color: "var(--text-secondary)", background: "rgba(255,255,255,0.04)" }}>
🌤 {weather ? "Загрузка..." : "—"}
</div>
)}
{/* Theme toggle + Demo badge */}
<div className="flex items-center gap-3">
{isDemo && (
<motion.span
className="text-xs px-2.5 py-1 rounded-full font-semibold"
style={{ background: "rgba(245,158,11,0.15)", color: "#f59e0b", border: "1px solid rgba(245,158,11,0.3)" }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
Demo
</motion.span>
)}
<ThemeToggle isDark={isDark} onToggle={onToggleTheme} />
</div>
</motion.div>
{/* Weather Modal */}
<AnimatePresence>
{showModal && weather && (
<motion.div
className="fixed inset-0 z-50 flex items-center justify-center p-6"
style={{ background: "rgba(0,0,0,0.7)", backdropFilter: "blur(12px)" }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowModal(false)}
>
<motion.div
className="glass-card p-8 w-full max-w-lg"
style={{ borderRadius: "28px", background: "rgba(15,15,25,0.9)", border: "1px solid rgba(255,255,255,0.12)" }}
initial={{ scale: 0.9, y: 20 }}
animate={{ scale: 1, y: 0 }}
exit={{ scale: 0.9, y: 20 }}
onClick={e => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold" style={{ color: "var(--text-primary)" }}>🌍 Санкт-Петербург</h2>
<button
onClick={() => setShowModal(false)}
className="w-9 h-9 rounded-full flex items-center justify-center text-lg"
style={{ background: "rgba(255,255,255,0.08)", color: "var(--text-secondary)" }}
></button>
</div>
{/* Current */}
<div className="flex items-center gap-5 mb-8">
<span style={{ fontSize: "72px", lineHeight: 1 }}>{getWeatherEmoji(weather.weatherCode)}</span>
<div>
<div className="font-black" style={{ fontSize: "64px", lineHeight: 1, color: "var(--text-primary)" }}>
{weather.temp}°
</div>
<div className="text-lg" style={{ color: "var(--text-secondary)" }}>{weather.desc}</div>
<div className="flex gap-4 mt-2 text-sm" style={{ color: "var(--text-secondary)" }}>
{weather.feelsLike && weather.feelsLike !== "—" && <span>Ощущается {weather.feelsLike}°</span>}
{weather.humidity && weather.humidity !== "—" && <span>💧 {weather.humidity}%</span>}
{weather.windSpeed && weather.windSpeed !== "—" && <span>💨 {weather.windSpeed} км/ч</span>}
</div>
</div>
</div>
{/* Forecast */}
{weather.forecast && weather.forecast.length > 0 && (
<div>
<div className="text-xs font-semibold uppercase tracking-widest mb-3" style={{ color: "var(--text-secondary)" }}>
Прогноз
</div>
<div className="grid gap-2">
{weather.forecast.map((day: any, i: number) => (
<div key={i} className="flex items-center justify-between px-4 py-3 rounded-2xl"
style={{ background: "rgba(255,255,255,0.04)", border: "1px solid rgba(255,255,255,0.06)" }}>
<span className="text-sm font-semibold w-16" style={{ color: "var(--text-primary)" }}>
{getDayName(day.date)}
</span>
<span className="text-2xl">{getWeatherEmoji(day.weatherCode)}</span>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>{day.desc}</span>
<span className="text-sm font-semibold" style={{ color: "var(--text-primary)" }}>
{day.maxTemp}° / <span style={{ color: "var(--text-secondary)" }}>{day.minTemp}°</span>
</span>
</div>
))}
</div>
</div>
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
</>
);
}