diff --git a/app/globals.css b/app/globals.css
index 0c7f81f..9529074 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,24 +1,24 @@
-@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
- --bg: #0a0a0f;
- --card-bg: rgba(255, 255, 255, 0.05);
+ --bg: #090912;
+ --card-bg: rgba(255, 255, 255, 0.04);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: rgba(255, 255, 255, 0.95);
- --text-secondary: rgba(255, 255, 255, 0.5);
+ --text-secondary: rgba(255, 255, 255, 0.45);
--accent: #6366f1;
--accent-2: #8b5cf6;
}
.light {
--bg: #f0f0f8;
- --card-bg: rgba(255, 255, 255, 0.8);
- --card-border: rgba(0, 0, 0, 0.08);
+ --card-bg: rgba(255, 255, 255, 0.75);
+ --card-border: rgba(0, 0, 0, 0.07);
--text-primary: rgba(15, 15, 30, 0.95);
- --text-secondary: rgba(15, 15, 30, 0.5);
+ --text-secondary: rgba(15, 15, 30, 0.45);
}
* {
@@ -49,7 +49,7 @@ body {
border: 1px solid var(--card-border);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
- border-radius: 20px;
+ border-radius: 24px;
transition: background 0.3s ease, border-color 0.3s ease;
}
@@ -61,34 +61,35 @@ body {
background: transparent;
}
::-webkit-scrollbar-thumb {
- background: rgba(99, 102, 241, 0.4);
+ background: rgba(99, 102, 241, 0.35);
border-radius: 2px;
}
-/* Custom toggle switch */
+/* Big toggle switch (60×32) */
.toggle-track {
position: relative;
- width: 52px;
- height: 28px;
- border-radius: 14px;
+ width: 60px;
+ height: 32px;
+ border-radius: 16px;
cursor: pointer;
transition: background-color 0.3s ease;
+ flex-shrink: 0;
}
.toggle-thumb {
position: absolute;
top: 3px;
left: 3px;
- width: 22px;
- height: 22px;
+ width: 26px;
+ height: 26px;
border-radius: 50%;
background: white;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.toggle-on .toggle-thumb {
- transform: translateX(24px);
+ transform: translateX(28px);
}
/* Custom range slider */
@@ -113,9 +114,9 @@ input[type='range']::-webkit-slider-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
- background: #6366f1;
+ background: #f59e0b;
cursor: pointer;
- box-shadow: 0 0 8px rgba(99, 102, 241, 0.6);
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.7);
transition: transform 0.15s ease;
}
@@ -127,10 +128,10 @@ input[type='range']::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
- background: #6366f1;
+ background: #f59e0b;
cursor: pointer;
border: none;
- box-shadow: 0 0 8px rgba(99, 102, 241, 0.6);
+ box-shadow: 0 0 10px rgba(245, 158, 11, 0.7);
}
/* Ambient orbs */
@@ -150,48 +151,78 @@ input[type='range']::-moz-range-thumb {
/* Progress bar */
.progress-bar {
- height: 8px;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.08);
+ height: 10px;
+ border-radius: 5px;
+ background: rgba(255, 255, 255, 0.07);
overflow: hidden;
}
.light .progress-bar {
- background: rgba(0, 0, 0, 0.08);
+ background: rgba(0, 0, 0, 0.07);
}
.progress-fill {
height: 100%;
- border-radius: 4px;
+ border-radius: 5px;
background: linear-gradient(90deg, #6366f1, #8b5cf6);
- transition: width 0.6s cubic-bezier(0.34, 1.56, 0.64, 1);
+ transition: width 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
/* Glow effects */
-.glow-indigo {
- box-shadow: 0 0 20px rgba(99, 102, 241, 0.3);
-}
-
-.glow-emerald {
- box-shadow: 0 0 20px rgba(16, 185, 129, 0.3);
-}
-
-.glow-rose {
- box-shadow: 0 0 20px rgba(244, 63, 94, 0.3);
-}
-
.glow-amber {
- box-shadow: 0 0 20px rgba(245, 158, 11, 0.3);
+ box-shadow: 0 0 24px rgba(245, 158, 11, 0.4);
+}
+
+.glow-blue {
+ box-shadow: 0 0 24px rgba(59, 130, 246, 0.4);
+}
+
+.glow-green {
+ box-shadow: 0 0 24px rgba(16, 185, 129, 0.4);
+}
+
+.glow-purple {
+ box-shadow: 0 0 24px rgba(139, 92, 246, 0.4);
}
/* Modal backdrop */
.modal-backdrop {
position: fixed;
inset: 0;
- background: rgba(0, 0, 0, 0.6);
- backdrop-filter: blur(8px);
+ background: rgba(0, 0, 0, 0.65);
+ backdrop-filter: blur(10px);
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
}
+
+/* Spin animation for air purifier */
+@keyframes spin-slow {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.spin-slow {
+ animation: spin-slow 4s linear infinite;
+}
+
+/* Orb animations */
+@keyframes orbMove1 {
+ 0%, 100% { transform: translate(0, 0) scale(1); }
+ 33% { transform: translate(40px, -30px) scale(1.05); }
+ 66% { transform: translate(-20px, 20px) scale(0.97); }
+}
+@keyframes orbMove2 {
+ 0%, 100% { transform: translate(0, 0) scale(1); }
+ 33% { transform: translate(-50px, 30px) scale(1.08); }
+ 66% { transform: translate(30px, -20px) scale(0.95); }
+}
+@keyframes orbMove3 {
+ 0%, 100% { transform: translate(0, 0) scale(1); }
+ 50% { transform: translate(20px, 40px) scale(1.04); }
+}
+@keyframes orbMove4 {
+ 0%, 100% { transform: translate(0, 0) scale(1); }
+ 50% { transform: translate(-30px, -20px) scale(1.06); }
+}
diff --git a/app/page.tsx b/app/page.tsx
index 2ee4489..8c36e7f 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -10,8 +10,20 @@ import AirPurifierCard from "@/components/cards/AirPurifierCard";
import TasksCard from "@/components/cards/TasksCard";
import WeatherCard from "@/components/cards/WeatherCard";
import SavingsCard from "@/components/cards/SavingsCard";
+import WeatherSavingsCard from "@/components/cards/WeatherSavingsCard";
import { useHA, useWeather, useTasks, useSavings } from "@/hooks/useHA";
+// Stagger container variants
+const containerVariants = {
+ hidden: {},
+ visible: { transition: { staggerChildren: 0.07 } },
+};
+
+const cardVariants = {
+ hidden: { opacity: 0, y: 20 },
+ visible: { opacity: 1, y: 0, transition: { duration: 0.35 } },
+};
+
export default function Home() {
const [isDark, setIsDark] = useState(true);
const [activeTab, setActiveTab] = useState("home");
@@ -54,39 +66,47 @@ export default function Home() {
>
{/* Ambient orbs */}
+
@@ -109,7 +129,7 @@ export default function Home() {
exit={{ opacity: 0 }}
>
+ {/* ═══════════════ HOME TAB ═══════════════ */}
{activeTab === "home" && (
- {/* Row 1 */}
- {/* Свет Гостиная */}
-
+ {/* 3-col grid:
+ Row 1: [Свет] [Климат] [Задачи]
+ Row 2: [Воздух ×2] [Погода+Накопления]
+ */}
+
+ {/* Свет Гостиная */}
+
+
+
- {/* Свет Спальня */}
-
+ {/* Термостат */}
+
+
+
- {/* Температура */}
-
+ {/* Задачи */}
+
+
+
- {/* Очиститель воздуха */}
-
+ {/* Очиститель воздуха — 2 колонки */}
+
+
+
- {/* Row 2 */}
- {/* Задачи — 2 колонки */}
-
-
+ {/* Погода + Накопления — правый нижний */}
+
+
+
-
- {/* Погода */}
-
-
- {/* Накопления */}
-
)}
+ {/* ═══════════════ DEVICES TAB ═══════════════ */}
{activeTab === "devices" && (
-
-
-
-
+
)}
+ {/* ═══════════════ TASKS TAB ═══════════════ */}
{activeTab === "tasks" && (
)}
+ {/* ═══════════════ SETTINGS TAB ═══════════════ */}
{activeTab === "settings" && (
{[
- {
- label: "HA URL",
- value:
- process.env.NEXT_PUBLIC_APP_URL
- ? "Настроен"
- : "http://192.168.31.110:8123",
- },
+ { label: "HA URL", value: "Настроен" },
{ label: "HA Token", value: isDemo ? "❌ Не настроен" : "✅ Настроен" },
{ label: "Vikunja", value: "✅ Подключён" },
{ label: "Pulse API", value: "✅ Подключён" },
].map((item) => (
-
+
{item.label}
-
+
{item.value}
diff --git a/components/BottomNav.tsx b/components/BottomNav.tsx
index 3c7d66e..5373c1c 100644
--- a/components/BottomNav.tsx
+++ b/components/BottomNav.tsx
@@ -9,16 +9,17 @@ interface Props {
}
const TABS = [
- { id: "home", label: "Главная", icon: Home },
- { id: "devices", label: "Устройства", icon: Cpu },
- { id: "tasks", label: "Задачи", icon: CheckSquare },
- { id: "settings", label: "Настройки", icon: Settings },
+ { id: "home", label: "Главная", icon: Home, color: "#6366f1" },
+ { id: "devices", label: "Устройства", icon: Cpu, color: "#3b82f6" },
+ { id: "tasks", label: "Задачи", icon: CheckSquare, color: "#8b5cf6" },
+ { id: "settings", label: "Настройки", icon: Settings, color: "#10b981" },
];
export default function BottomNav({ active, onChange }: Props) {
return (
onChange(tab.id)}
- className="flex flex-col items-center gap-1 px-6 py-2 rounded-xl relative"
- whileTap={{ scale: 0.88 }}
- style={{
- background: isActive
- ? "rgba(99,102,241,0.15)"
- : "transparent",
- }}
+ className="flex flex-col items-center gap-1.5 px-8 py-2 rounded-2xl relative"
+ whileTap={{ scale: 0.85 }}
>
{isActive && (
)}
{tab.label}
diff --git a/components/TopBar.tsx b/components/TopBar.tsx
index d524ba0..64d85c7 100644
--- a/components/TopBar.tsx
+++ b/components/TopBar.tsx
@@ -36,7 +36,7 @@ export default function TopBar({ isDark, onToggleTheme, weather }: Props) {
);
setDate(
now.toLocaleDateString("ru-RU", {
- weekday: "long",
+ weekday: "short",
day: "numeric",
month: "long",
})
@@ -50,6 +50,7 @@ export default function TopBar({ isDark, onToggleTheme, weather }: Props) {
return (
{time}
@@ -70,41 +71,35 @@ export default function TopBar({ isDark, onToggleTheme, weather }: Props) {
- {/* Weather */}
+ {/* Weather pill */}
{weather && (
-
+
{getWeatherEmoji(weather.weatherCode)}
{weather.temp}°C
- Ощущается {weather.feelsLike}°
+ {weather.desc}
-
- {weather.desc}
-
)}
@@ -112,7 +107,7 @@ export default function TopBar({ isDark, onToggleTheme, weather }: Props) {
{weather?.demo && (
m.id === currentMode)?.color || "#06b6d4";
+ const activeMode = MODES.find((m) => m.id === currentMode) || MODES[0];
+ const accentColor = isOn ? activeMode.color : "rgba(255,255,255,0.3)";
return (
-
-
-
+ {/* Animated icon */}
+
+
-
-
-
-
+
+
+
+
+
Очиститель воздуха
- {isOn ? currentMode : "Выключен"}
+ {isOn ? activeMode.label : "Выключен"}
+ {/* Toggle */}
- {isOn && (
-
+ {/* Mode buttons */}
+
+ {isOn && (
+
+ {MODES.map((mode) => {
+ const isActive = currentMode === mode.id;
+ return (
+ handleMode(mode.id)}
+ className="flex-1 py-2.5 rounded-2xl text-sm font-semibold"
+ style={
+ isActive
+ ? {
+ background: `${mode.color}22`,
+ border: `1.5px solid ${mode.color}60`,
+ color: mode.color,
+ boxShadow: `0 0 14px ${mode.color}30`,
+ }
+ : {
+ background: "rgba(255,255,255,0.05)",
+ border: "1px solid rgba(255,255,255,0.08)",
+ color: "var(--text-secondary)",
+ }
+ }
+ whileTap={{ scale: 0.88 }}
+ >
+ {mode.label}
+
+ );
+ })}
+
+ )}
+
+
+ {/* Offline state bottom fill */}
+ {!isOn && (
+
{MODES.map((mode) => (
- handleMode(mode.id)}
- className="flex-1 py-2 rounded-xl text-xs font-medium"
- style={
- currentMode === mode.id
- ? {
- background: `${mode.color}25`,
- border: `1px solid ${mode.color}60`,
- color: mode.color,
- }
- : {
- background: "rgba(255,255,255,0.06)",
- border: "1px solid rgba(255,255,255,0.08)",
- color: "var(--text-secondary)",
- }
- }
- whileTap={{ scale: 0.9 }}
+ className="flex-1 py-2.5 rounded-2xl text-sm font-semibold text-center"
+ style={{
+ background: "rgba(255,255,255,0.03)",
+ border: "1px solid rgba(255,255,255,0.05)",
+ color: "rgba(255,255,255,0.15)",
+ }}
>
{mode.label}
-
+
))}
-
+
)}
);
diff --git a/components/cards/LightCard.tsx b/components/cards/LightCard.tsx
index f117d94..ce3d387 100644
--- a/components/cards/LightCard.tsx
+++ b/components/cards/LightCard.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState, useCallback } from "react";
-import { motion } from "framer-motion";
+import { motion, AnimatePresence } from "framer-motion";
import { Lightbulb } from "lucide-react";
import { toggleLight, setLightBrightness } from "@/lib/api";
import { getBrightnessPct, pctToBrightness } from "@/lib/ha";
@@ -47,50 +47,42 @@ export default function LightCard({
return (
+ {/* Top row: icon + toggle */}
-
-
-
-
-
- {name}
-
-
- {isOn ? (showSlider ? `${localBrightness}%` : "Включён") : "Выключен"}
-
-
+ {/* Icon */}
+
+
+
{/* Toggle */}
- {showSlider && isOn && (
-
+
-
- Яркость
- {localBrightness}%
-
-
-
-
setLocalBrightness(parseInt(e.target.value))}
- onMouseUp={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))}
- onTouchEnd={(e) => handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))}
- className="w-full relative z-10"
- style={{ background: "transparent" }}
- />
-
-
- )}
+ {name}
+
+
+ {isOn ? (showSlider ? `Яркость ${localBrightness}%` : "Включён") : "Выключен"}
+
+
+ {/* Brightness slider */}
+
+ {showSlider && isOn && (
+
+
+
+
setLocalBrightness(parseInt(e.target.value))}
+ onMouseUp={(e) =>
+ handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))
+ }
+ onTouchEnd={(e) =>
+ handleBrightnessChange(parseInt((e.target as HTMLInputElement).value))
+ }
+ className="w-full relative z-10"
+ style={{ background: "transparent" }}
+ />
+
+
+ )}
+
+
);
}
diff --git a/components/cards/SavingsCard.tsx b/components/cards/SavingsCard.tsx
index 08b8e29..ce17025 100644
--- a/components/cards/SavingsCard.tsx
+++ b/components/cards/SavingsCard.tsx
@@ -1,7 +1,7 @@
"use client";
import { motion } from "framer-motion";
-import { PiggyBank } from "lucide-react";
+import { Target } from "lucide-react";
interface Saving {
id: number;
@@ -17,31 +17,51 @@ interface Props {
}
function formatAmount(n: number): string {
- if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
- if (n >= 1_000) return `${Math.round(n / 1_000)}K`;
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}М`;
+ if (n >= 1_000) return `${Math.round(n / 1_000)}К`;
return String(n);
}
+const COLORS = ["#f59e0b", "#3b82f6", "#10b981", "#8b5cf6", "#f43f5e"];
+
export default function SavingsCard({ savings }: Props) {
return (
-
-
-
+
- Накопления
-
+
+
+
+
+ Накопления
+
+
+ {savings.length} {savings.length === 1 ? "цель" : "целей"}
+
+
@@ -50,56 +70,57 @@ export default function SavingsCard({ savings }: Props) {
100,
Math.round((s.current_amount / s.target_amount) * 100)
);
- const color = s.color || "#6366f1";
+ const color = s.color || COLORS[i % COLORS.length];
return (
{s.icon && {s.icon}}
{s.name}
{pct}%
+ {/* Progress bar with glow */}
{formatAmount(s.current_amount)} ₽
- цель: {formatAmount(s.target_amount)} ₽
+ из {formatAmount(s.target_amount)} ₽
);
diff --git a/components/cards/TasksCard.tsx b/components/cards/TasksCard.tsx
index 546cbcd..e7f2831 100644
--- a/components/cards/TasksCard.tsx
+++ b/components/cards/TasksCard.tsx
@@ -2,7 +2,7 @@
import { useState, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
-import { CheckSquare, Square, Plus } from "lucide-react";
+import { CheckCircle2, Circle, Plus, ListTodo } from "lucide-react";
import { createTask, toggleTask } from "@/lib/api";
import AddTaskModal from "../AddTaskModal";
@@ -49,57 +49,84 @@ export default function TasksCard({ tasks, onUpdate }: Props) {
return (
<>
+ {/* Header */}
-
-
-
-
+
+
+
+
+
- Задачи сегодня
-
-
-
- {pending.length} осталось из {localTasks.length}
+ Задачи
+
+
+ {pending.length > 0
+ ? `${pending.length} из ${localTasks.length} осталось`
+ : "Всё готово!"}
+
+
+ {/* Add button */}
setModalOpen(true)}
- className="w-8 h-8 rounded-lg flex items-center justify-center"
+ className="w-10 h-10 rounded-xl flex items-center justify-center"
style={{
- background: "linear-gradient(135deg, #6366f1, #8b5cf6)",
- boxShadow: "0 0 12px rgba(99,102,241,0.4)",
+ background: "linear-gradient(135deg, #8b5cf6, #6366f1)",
+ boxShadow: "0 0 18px rgba(139,92,246,0.45)",
}}
- whileTap={{ scale: 0.85 }}
+ whileTap={{ scale: 0.82 }}
>
-
+
-
-
+ {/* Task list */}
+
+
{localTasks.length === 0 && (
- 🎉
+ 🎉
- Всё сделано!
+ Всё сделано на сегодня!
+ setModalOpen(true)}
+ className="mt-2 px-5 py-2.5 rounded-xl text-sm font-semibold"
+ style={{
+ background: "linear-gradient(135deg, rgba(139,92,246,0.25), rgba(99,102,241,0.2))",
+ border: "1px solid rgba(139,92,246,0.35)",
+ color: "#8b5cf6",
+ }}
+ whileTap={{ scale: 0.9 }}
+ >
+ + Добавить задачу
+
)}
@@ -107,32 +134,36 @@ export default function TasksCard({ tasks, onUpdate }: Props) {
handleToggle(task)}
whileTap={{ scale: 0.97 }}
>
- {task.done ? (
-
- ) : (
-
- )}
+
+ {task.done ? (
+
+ ) : (
+
+ )}
+
diff --git a/components/cards/TemperatureCard.tsx b/components/cards/TemperatureCard.tsx
index d54cf09..b73fe0b 100644
--- a/components/cards/TemperatureCard.tsx
+++ b/components/cards/TemperatureCard.tsx
@@ -22,6 +22,7 @@ export default function TemperatureCard({
}: Props) {
const [target, setTarget] = useState(targetTemp || 22);
const isHeating = state === "heat";
+ const isActive = state !== "off";
const adjust = useCallback(
async (delta: number) => {
@@ -33,97 +34,122 @@ export default function TemperatureCard({
[target, entityId, onUpdate]
);
- const tempDiff = currentTemp ? currentTemp - target : 0;
+ const accentColor = isHeating ? "#f43f5e" : "#3b82f6";
+ const accentBg = isHeating ? "rgba(244,63,94,0.08)" : "rgba(59,130,246,0.08)";
+ const accentBorder = isHeating ? "rgba(244,63,94,0.2)" : "rgba(59,130,246,0.2)";
+ const accentGlow = isHeating ? "rgba(244,63,94,0.25)" : "rgba(59,130,246,0.2)";
return (
+ {/* Icon row */}
-
-
-
-
-
- Термостат
-
-
- {isHeating ? "Нагрев" : "Ожидание"}
-
-
+
+
+
-
-
- {currentTemp?.toFixed(1) ?? "—"}°
-
-
- текущая
-
+ {/* Status badge */}
+
+ {isHeating ? "Нагрев" : isActive ? "Охлаждение" : "Выкл"}
-
+ {/* Current temperature — BIG */}
+
+ {currentTemp?.toFixed(1) ?? "—"}°
+
+
- Целевая температура
+ текущая температура
+
+
+ {/* Target temperature controls */}
+
+
+ Цель
+
+
adjust(-0.5)}
- className="w-8 h-8 rounded-lg flex items-center justify-center"
- style={{ background: "rgba(255,255,255,0.08)" }}
- whileTap={{ scale: 0.85 }}
+ className="w-10 h-10 rounded-xl flex items-center justify-center"
+ style={{
+ background: "rgba(255,255,255,0.08)",
+ border: "1px solid rgba(255,255,255,0.1)",
+ }}
+ whileTap={{ scale: 0.82 }}
>
-
+
+
{target}°
+
adjust(0.5)}
- className="w-8 h-8 rounded-lg flex items-center justify-center"
- style={{ background: "rgba(99,102,241,0.2)" }}
- whileTap={{ scale: 0.85 }}
+ className="w-10 h-10 rounded-xl flex items-center justify-center"
+ style={{
+ background: `${accentColor}22`,
+ border: `1px solid ${accentColor}50`,
+ }}
+ whileTap={{ scale: 0.82 }}
>
-
+
diff --git a/components/cards/WeatherCard.tsx b/components/cards/WeatherCard.tsx
index fc4bc10..8c3287e 100644
--- a/components/cards/WeatherCard.tsx
+++ b/components/cards/WeatherCard.tsx
@@ -30,14 +30,11 @@ export default function WeatherCard({ weather }: Props) {
if (!weather) {
return (
-
+
Загрузка погоды...
@@ -46,91 +43,93 @@ export default function WeatherCard({ weather }: Props) {
return (
-
+ {/* Location */}
+
+ 📍 Санкт-Петербург
+
+
+ {/* Current weather */}
+
+
+ {getWeatherEmoji(weather.weatherCode)}
+
- 🌍 Санкт-Петербург
+ {weather.temp}°
-
-
- {getWeatherEmoji(weather.weatherCode)}
-
-
-
- {weather.temp}°C
-
-
- {weather.desc}
-
-
+
+ {weather.desc}
-
-
-
-
- {weather.humidity}%
-
-
-
-
-
- {weather.windSpeed} км/ч
-
-
+
+
+ {/* Stats */}
+
+
+
+
+ {weather.humidity}%
+
+
+
+
+
+ {weather.windSpeed} км/ч
+
+
+
+ Ощущается {weather.feelsLike}°
{/* Forecast */}
- {(weather.forecast || []).map((day: any, i: number) => (
+ {(weather.forecast || []).slice(0, 3).map((day: any, i: number) => (
- {i === 0 ? "Сегодня" : formatDate(day.date)}
-
-
- {getWeatherEmoji(day.weatherCode)}
+ {i === 0 ? "Сег." : formatDate(day.date)}
+ {getWeatherEmoji(day.weatherCode)}
- {day.maxTemp}° / {day.minTemp}°
+ {day.maxTemp}°/{day.minTemp}°
))}
diff --git a/components/cards/WeatherSavingsCard.tsx b/components/cards/WeatherSavingsCard.tsx
new file mode 100644
index 0000000..a6a6458
--- /dev/null
+++ b/components/cards/WeatherSavingsCard.tsx
@@ -0,0 +1,85 @@
+"use client";
+
+import { useState } from "react";
+import { motion, AnimatePresence } from "framer-motion";
+import WeatherCard from "./WeatherCard";
+import SavingsCard from "./SavingsCard";
+
+interface Props {
+ weather: any;
+ savings: any[];
+}
+
+export default function WeatherSavingsCard({ weather, savings }: Props) {
+ const [view, setView] = useState<"weather" | "savings">("weather");
+
+ return (
+
+ {/* Toggle pills */}
+
+ {[
+ { id: "weather", label: "🌤 Погода" },
+ { id: "savings", label: "💰 Цели" },
+ ].map((tab) => (
+ setView(tab.id as "weather" | "savings")}
+ className="flex-1 py-1.5 rounded-xl text-xs font-semibold relative"
+ style={{
+ color: view === tab.id ? "var(--text-primary)" : "var(--text-secondary)",
+ }}
+ whileTap={{ scale: 0.95 }}
+ >
+ {view === tab.id && (
+
+ )}
+ {tab.label}
+
+ ))}
+
+
+ {/* Content */}
+
+
+ {view === "weather" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ );
+}