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:
242
components/cards/CreditCardForm.tsx
Normal file
242
components/cards/CreditCardForm.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { CreditCard } from '@/lib/types'
|
||||
import { X, Check } from 'lucide-react'
|
||||
|
||||
interface CreditCardFormProps {
|
||||
initialData?: Partial<CreditCard>
|
||||
onSubmit: (data: Omit<CreditCard, 'id'>) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
const DEFAULT_COLOR = '#6366f1'
|
||||
|
||||
export function CreditCardForm({ initialData, onSubmit, onCancel }: CreditCardFormProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
name: initialData?.name ?? '',
|
||||
lastFourDigits: initialData?.lastFourDigits ?? '',
|
||||
closingDay: initialData?.closingDay ?? 1,
|
||||
dueDay: initialData?.dueDay ?? 10,
|
||||
currentBalance: initialData?.currentBalance ?? 0,
|
||||
creditLimit: initialData?.creditLimit ?? 0,
|
||||
color: initialData?.color ?? DEFAULT_COLOR,
|
||||
})
|
||||
|
||||
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: Record<string, string> = {}
|
||||
|
||||
if (!formData.name.trim()) {
|
||||
newErrors.name = 'El nombre es requerido'
|
||||
}
|
||||
|
||||
if (!formData.lastFourDigits.trim()) {
|
||||
newErrors.lastFourDigits = 'Los últimos 4 dígitos son requeridos'
|
||||
} else if (!/^\d{4}$/.test(formData.lastFourDigits)) {
|
||||
newErrors.lastFourDigits = 'Debe ser exactamente 4 dígitos numéricos'
|
||||
}
|
||||
|
||||
if (formData.closingDay < 1 || formData.closingDay > 31) {
|
||||
newErrors.closingDay = 'El día debe estar entre 1 y 31'
|
||||
}
|
||||
|
||||
if (formData.dueDay < 1 || formData.dueDay > 31) {
|
||||
newErrors.dueDay = 'El día debe estar entre 1 y 31'
|
||||
}
|
||||
|
||||
if (formData.creditLimit <= 0) {
|
||||
newErrors.creditLimit = 'El límite de crédito debe ser mayor a 0'
|
||||
}
|
||||
|
||||
if (formData.currentBalance < 0) {
|
||||
newErrors.currentBalance = 'El balance no puede ser negativo'
|
||||
}
|
||||
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
}
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (validateForm()) {
|
||||
onSubmit(formData)
|
||||
}
|
||||
}
|
||||
|
||||
const updateField = (field: keyof typeof formData, value: string | number) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
||||
// Clear error when user starts typing
|
||||
if (errors[field]) {
|
||||
setErrors((prev) => {
|
||||
const newErrors = { ...prev }
|
||||
delete newErrors[field]
|
||||
return newErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleLastFourDigitsChange = (value: string) => {
|
||||
// Only allow digits and max 4 characters
|
||||
const digitsOnly = value.replace(/\D/g, '').slice(0, 4)
|
||||
updateField('lastFourDigits', digitsOnly)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="space-y-4 rounded-xl bg-slate-800 p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{initialData ? 'Editar Tarjeta' : 'Nueva Tarjeta'}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-full p-1 text-slate-400 transition-colors hover:bg-slate-700 hover:text-white"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
{/* Card Name */}
|
||||
<div className="sm:col-span-2">
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Nombre de la tarjeta
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => updateField('name', e.target.value)}
|
||||
placeholder="Ej: Visa Banco Galicia"
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.name && <p className="mt-1 text-sm text-rose-400">{errors.name}</p>}
|
||||
</div>
|
||||
|
||||
{/* Last 4 Digits */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Últimos 4 dígitos
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={formData.lastFourDigits}
|
||||
onChange={(e) => handleLastFourDigitsChange(e.target.value)}
|
||||
placeholder="1234"
|
||||
maxLength={4}
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.lastFourDigits && (
|
||||
<p className="mt-1 text-sm text-rose-400">{errors.lastFourDigits}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Color Picker */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">Color</label>
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="color"
|
||||
value={formData.color}
|
||||
onChange={(e) => updateField('color', e.target.value)}
|
||||
className="h-10 w-20 cursor-pointer rounded-lg border border-slate-600 bg-slate-700"
|
||||
/>
|
||||
<span className="text-sm text-slate-400">{formData.color}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Closing Day */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Día de cierre
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={31}
|
||||
value={formData.closingDay}
|
||||
onChange={(e) => updateField('closingDay', parseInt(e.target.value) || 1)}
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.closingDay && (
|
||||
<p className="mt-1 text-sm text-rose-400">{errors.closingDay}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Due Day */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Día de vencimiento
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={31}
|
||||
value={formData.dueDay}
|
||||
onChange={(e) => updateField('dueDay', parseInt(e.target.value) || 1)}
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.dueDay && <p className="mt-1 text-sm text-rose-400">{errors.dueDay}</p>}
|
||||
</div>
|
||||
|
||||
{/* Credit Limit */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Límite de crédito
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={formData.creditLimit || ''}
|
||||
onChange={(e) => updateField('creditLimit', parseFloat(e.target.value) || 0)}
|
||||
placeholder="0.00"
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.creditLimit && (
|
||||
<p className="mt-1 text-sm text-rose-400">{errors.creditLimit}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Current Balance */}
|
||||
<div>
|
||||
<label className="mb-1 block text-sm font-medium text-slate-300">
|
||||
Balance actual
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={formData.currentBalance || ''}
|
||||
onChange={(e) => updateField('currentBalance', parseFloat(e.target.value) || 0)}
|
||||
placeholder="0.00"
|
||||
className="w-full rounded-lg border border-slate-600 bg-slate-700 px-4 py-2 text-white placeholder-slate-400 focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500/20"
|
||||
/>
|
||||
{errors.currentBalance && (
|
||||
<p className="mt-1 text-sm text-rose-400">{errors.currentBalance}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="rounded-lg border border-slate-600 px-4 py-2 text-sm font-medium text-slate-300 transition-colors hover:bg-slate-700"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-2 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
{initialData ? 'Guardar cambios' : 'Crear tarjeta'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user