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)
149 lines
3.8 KiB
TypeScript
149 lines
3.8 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Check, Trash2 } from 'lucide-react'
|
|
import { Alert } from '@/lib/types'
|
|
import { cn } from '@/lib/utils'
|
|
import { AlertIcon } from './AlertIcon'
|
|
|
|
interface AlertItemProps {
|
|
alert: Alert
|
|
onMarkRead: () => void
|
|
onDelete: () => void
|
|
}
|
|
|
|
const severityStyles = {
|
|
info: 'text-blue-400',
|
|
warning: 'text-amber-400',
|
|
danger: 'text-red-400',
|
|
}
|
|
|
|
function getRelativeTime(date: string): string {
|
|
const now = new Date()
|
|
const alertDate = new Date(date)
|
|
const diffMs = now.getTime() - alertDate.getTime()
|
|
const diffMins = Math.floor(diffMs / (1000 * 60))
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffMins < 1) {
|
|
return 'ahora'
|
|
}
|
|
if (diffMins < 60) {
|
|
return `hace ${diffMins} min`
|
|
}
|
|
if (diffHours < 24) {
|
|
return `hace ${diffHours} hora${diffHours > 1 ? 's' : ''}`
|
|
}
|
|
if (diffDays === 1) {
|
|
return 'ayer'
|
|
}
|
|
if (diffDays < 7) {
|
|
return `hace ${diffDays} días`
|
|
}
|
|
|
|
return alertDate.toLocaleDateString('es-AR', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
})
|
|
}
|
|
|
|
export function AlertItem({ alert, onMarkRead, onDelete }: AlertItemProps) {
|
|
const [showActions, setShowActions] = useState(false)
|
|
const [isExiting, setIsExiting] = useState(false)
|
|
|
|
const handleMarkRead = () => {
|
|
setIsExiting(true)
|
|
setTimeout(() => {
|
|
onMarkRead()
|
|
}, 200)
|
|
}
|
|
|
|
const handleDelete = () => {
|
|
setIsExiting(true)
|
|
setTimeout(() => {
|
|
onDelete()
|
|
}, 200)
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'group relative flex items-center gap-3 p-3 rounded-lg',
|
|
'transition-all duration-200',
|
|
'hover:bg-white/5',
|
|
isExiting && 'opacity-0 -translate-x-4',
|
|
!alert.isRead && 'bg-white/[0.02]'
|
|
)}
|
|
onMouseEnter={() => setShowActions(true)}
|
|
onMouseLeave={() => setShowActions(false)}
|
|
role="listitem"
|
|
>
|
|
{/* Unread indicator */}
|
|
{!alert.isRead && (
|
|
<span className="absolute left-1 top-1/2 -translate-y-1/2 h-2 w-2 rounded-full bg-blue-500" />
|
|
)}
|
|
|
|
{/* Icon */}
|
|
<div
|
|
className={cn(
|
|
'flex-shrink-0',
|
|
severityStyles[alert.severity],
|
|
!alert.isRead && 'ml-3'
|
|
)}
|
|
>
|
|
<AlertIcon type={alert.type} className="h-4 w-4" />
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="flex-1 min-w-0">
|
|
<p
|
|
className={cn(
|
|
'text-sm truncate',
|
|
alert.isRead ? 'text-gray-400' : 'text-white font-medium'
|
|
)}
|
|
>
|
|
{alert.title}
|
|
</p>
|
|
<p className="text-xs text-gray-500 mt-0.5">{getRelativeTime(alert.date)}</p>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
<div
|
|
className={cn(
|
|
'flex items-center gap-1 transition-opacity duration-200',
|
|
showActions ? 'opacity-100' : 'opacity-0'
|
|
)}
|
|
>
|
|
{!alert.isRead && (
|
|
<button
|
|
onClick={handleMarkRead}
|
|
className={cn(
|
|
'p-1.5 rounded-md transition-colors',
|
|
'text-gray-500 hover:text-green-400 hover:bg-green-400/10',
|
|
'focus:outline-none focus:ring-2 focus:ring-green-400/20'
|
|
)}
|
|
title="Marcar como leída"
|
|
aria-label="Marcar como leída"
|
|
>
|
|
<Check className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={handleDelete}
|
|
className={cn(
|
|
'p-1.5 rounded-md transition-colors',
|
|
'text-gray-500 hover:text-red-400 hover:bg-red-400/10',
|
|
'focus:outline-none focus:ring-2 focus:ring-red-400/20'
|
|
)}
|
|
title="Eliminar"
|
|
aria-label="Eliminar alerta"
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|