Files
digital-home-dashboard/src/components/widgets/SavingsWidget.tsx
Cosmo 9bf8f114e2
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m6s
feat: add SavingsWidget + GitActivityWidget
2026-04-16 10:02:34 +00:00

108 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
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 { useEffect, useState, useCallback } from "react";
import { RefreshCw, PiggyBank } from "lucide-react";
interface Goal {
name: string;
amount: number;
}
interface SavingsData {
goals: Goal[];
total: number;
error?: string;
}
function formatMoney(n: number): string {
return n.toLocaleString("ru-RU") + " ₽";
}
// Целевые суммы (максимум для прогресс-бара)
const TARGETS: Record<string, number> = {
"🏠 Квартира": 500000,
"✈️ Отпуск": 200000,
};
export function SavingsWidget() {
const [data, setData] = useState<SavingsData | null>(null);
const [loading, setLoading] = useState(true);
const fetchData = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/savings", { cache: "no-store" });
setData(await res.json());
} catch {
setData({ goals: [], total: 0, error: "Ошибка загрузки" });
} finally {
setLoading(false);
}
}, []);
useEffect(() => { fetchData(); }, [fetchData]);
return (
<div className="card p-5 space-y-4" style={{ borderTop: "2px solid #10b981" }}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: "rgba(16,185,129,0.15)" }}>
<PiggyBank className="w-4 h-4" style={{ color: "#10b981" }} />
</div>
<span className="text-sm font-medium text-slate-300">Накопления</span>
</div>
<button onClick={fetchData} className="text-slate-600 hover:text-slate-300 transition-colors">
<RefreshCw className={`w-3.5 h-3.5 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
{loading ? (
<div className="space-y-3 animate-pulse">
{[1, 2].map(i => <div key={i} className="h-12 bg-white/5 rounded-xl" />)}
</div>
) : data?.error ? (
<div className="text-xs text-slate-600 text-center py-4">{data.error}</div>
) : (
<>
{/* Total */}
<div className="flex justify-between items-center py-2 px-3 rounded-xl" style={{ background: "rgba(16,185,129,0.08)" }}>
<span className="text-xs text-slate-400">Итого накоплено</span>
<span className="text-base font-bold text-white">{formatMoney(data?.total || 0)}</span>
</div>
{/* Goals */}
<div className="space-y-3">
{(data?.goals || []).map(goal => {
const target = TARGETS[goal.name];
const pct = target ? Math.min(100, (goal.amount / target) * 100) : null;
const color = goal.name.includes("Квартир") ? "#6366f1" :
goal.name.includes("Отпуск") ? "#f59e0b" : "#10b981";
return (
<div key={goal.name} className="space-y-1.5">
<div className="flex justify-between text-xs">
<span className="text-slate-300">{goal.name}</span>
<div className="flex items-center gap-1.5">
<span className="text-white font-medium">{formatMoney(goal.amount)}</span>
{target && <span className="text-slate-600">/ {formatMoney(target)}</span>}
</div>
</div>
{pct !== null && (
<div className="h-1.5 rounded-full" style={{ background: "rgba(255,255,255,0.06)" }}>
<div
className="h-full rounded-full transition-all duration-700"
style={{ width: `${pct}%`, background: color, boxShadow: `0 0 8px ${color}50` }}
/>
</div>
)}
{target && (
<div className="text-[10px] text-slate-600 text-right">{pct?.toFixed(0)}% от цели</div>
)}
</div>
);
})}
</div>
</>
)}
</div>
);
}