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:
58
app/alerts/page.tsx
Normal file
58
app/alerts/page.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { AlertPanel, useAlerts } from '@/components/alerts'
|
||||
import { useSidebar } from '@/app/providers'
|
||||
import { RefreshCw } from 'lucide-react'
|
||||
|
||||
export default function AlertsPage() {
|
||||
const { isOpen, toggle, close } = useSidebar()
|
||||
const { regenerateAlerts, dismissAll, unreadCount } = useAlerts()
|
||||
|
||||
const handleRegenerateAlerts = () => {
|
||||
regenerateAlerts()
|
||||
}
|
||||
|
||||
const handleDismissAll = () => {
|
||||
dismissAll()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
||||
|
||||
<div className="lg:ml-64 min-h-screen flex flex-col">
|
||||
<Header onMenuClick={toggle} title="Alertas" />
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-3 mb-6">
|
||||
<button
|
||||
onClick={handleRegenerateAlerts}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
||||
>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
Regenerar Alertas
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleDismissAll}
|
||||
className="inline-flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-slate-500/20"
|
||||
>
|
||||
Limpiar Todas
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Alert Panel */}
|
||||
<div className="w-full">
|
||||
<AlertPanel />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
app/budget/page.tsx
Normal file
27
app/budget/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { BudgetSection } from '@/components/budget'
|
||||
import { useSidebar } from '@/app/providers'
|
||||
import { useAlerts } from '@/components/alerts'
|
||||
|
||||
export default function BudgetPage() {
|
||||
const { isOpen, close, toggle } = useSidebar()
|
||||
const { unreadCount } = useAlerts()
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-slate-950">
|
||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-screen">
|
||||
<Header onMenuClick={toggle} title="Presupuesto" />
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20">
|
||||
<BudgetSection />
|
||||
</main>
|
||||
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
27
app/cards/page.tsx
Normal file
27
app/cards/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout';
|
||||
import { CardSection } from '@/components/cards';
|
||||
import { useSidebar } from '@/app/providers';
|
||||
import { useAlerts } from '@/components/alerts';
|
||||
|
||||
export default function CardsPage() {
|
||||
const { isOpen, toggle, close } = useSidebar();
|
||||
const { unreadCount } = useAlerts();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
||||
|
||||
<div className="lg:ml-64 min-h-screen flex flex-col">
|
||||
<Header onMenuClick={toggle} title="Tarjetas de Crédito" />
|
||||
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20">
|
||||
<CardSection />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
app/debts/page.tsx
Normal file
32
app/debts/page.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { DebtSection } from '@/components/debts'
|
||||
import { useSidebar } from '@/app/providers'
|
||||
import { useAlerts } from '@/components/alerts'
|
||||
|
||||
export default function DebtsPage() {
|
||||
const { isOpen, close, open } = useSidebar()
|
||||
const { unreadCount } = useAlerts()
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
{/* Sidebar */}
|
||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
||||
|
||||
{/* Main content */}
|
||||
<div className="lg:ml-64 min-h-screen flex flex-col">
|
||||
{/* Header */}
|
||||
<Header onMenuClick={open} title="Deudas" />
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8">
|
||||
<DebtSection />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
332
app/globals.css
Normal file
332
app/globals.css
Normal file
@@ -0,0 +1,332 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
/* Base Colors - Dark Theme (slate/emerald) */
|
||||
--background: 222 47% 11%;
|
||||
--foreground: 210 40% 98%;
|
||||
|
||||
/* Card Colors */
|
||||
--card: 217 33% 17%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
|
||||
/* Popover Colors */
|
||||
--popover: 217 33% 17%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
|
||||
/* Primary - Emerald */
|
||||
--primary: 160 84% 39%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
|
||||
/* Secondary */
|
||||
--secondary: 217 33% 17%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
|
||||
/* Muted */
|
||||
--muted: 217 33% 17%;
|
||||
--muted-foreground: 215 20% 65%;
|
||||
|
||||
/* Accent */
|
||||
--accent: 217 33% 17%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
|
||||
/* Destructive */
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
|
||||
/* Border & Input */
|
||||
--border: 215 28% 25%;
|
||||
--input: 215 28% 25%;
|
||||
|
||||
/* Ring - Emerald */
|
||||
--ring: 160 84% 39%;
|
||||
|
||||
/* Radius */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
|
||||
/* Base Styles */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Input Styles */
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
background-color: hsl(var(--card));
|
||||
border-color: hsl(var(--border));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
input::placeholder,
|
||||
textarea::placeholder {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background-color: hsl(160 84% 39% / 0.3);
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
/* Focus Ring */
|
||||
:focus-visible {
|
||||
outline: none;
|
||||
ring: 2px;
|
||||
ring-color: hsl(var(--ring));
|
||||
ring-offset: 2px;
|
||||
ring-offset-color: hsl(var(--background));
|
||||
}
|
||||
|
||||
/* Custom Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: hsl(var(--background));
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(var(--border));
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(var(--muted-foreground) / 0.5);
|
||||
}
|
||||
|
||||
/* Firefox Scrollbar */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: hsl(var(--border)) hsl(var(--background));
|
||||
}
|
||||
|
||||
/* Utility Classes */
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, hsl(160 84% 39%) 0%, hsl(168 76% 42%) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: hsl(var(--card) / 0.8);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid hsl(var(--border) / 0.5);
|
||||
}
|
||||
|
||||
.text-balance {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
/* Loading States */
|
||||
.skeleton {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
hsl(var(--muted)) 25%,
|
||||
hsl(var(--muted-foreground) / 0.3) 50%,
|
||||
hsl(var(--muted)) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-pulse 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
@keyframes skeleton-pulse {
|
||||
0% {
|
||||
background-position: 200% 0;
|
||||
}
|
||||
100% {
|
||||
background-position: -200% 0;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 2px solid hsl(var(--border));
|
||||
border-top-color: hsl(var(--primary));
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.spinner-lg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-slow {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce-slight {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animation Utility Classes */
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-fade-in-down {
|
||||
animation: fadeInDown 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-left {
|
||||
animation: slideInLeft 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-slide-in-right {
|
||||
animation: slideInRight 0.3s ease-out forwards;
|
||||
}
|
||||
|
||||
.animate-pulse-slow {
|
||||
animation: pulse-slow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-bounce-slight {
|
||||
animation: bounce-slight 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Stagger Animation Delays */
|
||||
.stagger-1 { animation-delay: 0.05s; }
|
||||
.stagger-2 { animation-delay: 0.1s; }
|
||||
.stagger-3 { animation-delay: 0.15s; }
|
||||
.stagger-4 { animation-delay: 0.2s; }
|
||||
.stagger-5 { animation-delay: 0.25s; }
|
||||
.stagger-6 { animation-delay: 0.3s; }
|
||||
.stagger-7 { animation-delay: 0.35s; }
|
||||
.stagger-8 { animation-delay: 0.4s; }
|
||||
|
||||
/* Reduced Motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
30
app/layout.tsx
Normal file
30
app/layout.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Finanzas Personales",
|
||||
description: "Gestiona tus finanzas personales de forma inteligente",
|
||||
keywords: ["finanzas", "presupuesto", "gastos", "ingresos", "ahorro"],
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="es" className={inter.variable} suppressHydrationWarning>
|
||||
<body className={`${inter.className} antialiased min-h-screen bg-slate-950 text-slate-50`}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
134
app/page.tsx
Normal file
134
app/page.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { SummarySection, QuickActions, RecentActivity } from '@/components/dashboard'
|
||||
import { useSidebar } from '@/app/providers'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { AlertBanner, useAlerts } from '@/components/alerts'
|
||||
import { AddDebtModal } from '@/components/modals/AddDebtModal'
|
||||
import { AddCardModal } from '@/components/modals/AddCardModal'
|
||||
import { AddPaymentModal } from '@/components/modals/AddPaymentModal'
|
||||
|
||||
export default function Home() {
|
||||
// Sidebar control
|
||||
const sidebar = useSidebar()
|
||||
|
||||
// Datos del store
|
||||
const markAlertAsRead = useFinanzasStore((state) => state.markAlertAsRead)
|
||||
const deleteAlert = useFinanzasStore((state) => state.deleteAlert)
|
||||
|
||||
// Alertas
|
||||
const { unreadAlerts, unreadCount, regenerateAlerts } = useAlerts()
|
||||
|
||||
// Estados locales para modales
|
||||
const [isAddDebtModalOpen, setIsAddDebtModalOpen] = useState(false)
|
||||
const [isAddCardModalOpen, setIsAddCardModalOpen] = useState(false)
|
||||
const [isAddPaymentModalOpen, setIsAddPaymentModalOpen] = useState(false)
|
||||
|
||||
// Efecto para regenerar alertas al cargar la página
|
||||
useEffect(() => {
|
||||
regenerateAlerts()
|
||||
}, [regenerateAlerts])
|
||||
|
||||
// Efecto para manejar resize de ventana
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
sidebar.open()
|
||||
} else {
|
||||
sidebar.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Estado inicial
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [sidebar])
|
||||
|
||||
// Handlers para modales
|
||||
const handleAddDebt = () => {
|
||||
setIsAddDebtModalOpen(true)
|
||||
}
|
||||
|
||||
const handleAddCard = () => {
|
||||
setIsAddCardModalOpen(true)
|
||||
}
|
||||
|
||||
const handleAddPayment = () => {
|
||||
setIsAddPaymentModalOpen(true)
|
||||
}
|
||||
|
||||
// Primeras 3 alertas no leídas
|
||||
const topAlerts = unreadAlerts.slice(0, 3)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-slate-950">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
isOpen={sidebar.isOpen}
|
||||
onClose={sidebar.close}
|
||||
unreadAlertsCount={unreadCount}
|
||||
/>
|
||||
|
||||
{/* Main content */}
|
||||
<div className="flex flex-1 flex-col lg:ml-0">
|
||||
{/* Header */}
|
||||
<Header onMenuClick={sidebar.toggle} title="Dashboard" />
|
||||
|
||||
{/* Main content area */}
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8">
|
||||
<div className="mx-auto max-w-7xl space-y-6">
|
||||
{/* Alertas destacadas */}
|
||||
{topAlerts.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{topAlerts.map((alert) => (
|
||||
<AlertBanner
|
||||
key={alert.id}
|
||||
alert={alert}
|
||||
onDismiss={() => deleteAlert(alert.id)}
|
||||
onMarkRead={() => markAlertAsRead(alert.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sección de resumen */}
|
||||
<SummarySection />
|
||||
|
||||
{/* Acciones rápidas */}
|
||||
<QuickActions
|
||||
onAddDebt={handleAddDebt}
|
||||
onAddCard={handleAddCard}
|
||||
onAddPayment={handleAddPayment}
|
||||
/>
|
||||
|
||||
{/* Actividad reciente */}
|
||||
<RecentActivity limit={5} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile navigation */}
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
|
||||
{/* Modales */}
|
||||
<AddDebtModal
|
||||
isOpen={isAddDebtModalOpen}
|
||||
onClose={() => setIsAddDebtModalOpen(false)}
|
||||
/>
|
||||
|
||||
<AddCardModal
|
||||
isOpen={isAddCardModalOpen}
|
||||
onClose={() => setIsAddCardModalOpen(false)}
|
||||
/>
|
||||
|
||||
<AddPaymentModal
|
||||
isOpen={isAddPaymentModalOpen}
|
||||
onClose={() => setIsAddPaymentModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
app/providers.tsx
Normal file
41
app/providers.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from "react";
|
||||
|
||||
interface SidebarContextType {
|
||||
isOpen: boolean;
|
||||
toggle: () => void;
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextType | undefined>(undefined);
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
|
||||
const toggleSidebar = () => setIsSidebarOpen((prev) => !prev);
|
||||
const closeSidebar = () => setIsSidebarOpen(false);
|
||||
const openSidebar = () => setIsSidebarOpen(true);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider
|
||||
value={{
|
||||
isOpen: isSidebarOpen,
|
||||
toggle: toggleSidebar,
|
||||
close: closeSidebar,
|
||||
open: openSidebar,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSidebar() {
|
||||
const context = useContext(SidebarContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSidebar must be used within a Providers");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
Reference in New Issue
Block a user