All checks were successful
Deploy to Coolify / deploy (push) Successful in 44s
- Next.js 14 + TypeScript + Tailwind CSS - Glassmorphism design with ambient orbs - Cards: Light x2, Temperature, AirPurifier, Tasks, Weather, Savings - Home Assistant integration (demo mode if no token) - Vikunja tasks API - Pulse savings API - wttr.in weather - Framer Motion animations - Dark/light theme toggle - Bottom navigation - Dockerfile for deployment
120 lines
3.4 KiB
TypeScript
120 lines
3.4 KiB
TypeScript
"use client";
|
||
|
||
import { motion } from "framer-motion";
|
||
import { PiggyBank } from "lucide-react";
|
||
|
||
interface Saving {
|
||
id: number;
|
||
name: string;
|
||
current_amount: number;
|
||
target_amount: number;
|
||
color?: string;
|
||
icon?: string;
|
||
}
|
||
|
||
interface Props {
|
||
savings: Saving[];
|
||
}
|
||
|
||
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`;
|
||
return String(n);
|
||
}
|
||
|
||
export default function SavingsCard({ savings }: Props) {
|
||
return (
|
||
<motion.div
|
||
className="glass-card p-5 h-full flex flex-col"
|
||
style={{
|
||
background: "rgba(99,102,241,0.04)",
|
||
border: "1px solid rgba(99,102,241,0.12)",
|
||
}}
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
whileHover={{ scale: 1.01 }}
|
||
>
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<PiggyBank size={18} color="#6366f1" />
|
||
<span
|
||
className="text-sm font-semibold"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
Накопления
|
||
</span>
|
||
</div>
|
||
|
||
<div className="flex-1 space-y-4">
|
||
{savings.map((s, i) => {
|
||
const pct = Math.min(
|
||
100,
|
||
Math.round((s.current_amount / s.target_amount) * 100)
|
||
);
|
||
const color = s.color || "#6366f1";
|
||
|
||
return (
|
||
<motion.div
|
||
key={s.id}
|
||
initial={{ opacity: 0, y: 10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ delay: i * 0.1 }}
|
||
>
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
{s.icon && <span className="text-base">{s.icon}</span>}
|
||
<span
|
||
className="text-xs font-medium"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
{s.name}
|
||
</span>
|
||
</div>
|
||
<span
|
||
className="text-xs font-semibold"
|
||
style={{ color }}
|
||
>
|
||
{pct}%
|
||
</span>
|
||
</div>
|
||
|
||
<div className="progress-bar">
|
||
<motion.div
|
||
className="progress-fill"
|
||
style={{
|
||
background: `linear-gradient(90deg, ${color}aa, ${color})`,
|
||
width: `${pct}%`,
|
||
}}
|
||
initial={{ width: "0%" }}
|
||
animate={{ width: `${pct}%` }}
|
||
transition={{
|
||
duration: 1,
|
||
delay: i * 0.2,
|
||
ease: "easeOut",
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
<div
|
||
className="flex justify-between text-xs mt-1"
|
||
style={{ color: "var(--text-secondary)" }}
|
||
>
|
||
<span>{formatAmount(s.current_amount)} ₽</span>
|
||
<span>цель: {formatAmount(s.target_amount)} ₽</span>
|
||
</div>
|
||
</motion.div>
|
||
);
|
||
})}
|
||
|
||
{savings.length === 0 && (
|
||
<div
|
||
className="text-center py-6 text-sm"
|
||
style={{ color: "var(--text-secondary)" }}
|
||
>
|
||
Нет данных о накоплениях
|
||
</div>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
);
|
||
}
|