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:
148
components/alerts/AlertItem.tsx
Normal file
148
components/alerts/AlertItem.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user