feat: add SavingsWidget + GitActivityWidget
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m6s
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m6s
This commit is contained in:
107
src/components/widgets/SavingsWidget.tsx
Normal file
107
src/components/widgets/SavingsWidget.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user