feat: initial commit - finanzas app
Complete personal finance management application with: - Dashboard with financial metrics and alerts - Credit card management and payments - Fixed and variable debt tracking - Monthly budget planning - Intelligent alert system - Responsive design with Tailwind CSS Tech stack: Next.js 14, TypeScript, Zustand, Recharts 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
123
components/cards/CreditCardWidget.tsx
Normal file
123
components/cards/CreditCardWidget.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
'use client'
|
||||
|
||||
import { CreditCard } from '@/lib/types'
|
||||
import { formatCurrency, getCardUtilization, getDaysUntil, calculateNextClosingDate, calculateNextDueDate } from '@/lib/utils'
|
||||
import { Pencil, Trash2 } from 'lucide-react'
|
||||
|
||||
interface CreditCardWidgetProps {
|
||||
card: CreditCard
|
||||
onEdit: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
export function CreditCardWidget({ card, onEdit, onDelete }: CreditCardWidgetProps) {
|
||||
const utilization = getCardUtilization(card.currentBalance, card.creditLimit)
|
||||
const nextClosing = calculateNextClosingDate(card.closingDay)
|
||||
const nextDue = calculateNextDueDate(card.dueDay)
|
||||
const daysUntilClosing = getDaysUntil(nextClosing)
|
||||
const daysUntilDue = getDaysUntil(nextDue)
|
||||
|
||||
const getUtilizationColor = (util: number): string => {
|
||||
if (util < 30) return 'bg-emerald-500'
|
||||
if (util < 70) return 'bg-amber-500'
|
||||
return 'bg-rose-500'
|
||||
}
|
||||
|
||||
const getUtilizationTextColor = (util: number): string => {
|
||||
if (util < 30) return 'text-emerald-400'
|
||||
if (util < 70) return 'text-amber-400'
|
||||
return 'text-rose-400'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative overflow-hidden rounded-2xl p-6 text-white shadow-lg transition-transform hover:scale-[1.02]"
|
||||
style={{
|
||||
aspectRatio: '1.586',
|
||||
background: `linear-gradient(135deg, ${card.color} 0%, ${adjustColor(card.color, -30)} 100%)`,
|
||||
}}
|
||||
>
|
||||
{/* Decorative circles */}
|
||||
<div className="absolute -right-8 -top-8 h-32 w-32 rounded-full bg-white/10" />
|
||||
<div className="absolute -bottom-12 -left-12 h-40 w-40 rounded-full bg-white/5" />
|
||||
|
||||
{/* Header with card name and actions */}
|
||||
<div className="relative flex items-start justify-between">
|
||||
<h3 className="text-lg font-semibold tracking-wide">{card.name}</h3>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={onEdit}
|
||||
className="rounded-full p-1.5 transition-colors hover:bg-white/20"
|
||||
aria-label="Editar tarjeta"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className="rounded-full p-1.5 transition-colors hover:bg-white/20"
|
||||
aria-label="Eliminar tarjeta"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card number */}
|
||||
<div className="relative mt-8">
|
||||
<p className="font-mono text-2xl tracking-widest">
|
||||
**** **** **** {card.lastFourDigits}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Balance */}
|
||||
<div className="relative mt-6">
|
||||
<p className="text-sm text-white/70">Balance actual</p>
|
||||
<p className="text-2xl font-bold">{formatCurrency(card.currentBalance)}</p>
|
||||
</div>
|
||||
|
||||
{/* Utilization badge */}
|
||||
<div className="relative mt-4 flex items-center gap-3">
|
||||
<div className="flex items-center gap-2 rounded-full bg-black/30 px-3 py-1">
|
||||
<div className={`h-2 w-2 rounded-full ${getUtilizationColor(utilization)}`} />
|
||||
<span className={`text-sm font-medium ${getUtilizationTextColor(utilization)}`}>
|
||||
{utilization.toFixed(0)}% usado
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-sm text-white/60">
|
||||
de {formatCurrency(card.creditLimit)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Footer with closing and due dates */}
|
||||
<div className="relative mt-4 flex justify-between text-xs text-white/70">
|
||||
<div>
|
||||
<span className="block">Cierre</span>
|
||||
<span className="font-medium text-white">
|
||||
{card.closingDay} ({daysUntilClosing === 0 ? 'hoy' : daysUntilClosing > 0 ? `en ${daysUntilClosing} días` : `hace ${Math.abs(daysUntilClosing)} días`})
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="block">Vencimiento</span>
|
||||
<span className="font-medium text-white">
|
||||
{card.dueDay} ({daysUntilDue === 0 ? 'hoy' : daysUntilDue > 0 ? `en ${daysUntilDue} días` : `hace ${Math.abs(daysUntilDue)} días`})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajusta el brillo de un color hexadecimal
|
||||
* @param color - Color en formato hex (#RRGGBB)
|
||||
* @param amount - Cantidad a ajustar (negativo para oscurecer, positivo para aclarar)
|
||||
* @returns Color ajustado en formato hex
|
||||
*/
|
||||
function adjustColor(color: string, amount: number): string {
|
||||
const hex = color.replace('#', '')
|
||||
const r = Math.max(0, Math.min(255, parseInt(hex.substring(0, 2), 16) + amount))
|
||||
const g = Math.max(0, Math.min(255, parseInt(hex.substring(2, 4), 16) + amount))
|
||||
const b = Math.max(0, Math.min(255, parseInt(hex.substring(4, 6), 16) + amount))
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
|
||||
}
|
||||
Reference in New Issue
Block a user