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:
renato97
2026-01-29 00:00:32 +00:00
commit 712b06f118
65 changed files with 8556 additions and 0 deletions

View File

@@ -0,0 +1,57 @@
'use client';
import { Menu } from 'lucide-react';
import { format } from 'date-fns';
import { es } from 'date-fns/locale';
import { Logo } from './Logo';
interface HeaderProps {
onMenuClick: () => void;
title: string;
}
export function Header({ onMenuClick, title }: HeaderProps) {
const currentDate = format(new Date(), "EEEE, d 'de' MMMM 'de' yyyy", {
locale: es,
});
// Capitalizar primera letra
const formattedDate =
currentDate.charAt(0).toUpperCase() + currentDate.slice(1);
return (
<header className="sticky top-0 z-30 bg-slate-900/95 backdrop-blur-sm border-b border-slate-800">
<div className="flex items-center justify-between h-16 px-4 md:px-6">
{/* Left section */}
<div className="flex items-center gap-4">
<button
onClick={onMenuClick}
className="lg:hidden p-2 -ml-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
aria-label="Abrir menú"
>
<Menu className="w-6 h-6" />
</button>
<div className="flex items-center gap-3">
<div className="lg:hidden">
<Logo size="sm" showText={false} />
</div>
<h1 className="text-lg md:text-xl font-semibold text-slate-100">
{title}
</h1>
</div>
</div>
{/* Right section */}
<div className="flex items-center gap-4">
<div className="hidden md:flex items-center gap-2">
<Logo size="sm" showText />
</div>
<time className="text-sm text-slate-400 hidden sm:block">
{formattedDate}
</time>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,36 @@
import { Wallet } from 'lucide-react';
interface LogoProps {
size?: 'sm' | 'md' | 'lg';
showText?: boolean;
}
const sizeMap = {
sm: {
icon: 24,
text: 'text-lg',
},
md: {
icon: 32,
text: 'text-xl',
},
lg: {
icon: 40,
text: 'text-2xl',
},
};
export function Logo({ size = 'md', showText = true }: LogoProps) {
const { icon, text } = sizeMap[size];
return (
<div className="flex items-center gap-2">
<div className="flex items-center justify-center">
<Wallet className="text-emerald-500" size={icon} strokeWidth={2} />
</div>
{showText && (
<span className={`font-bold text-slate-100 ${text}`}>Finanzas</span>
)}
</div>
);
}

View File

@@ -0,0 +1,72 @@
'use client';
import {
LayoutDashboard,
Wallet,
CreditCard,
PiggyBank,
Bell,
} from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface MobileNavProps {
unreadAlertsCount?: number;
}
const navigationItems = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Deudas', href: '/debts', icon: Wallet },
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
{ name: 'Presupuesto', href: '/budget', icon: PiggyBank },
{ name: 'Alertas', href: '/alerts', icon: Bell, hasBadge: true },
];
export function MobileNav({ unreadAlertsCount = 0 }: MobileNavProps) {
const pathname = usePathname();
const isActive = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
return (
<nav className="fixed bottom-0 left-0 right-0 z-40 bg-slate-900 border-t border-slate-800 lg:hidden">
<ul className="flex items-center justify-around h-16">
{navigationItems.map((item) => {
const active = isActive(item.href);
const Icon = item.icon;
return (
<li key={item.name} className="flex-1">
<Link
href={item.href}
className={`
flex flex-col items-center justify-center gap-1 py-2
transition-colors relative
${
active
? 'text-emerald-500'
: 'text-slate-400 hover:text-slate-300'
}
`}
>
<div className="relative">
<Icon className="w-6 h-6" />
{item.hasBadge && unreadAlertsCount > 0 && (
<span className="absolute -top-1 -right-1 flex items-center justify-center min-w-[16px] h-4 px-1 text-[10px] font-semibold bg-red-500 text-white rounded-full">
{unreadAlertsCount > 99 ? '99+' : unreadAlertsCount}
</span>
)}
</div>
<span className="text-[10px] font-medium">{item.name}</span>
</Link>
</li>
);
})}
</ul>
</nav>
);
}

View File

@@ -0,0 +1,18 @@
import { ReactNode } from 'react';
interface PageContainerProps {
children: ReactNode;
title: string;
}
export function PageContainer({ children, title }: PageContainerProps) {
return (
<main className="min-h-screen bg-slate-950">
<div className="max-w-7xl mx-auto p-4 md:p-6 lg:p-8 pb-24 lg:pb-8">
<div className="space-y-6">
{children}
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,35 @@
import { ReactNode } from 'react';
interface SectionAction {
label: string;
onClick: () => void;
}
interface SectionProps {
title: string;
children: ReactNode;
action?: SectionAction;
}
export function Section({ title, children, action }: SectionProps) {
return (
<section className="bg-slate-900 rounded-lg border border-slate-800">
<div className="flex items-center justify-between px-4 py-3 md:px-6 md:py-4 border-b border-slate-800">
<h2 className="text-base md:text-lg font-semibold text-slate-100">
{title}
</h2>
{action && (
<button
onClick={action.onClick}
className="px-3 py-1.5 text-sm font-medium text-emerald-400 bg-emerald-500/10 hover:bg-emerald-500/20 border border-emerald-500/20 rounded-lg transition-colors"
>
{action.label}
</button>
)}
</div>
<div className="p-4 md:p-6">
{children}
</div>
</section>
);
}

View File

@@ -0,0 +1,122 @@
'use client';
import {
LayoutDashboard,
Wallet,
CreditCard,
PiggyBank,
Bell,
X,
} from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Logo } from './Logo';
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
unreadAlertsCount?: number;
}
const navigationItems = [
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
{ name: 'Deudas', href: '/debts', icon: Wallet },
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
{ name: 'Presupuesto', href: '/budget', icon: PiggyBank },
{ name: 'Alertas', href: '/alerts', icon: Bell, hasBadge: true },
];
export function Sidebar({
isOpen,
onClose,
unreadAlertsCount = 0,
}: SidebarProps) {
const pathname = usePathname();
const isActive = (href: string) => {
if (href === '/') {
return pathname === '/';
}
return pathname.startsWith(href);
};
return (
<>
{/* Mobile overlay */}
{isOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={onClose}
aria-hidden="true"
/>
)}
{/* Sidebar */}
<aside
className={`
fixed top-0 left-0 z-50 h-full w-64 bg-slate-900 border-r border-slate-800
transform transition-transform duration-300 ease-in-out
lg:translate-x-0 lg:static lg:h-screen
${isOpen ? 'translate-x-0' : '-translate-x-full'}
`}
>
<div className="flex flex-col h-full">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-slate-800">
<Logo size="md" showText />
<button
onClick={onClose}
className="lg:hidden p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
aria-label="Cerrar menú"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto py-4 px-3">
<ul className="space-y-1">
{navigationItems.map((item) => {
const active = isActive(item.href);
const Icon = item.icon;
return (
<li key={item.name}>
<Link
href={item.href}
onClick={onClose}
className={`
flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium
transition-colors relative
${
active
? 'bg-slate-800 text-emerald-400 border-l-2 border-emerald-500'
: 'text-slate-300 hover:bg-slate-800 hover:text-slate-100'
}
`}
>
<Icon className="w-5 h-5 flex-shrink-0" />
<span className="flex-1">{item.name}</span>
{item.hasBadge && unreadAlertsCount > 0 && (
<span className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 text-xs font-semibold bg-red-500 text-white rounded-full">
{unreadAlertsCount > 99 ? '99+' : unreadAlertsCount}
</span>
)}
</Link>
</li>
);
})}
</ul>
</nav>
{/* Footer */}
<div className="p-4 border-t border-slate-800">
<p className="text-xs text-slate-500 text-center">
Finanzas v{process.env.NEXT_PUBLIC_APP_VERSION || '1.0.0'}
</p>
</div>
</div>
</aside>
</>
);
}

View File

@@ -0,0 +1,6 @@
export { Sidebar } from './Sidebar';
export { Header } from './Header';
export { MobileNav } from './MobileNav';
export { Logo } from './Logo';
export { PageContainer } from './PageContainer';
export { Section } from './Section';