197 lines
8.5 KiB
TypeScript
197 lines
8.5 KiB
TypeScript
import React from 'react';
|
|
import { motion } from 'framer-motion';
|
|
import { GlassCard } from '@/components/ui/GlassCard';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Progress } from '@/components/ui/progress';
|
|
import {
|
|
Wallet,
|
|
TrendingUp,
|
|
Home,
|
|
Car,
|
|
Utensils,
|
|
ShoppingCart,
|
|
Target,
|
|
ArrowRight,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
import { cn, formatCurrency } from '@/lib/utils';
|
|
|
|
// Mock Data
|
|
const budgetData = {
|
|
limit: 15000,
|
|
spent: 12500,
|
|
categories: [
|
|
{ name: 'Housing', icon: Home, spent: 6500, limit: 7000, color: 'bg-blue-500' },
|
|
{ name: 'Food & Dining', icon: Utensils, spent: 2800, limit: 3000, color: 'bg-green-500' },
|
|
{ name: 'Transportation', icon: Car, spent: 1200, limit: 1500, color: 'bg-orange-500' },
|
|
{ name: 'Shopping', icon: ShoppingCart, spent: 2000, limit: 1500, color: 'bg-pink-500' }, // Over budget
|
|
],
|
|
goals: [
|
|
{ name: 'Buy a House', target: 800000, current: 200000, color: 'bg-purple-500' },
|
|
{ name: 'Retirement', target: 2000000, current: 150000, color: 'bg-indigo-500' },
|
|
]
|
|
};
|
|
|
|
export default function BudgetManager() {
|
|
const percentage = Math.min(100, (budgetData.spent / budgetData.limit) * 100);
|
|
const circumference = 2 * Math.PI * 120; // Radius 120
|
|
const strokeDashoffset = circumference - (percentage / 100) * circumference;
|
|
|
|
return (
|
|
<div className="max-w-6xl mx-auto space-y-8 pb-20 animate-fade-in">
|
|
{/* Header */}
|
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900">Monthly Budget & Goals</h1>
|
|
<p className="text-sm sm:text-base text-slate-500">Track your spending and save for the future.</p>
|
|
</div>
|
|
<Button className="w-full sm:w-auto gap-2 bg-slate-900 text-white hover:bg-slate-800 rounded-full">
|
|
<Wallet className="w-4 h-4" /> Edit Budget
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 sm:gap-8">
|
|
{/* Main Budget Ring */}
|
|
<GlassCard className="col-span-1 lg:col-span-1 flex flex-col items-center justify-center p-6 sm:p-8 relative overflow-hidden">
|
|
<div className="relative w-64 h-64 sm:w-72 sm:h-72 flex items-center justify-center">
|
|
{/* Background Circle */}
|
|
<svg className="w-full h-full transform -rotate-90">
|
|
<circle
|
|
cx="50%"
|
|
cy="50%"
|
|
r="45%"
|
|
stroke="currentColor"
|
|
strokeWidth="24"
|
|
fill="transparent"
|
|
className="text-slate-100"
|
|
/>
|
|
{/* Progress Circle */}
|
|
<motion.circle
|
|
initial={{ strokeDashoffset: circumference }}
|
|
animate={{ strokeDashoffset }}
|
|
transition={{ duration: 1.5, ease: "easeOut" }}
|
|
cx="50%"
|
|
cy="50%"
|
|
r="45%"
|
|
stroke="currentColor"
|
|
strokeWidth="24"
|
|
fill="transparent"
|
|
strokeDasharray={circumference}
|
|
strokeLinecap="round"
|
|
className={cn(
|
|
"text-blue-500 drop-shadow-lg",
|
|
percentage > 90 ? "text-red-500" : percentage > 75 ? "text-orange-500" : "text-blue-500"
|
|
)}
|
|
/>
|
|
</svg>
|
|
<div className="absolute flex flex-col items-center">
|
|
<span className="text-xs sm:text-sm text-slate-500 font-medium uppercase tracking-wider">Total Spent</span>
|
|
<span className="text-3xl sm:text-4xl font-bold text-slate-900 mt-1">
|
|
{formatCurrency(budgetData.spent)}
|
|
</span>
|
|
<span className="text-xs sm:text-sm text-slate-400 mt-1">
|
|
of {formatCurrency(budgetData.limit)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{percentage > 90 && (
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 10 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="mt-6 flex items-center gap-2 text-red-600 bg-red-50 px-4 py-2 rounded-full border border-red-100"
|
|
>
|
|
<AlertCircle className="w-4 h-4" />
|
|
<span className="text-sm font-medium">You've used {percentage.toFixed(0)}% of your budget!</span>
|
|
</motion.div>
|
|
)}
|
|
</GlassCard>
|
|
|
|
{/* Categories & Goals */}
|
|
<div className="col-span-1 lg:col-span-2 space-y-8">
|
|
{/* Categories Breakdown */}
|
|
<GlassCard className="p-6">
|
|
<h3 className="text-lg font-semibold mb-6 flex items-center gap-2">
|
|
<Utensils className="w-5 h-5 text-slate-500" /> Category Breakdown
|
|
</h3>
|
|
<div className="space-y-6">
|
|
{budgetData.categories.map((cat, index) => (
|
|
<motion.div
|
|
key={cat.name}
|
|
initial={{ opacity: 0, x: -20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: index * 0.1 }}
|
|
className="space-y-2"
|
|
>
|
|
<div className="flex justify-between items-center text-sm">
|
|
<div className="flex items-center gap-2">
|
|
<div className={cn("p-1.5 rounded-lg text-white", cat.color)}>
|
|
<cat.icon className="w-3.5 h-3.5" />
|
|
</div>
|
|
<span className="font-medium text-slate-700">{cat.name}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<span className="font-semibold">{formatCurrency(cat.spent)}</span>
|
|
<span className="text-slate-400">/ {formatCurrency(cat.limit)}</span>
|
|
</div>
|
|
</div>
|
|
<div className="h-2.5 bg-slate-100 rounded-full overflow-hidden">
|
|
<motion.div
|
|
initial={{ width: 0 }}
|
|
animate={{ width: `${Math.min(100, (cat.spent / cat.limit) * 100)}%` }}
|
|
transition={{ duration: 1, delay: 0.5 + (index * 0.1) }}
|
|
className={cn("h-full rounded-full", cat.color)}
|
|
/>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</GlassCard>
|
|
|
|
{/* Long-term Goals */}
|
|
<div>
|
|
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<Target className="w-5 h-5 text-slate-500" /> Long-term Planning
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{budgetData.goals.map((goal, index) => (
|
|
<GlassCard key={goal.name} className="p-5 relative overflow-hidden group">
|
|
<div className="relative z-10">
|
|
<div className="flex justify-between items-start mb-4">
|
|
<div>
|
|
<h4 className="font-semibold text-slate-900">{goal.name}</h4>
|
|
<div className="bg-green-100 text-green-700 text-xs px-2 py-0.5 rounded-full inline-block mt-1 font-medium">
|
|
On Track
|
|
</div>
|
|
</div>
|
|
<div className={cn("p-2 rounded-xl text-white opacity-80", goal.color)}>
|
|
<Target className="w-5 h-5" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-slate-500">Progress</span>
|
|
<span className="font-bold text-slate-900">{Math.round((goal.current / goal.target) * 100)}%</span>
|
|
</div>
|
|
<Progress value={(goal.current / goal.target) * 100} className="h-2" indicatorClassName={goal.color} />
|
|
<div className="flex justify-between text-xs text-slate-400 pt-1">
|
|
<span>{formatCurrency(goal.current)}</span>
|
|
<span>{formatCurrency(goal.target)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Hover Effect Background */}
|
|
<div className={cn(
|
|
"absolute inset-0 opacity-0 group-hover:opacity-10 transition-opacity duration-500",
|
|
goal.color
|
|
)} />
|
|
</GlassCard>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |