"use client";
import { useState, useMemo } from "react";
import {
Wallet,
Home,
Car,
Utensils,
ShoppingBag,
Gamepad2,
PiggyBank,
Heart,
Check,
AlertTriangle,
RotateCcw,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Slider } from "@/components/ui/slider";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
type Category = {
id: string;
name: string;
icon: React.ReactNode;
color: string;
percentage: number;
};
type Template = {
id: string;
name: string;
description: string;
allocations: Record<string, number>;
};
const templates: Template[] = [
{
id: "balanced",
name: "Balanced",
description: "50/30/20 rule",
allocations: { housing: 30, food: 15, transport: 10, shopping: 10, entertainment: 10, savings: 20, health: 5 },
},
{
id: "saver",
name: "Saver",
description: "Focus on savings",
allocations: { housing: 25, food: 15, transport: 10, shopping: 5, entertainment: 5, savings: 35, health: 5 },
},
{
id: "lifestyle",
name: "Lifestyle",
description: "More flexibility",
allocations: { housing: 30, food: 20, transport: 10, shopping: 15, entertainment: 15, savings: 5, health: 5 },
},
];
const categoryIcons: Record<string, React.ReactNode> = {
housing: <Home className="h-4 w-4" />,
food: <Utensils className="h-4 w-4" />,
transport: <Car className="h-4 w-4" />,
shopping: <ShoppingBag className="h-4 w-4" />,
entertainment: <Gamepad2 className="h-4 w-4" />,
savings: <PiggyBank className="h-4 w-4" />,
health: <Heart className="h-4 w-4" />,
};
const initialCategories: Category[] = [
{ id: "housing", name: "Housing", icon: categoryIcons.housing, color: "bg-foreground", percentage: 30 },
{ id: "food", name: "Food", icon: categoryIcons.food, color: "bg-foreground/80", percentage: 15 },
{ id: "transport", name: "Transport", icon: categoryIcons.transport, color: "bg-foreground/60", percentage: 10 },
{ id: "shopping", name: "Shopping", icon: categoryIcons.shopping, color: "bg-foreground/50", percentage: 10 },
{ id: "entertainment", name: "Entertainment", icon: categoryIcons.entertainment, color: "bg-foreground/40", percentage: 10 },
{ id: "savings", name: "Savings", icon: categoryIcons.savings, color: "bg-foreground/30", percentage: 20 },
{ id: "health", name: "Health", icon: categoryIcons.health, color: "bg-foreground/20", percentage: 5 },
];
export const title = "React Dialog Block Budget Allocator";
export default function DialogBudgetAllocator() {
const [open, setOpen] = useState(false);
const [categories, setCategories] = useState<Category[]>(initialCategories);
const [selectedTemplate, setSelectedTemplate] = useState<string>("");
const [totalBudget] = useState(5000);
const totalAllocated = useMemo(() => {
return categories.reduce((sum, cat) => sum + cat.percentage, 0);
}, [categories]);
const remaining = 100 - totalAllocated;
const isOverBudget = remaining < 0;
const isUnderBudget = remaining > 0;
const isBalanced = remaining === 0;
const handleSliderChange = (categoryId: string, value: number[]) => {
setCategories((prev) =>
prev.map((cat) =>
cat.id === categoryId ? { ...cat, percentage: value[0] } : cat
)
);
setSelectedTemplate("");
};
const handleTemplateChange = (templateId: string) => {
const template = templates.find((t) => t.id === templateId);
if (template) {
setCategories((prev) =>
prev.map((cat) => ({
...cat,
percentage: template.allocations[cat.id] || 0,
}))
);
setSelectedTemplate(templateId);
}
};
const handleReset = () => {
setCategories(initialCategories);
setSelectedTemplate("");
};
const handleSave = () => {
console.log("Budget allocation saved:", categories);
setOpen(false);
};
const handleClose = () => {
setOpen(false);
};
const formatCurrency = (percentage: number) => {
const amount = (totalBudget * percentage) / 100;
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
minimumFractionDigits: 0,
}).format(amount);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Wallet className="mr-2 h-4 w-4" />
Allocate Budget
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Wallet className="h-5 w-5" />
Budget Allocator
</DialogTitle>
<DialogDescription>
Distribute your ${totalBudget.toLocaleString()} monthly budget across categories.
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Template selector */}
<div className="flex items-center gap-2">
<Select value={selectedTemplate} onValueChange={handleTemplateChange}>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Choose a template" />
</SelectTrigger>
<SelectContent>
{templates.map((template) => (
<SelectItem key={template.id} value={template.id}>
{template.name} — {template.description}
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="icon" onClick={handleReset}>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
{/* Visual breakdown bar */}
<div className="space-y-2">
<div className="h-3 w-full rounded-full overflow-hidden flex border">
{categories.map((cat) => (
<div
key={cat.id}
className={cn("h-full transition-all duration-300", cat.color)}
style={{ width: `${(cat.percentage / Math.max(totalAllocated, 100)) * 100}%` }}
/>
))}
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
{isOverBudget
? `${Math.abs(remaining)}% over budget`
: isUnderBudget
? `${remaining}% remaining`
: "Perfectly balanced"}
</span>
<span className={cn(
"font-medium tabular-nums",
isOverBudget && "text-destructive"
)}>
{totalAllocated}%
</span>
</div>
</div>
{/* Category sliders */}
<div className="space-y-4">
{categories.map((category) => (
<div key={category.id} className="space-y-2">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-muted-foreground">
{category.icon}
<span className="text-sm text-foreground">{category.name}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<span className="text-muted-foreground tabular-nums w-16 text-right">
{formatCurrency(category.percentage)}
</span>
<span className="font-medium tabular-nums w-10 text-right">
{category.percentage}%
</span>
</div>
</div>
<Slider
value={[category.percentage]}
onValueChange={(value) => handleSliderChange(category.id, value)}
min={0}
max={50}
step={1}
/>
</div>
))}
</div>
{isOverBudget && (
<p className="text-sm text-destructive flex items-center gap-2">
<AlertTriangle className="h-4 w-4" />
Reduce allocations to stay within budget.
</p>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={isOverBudget}>
<Check className="mr-2 h-4 w-4" />
Save Budget
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}