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/AlertPanel.tsx
Normal file
148
components/alerts/AlertPanel.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Bell, CheckCheck, Trash2, Inbox } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { AlertItem } from './AlertItem'
|
||||
import { AlertBadge } from './AlertBadge'
|
||||
|
||||
type TabType = 'all' | 'unread'
|
||||
|
||||
export function AlertPanel() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('all')
|
||||
|
||||
const alerts = useFinanzasStore((state) => state.alerts)
|
||||
const markAlertAsRead = useFinanzasStore((state) => state.markAlertAsRead)
|
||||
const deleteAlert = useFinanzasStore((state) => state.deleteAlert)
|
||||
const clearAllAlerts = useFinanzasStore((state) => state.clearAllAlerts)
|
||||
|
||||
const unreadAlerts = alerts.filter((alert) => !alert.isRead)
|
||||
const unreadCount = unreadAlerts.length
|
||||
|
||||
const displayedAlerts = activeTab === 'unread' ? unreadAlerts : alerts
|
||||
|
||||
const handleMarkAllRead = () => {
|
||||
unreadAlerts.forEach((alert) => {
|
||||
markAlertAsRead(alert.id)
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearAll = () => {
|
||||
clearAllAlerts()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md bg-gray-900 rounded-xl border border-gray-800 shadow-xl">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<Bell className="h-5 w-5 text-gray-400" />
|
||||
{unreadCount > 0 && <AlertBadge count={unreadCount} variant="dot" />}
|
||||
</div>
|
||||
<h3 className="font-semibold text-white">Alertas</h3>
|
||||
{unreadCount > 0 && (
|
||||
<span className="text-xs text-gray-500">({unreadCount})</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{unreadCount > 0 && (
|
||||
<button
|
||||
onClick={handleMarkAllRead}
|
||||
className={cn(
|
||||
'p-2 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 todas como leídas"
|
||||
aria-label="Marcar todas como leídas"
|
||||
>
|
||||
<CheckCheck className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{alerts.length > 0 && (
|
||||
<button
|
||||
onClick={handleClearAll}
|
||||
className={cn(
|
||||
'p-2 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="Limpiar todas"
|
||||
aria-label="Limpiar todas las alertas"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
{alerts.length > 0 && (
|
||||
<div className="flex border-b border-gray-800">
|
||||
<button
|
||||
onClick={() => setActiveTab('all')}
|
||||
className={cn(
|
||||
'flex-1 px-4 py-2 text-sm font-medium transition-colors',
|
||||
'focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500/20',
|
||||
activeTab === 'all'
|
||||
? 'text-white border-b-2 border-blue-500'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
)}
|
||||
>
|
||||
Todas
|
||||
<span className="ml-1.5 text-xs text-gray-600">({alerts.length})</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('unread')}
|
||||
className={cn(
|
||||
'flex-1 px-4 py-2 text-sm font-medium transition-colors',
|
||||
'focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500/20',
|
||||
activeTab === 'unread'
|
||||
? 'text-white border-b-2 border-blue-500'
|
||||
: 'text-gray-500 hover:text-gray-300'
|
||||
)}
|
||||
>
|
||||
No leídas
|
||||
{unreadCount > 0 && (
|
||||
<span className="ml-1.5 text-xs text-blue-400">({unreadCount})</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Alert List */}
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{displayedAlerts.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
|
||||
<div className="h-12 w-12 rounded-full bg-gray-800 flex items-center justify-center mb-3">
|
||||
<Inbox className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm">
|
||||
{activeTab === 'unread'
|
||||
? 'No tienes alertas sin leer'
|
||||
: 'No tienes alertas'}
|
||||
</p>
|
||||
<p className="text-gray-600 text-xs mt-1">
|
||||
Las alertas aparecerán cuando haya pagos próximos o eventos importantes
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-800/50" role="list">
|
||||
{displayedAlerts.map((alert) => (
|
||||
<AlertItem
|
||||
key={alert.id}
|
||||
alert={alert}
|
||||
onMarkRead={() => markAlertAsRead(alert.id)}
|
||||
onDelete={() => deleteAlert(alert.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user