Refactor: Implement DashboardLayout, fix mobile nav, and resolve scroll issues
This commit is contained in:
73
.gitignore
vendored
73
.gitignore
vendored
@@ -1,34 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Local env files
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Security
|
||||
server-settings.json
|
||||
# Dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# Testing
|
||||
coverage
|
||||
|
||||
# Next.js
|
||||
.next/
|
||||
out/
|
||||
build
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# Debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Local env files
|
||||
.env*.local
|
||||
|
||||
# Vercel
|
||||
.vercel
|
||||
|
||||
# TypeScript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# Security
|
||||
server-settings.json
|
||||
.env
|
||||
auth-otp.json
|
||||
|
||||
# Local dev
|
||||
local_Caddyfile
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
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 { regenerateAlerts, dismissAll } = useAlerts()
|
||||
|
||||
const handleRegenerateAlerts = () => {
|
||||
regenerateAlerts()
|
||||
@@ -18,41 +16,31 @@ export default function AlertsPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950">
|
||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
||||
<DashboardLayout title="Alertas">
|
||||
<div className="space-y-6">
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<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>
|
||||
|
||||
<div className="lg:ml-64 min-h-screen flex flex-col">
|
||||
<Header onMenuClick={toggle} title="Alertas" />
|
||||
<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>
|
||||
|
||||
<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} />
|
||||
{/* Alert Panel */}
|
||||
<div className="w-full">
|
||||
<AlertPanel />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
25
app/api/auth/send/route.ts
Normal file
25
app/api/auth/send/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import TelegramBot from 'node-telegram-bot-api';
|
||||
import { generateOTP, saveOTP } from '@/lib/otp';
|
||||
|
||||
export async function POST() {
|
||||
const token = process.env.TELEGRAM_BOT_TOKEN;
|
||||
const chatId = process.env.TELEGRAM_CHAT_ID;
|
||||
|
||||
if (!token || !chatId) {
|
||||
console.error("Telegram credentials missing");
|
||||
return NextResponse.json({ error: 'Telegram not configured' }, { status: 500 });
|
||||
}
|
||||
|
||||
const otp = generateOTP();
|
||||
saveOTP(otp);
|
||||
|
||||
const bot = new TelegramBot(token, { polling: false });
|
||||
try {
|
||||
await bot.sendMessage(chatId, `🔐 Your Login Code: ${otp}`);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('Telegram Error:', error);
|
||||
return NextResponse.json({ error: 'Failed to send message: ' + error.message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
24
app/api/auth/verify/route.ts
Normal file
24
app/api/auth/verify/route.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { verifyOTP } from '@/lib/otp';
|
||||
import { cookies } from 'next/headers';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const { code } = await req.json();
|
||||
|
||||
if (verifyOTP(code)) {
|
||||
// Set cookie
|
||||
cookies().set('auth_token', 'true', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 60 * 60 * 24 * 7, // 1 week
|
||||
path: '/'
|
||||
});
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid Code' }, { status: 401 });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid Request' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
17
app/api/sync/route.ts
Normal file
17
app/api/sync/route.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getDatabase, saveDatabase } from '@/lib/server-db';
|
||||
|
||||
export async function GET() {
|
||||
const data = getDatabase();
|
||||
return NextResponse.json(data);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
try {
|
||||
const body = await req.json();
|
||||
saveDatabase(body);
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: 'Invalid Data' }, { status: 400 });
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
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>
|
||||
<DashboardLayout title="Presupuesto">
|
||||
<BudgetSection />
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout';
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
||||
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>
|
||||
<DashboardLayout title="Tarjetas de Crédito">
|
||||
<CardSection />
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,32 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
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>
|
||||
<DashboardLayout title="Deudas">
|
||||
<DebtSection />
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
165
app/incomes/page.tsx
Normal file
165
app/incomes/page.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { Plus, Trash2, TrendingUp, DollarSign } from 'lucide-react'
|
||||
import { format } from 'date-fns'
|
||||
import { es } from 'date-fns/locale'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
|
||||
export default function IncomesPage() {
|
||||
const incomes = useFinanzasStore((state) => state.incomes) || []
|
||||
const addIncome = useFinanzasStore((state) => state.addIncome)
|
||||
const removeIncome = useFinanzasStore((state) => state.removeIncome)
|
||||
|
||||
const [newIncome, setNewIncome] = useState({
|
||||
amount: '',
|
||||
description: '',
|
||||
category: 'salary' as const,
|
||||
})
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!newIncome.amount || !newIncome.description) return
|
||||
|
||||
addIncome({
|
||||
amount: parseFloat(newIncome.amount),
|
||||
description: newIncome.description,
|
||||
category: newIncome.category,
|
||||
date: new Date().toISOString(),
|
||||
})
|
||||
setNewIncome({ amount: '', description: '', category: 'salary' })
|
||||
}
|
||||
|
||||
const totalIncomes = incomes.reduce((acc, curr) => acc + curr.amount, 0)
|
||||
|
||||
return (
|
||||
<DashboardLayout title="Ingresos">
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-3xl font-bold flex items-center gap-2 text-white">
|
||||
<TrendingUp className="text-green-500" /> Ingresos
|
||||
</h2>
|
||||
<div className="text-right">
|
||||
<p className="text-sm text-gray-400">Total Acumulado</p>
|
||||
<p className="text-2xl font-bold text-green-400">
|
||||
${totalIncomes.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6">
|
||||
{/* Formulario */}
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-xl md:col-span-1 h-fit overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-700">
|
||||
<h3 className="font-semibold text-lg text-white">Nuevo Ingreso</h3>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="text-sm text-gray-400 block mb-2">Descripción</label>
|
||||
<input
|
||||
placeholder="Ej. Pago Freelance"
|
||||
value={newIncome.description}
|
||||
onChange={(e) =>
|
||||
setNewIncome({ ...newIncome, description: e.target.value })
|
||||
}
|
||||
className="w-full bg-slate-700 border-slate-600 border rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-400 block mb-2">Monto</label>
|
||||
<div className="relative">
|
||||
<DollarSign className="absolute left-3 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
value={newIncome.amount}
|
||||
onChange={(e) =>
|
||||
setNewIncome({ ...newIncome, amount: e.target.value })
|
||||
}
|
||||
className="w-full pl-9 bg-slate-700 border-slate-600 border rounded-lg px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm text-gray-400 block mb-2">Categoría</label>
|
||||
<select
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-lg px-3 py-2 text-sm text-white focus:outline-none focus:ring-2 focus:ring-emerald-500"
|
||||
value={newIncome.category}
|
||||
onChange={(e) =>
|
||||
setNewIncome({
|
||||
...newIncome,
|
||||
category: e.target.value as any,
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="salary">Salario</option>
|
||||
<option value="freelance">Freelance</option>
|
||||
<option value="business">Negocio</option>
|
||||
<option value="gift">Regalo</option>
|
||||
<option value="other">Otro</option>
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
className="w-full bg-emerald-600 hover:bg-emerald-700 text-white font-semibold py-2 px-4 rounded-lg flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Agregar Ingreso
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lista */}
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-xl md:col-span-2 overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-700">
|
||||
<h3 className="font-semibold text-lg text-white">Historial de Ingresos</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{incomes.length === 0 ? (
|
||||
<p className="text-center text-gray-500 py-8">
|
||||
No hay ingresos registrados aún.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{incomes
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||
)
|
||||
.map((income) => (
|
||||
<div
|
||||
key={income.id}
|
||||
className="flex items-center justify-between p-4 bg-slate-700/50 rounded-lg border border-slate-700 hover:border-slate-600 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<p className="font-semibold text-white">
|
||||
{income.description}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400 capitalize">
|
||||
{income.category} •{' '}
|
||||
{format(new Date(income.date), "d 'de' MMMM", {
|
||||
locale: es,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-emerald-400 font-mono font-bold">
|
||||
+${income.amount.toLocaleString()}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeIncome(income.id)}
|
||||
className="text-slate-500 hover:text-red-400 transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
115
app/login/page.tsx
Normal file
115
app/login/page.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Lock, Send, Loader2 } from 'lucide-react';
|
||||
|
||||
export default function LoginPage() {
|
||||
const [step, setStep] = useState<'initial' | 'verify'>('initial');
|
||||
const [code, setCode] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const router = useRouter();
|
||||
|
||||
const sendCode = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('/api/auth/send', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Failed to send code');
|
||||
setStep('verify');
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const verifyCode = async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('/api/auth/verify', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ code })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.error || 'Invalid code');
|
||||
|
||||
// Redirect
|
||||
router.push('/');
|
||||
router.refresh();
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-900 text-white p-4">
|
||||
<div className="w-full max-w-md bg-gray-800 rounded-lg shadow-xl p-8 border border-gray-700">
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
<div className="bg-blue-600 p-3 rounded-full mb-4">
|
||||
<Lock className="w-8 h-8 text-white" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold">Secure Access</h1>
|
||||
<p className="text-gray-400 mt-2 text-center">
|
||||
Finanzas Personales
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500 text-red-200 p-3 rounded mb-4 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'initial' ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300 text-center text-sm">
|
||||
Click below to receive a login code via Telegram.
|
||||
</p>
|
||||
<button
|
||||
onClick={sendCode}
|
||||
disabled={loading}
|
||||
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-semibold py-3 px-4 rounded-lg transition flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Loader2 className="animate-spin" /> : <Send size={20} />}
|
||||
Send Code to Telegram
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-gray-300 text-center text-sm">
|
||||
Enter the 6-digit code sent to your Telegram.
|
||||
</p>
|
||||
<input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="123456"
|
||||
className="w-full bg-gray-700 border border-gray-600 rounded-lg px-4 py-3 text-center text-2xl tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
maxLength={6}
|
||||
/>
|
||||
<button
|
||||
onClick={verifyCode}
|
||||
disabled={loading || code.length < 4}
|
||||
className="w-full bg-green-600 hover:bg-green-500 text-white font-semibold py-3 px-4 rounded-lg transition flex items-center justify-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{loading ? <Loader2 className="animate-spin" /> : 'Verify & Login'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStep('initial')}
|
||||
className="w-full text-gray-400 hover:text-white text-sm"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
app/page.tsx
100
app/page.tsx
@@ -1,25 +1,21 @@
|
||||
'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'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
|
||||
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()
|
||||
const { unreadAlerts, regenerateAlerts } = useAlerts()
|
||||
|
||||
// Estados locales para modales
|
||||
const [isAddDebtModalOpen, setIsAddDebtModalOpen] = useState(false)
|
||||
@@ -31,23 +27,6 @@ export default function Home() {
|
||||
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)
|
||||
@@ -65,54 +44,35 @@ export default function Home() {
|
||||
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} />
|
||||
<DashboardLayout title="Dashboard">
|
||||
<div className="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>
|
||||
</main>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile navigation */}
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
{/* Sección de resumen */}
|
||||
<SummarySection />
|
||||
|
||||
{/* Acciones rápidas */}
|
||||
<QuickActions
|
||||
onAddDebt={handleAddDebt}
|
||||
onAddCard={handleAddCard}
|
||||
onAddPayment={handleAddPayment}
|
||||
/>
|
||||
|
||||
{/* Actividad reciente */}
|
||||
<RecentActivity limit={5} />
|
||||
</div>
|
||||
|
||||
{/* Modales */}
|
||||
<AddDebtModal
|
||||
@@ -129,6 +89,6 @@ export default function Home() {
|
||||
isOpen={isAddPaymentModalOpen}
|
||||
onClose={() => setIsAddPaymentModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, ReactNode } from "react";
|
||||
import { DataSync } from "@/components/DataSync";
|
||||
|
||||
interface SidebarContextType {
|
||||
isOpen: boolean;
|
||||
@@ -27,6 +28,7 @@ export function Providers({ children }: { children: ReactNode }) {
|
||||
open: openSidebar,
|
||||
}}
|
||||
>
|
||||
<DataSync />
|
||||
{children}
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,142 +1,145 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { predictNextBill, calculateTrend } from '@/lib/predictions'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { Zap, Droplets, Flame, Wifi, TrendingUp, TrendingDown, Plus, History } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AddServiceModal } from '@/components/modals/AddServiceModal'
|
||||
|
||||
const SERVICES = [
|
||||
{ id: 'electricity', label: 'Luz (Electricidad)', icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-400/10' },
|
||||
{ id: 'water', label: 'Agua', icon: Droplets, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
||||
{ id: 'gas', label: 'Gas', icon: Flame, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
||||
{ id: 'internet', label: 'Internet', icon: Wifi, color: 'text-cyan-400', bg: 'bg-cyan-400/10' },
|
||||
]
|
||||
|
||||
export default function ServicesPage() {
|
||||
const serviceBills = useFinanzasStore((state) => state.serviceBills)
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Servicios y Predicciones</h1>
|
||||
<p className="text-slate-400 text-sm">Gestiona tus consumos de Luz, Agua y Gas.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-white rounded-lg transition shadow-lg shadow-cyan-500/20 font-medium self-start sm:self-auto"
|
||||
>
|
||||
<Plus size={18} /> Nuevo Pago
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Service Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{SERVICES.map((service) => {
|
||||
const Icon = service.icon
|
||||
const prediction = predictNextBill(serviceBills, service.id as any)
|
||||
const trend = calculateTrend(serviceBills, service.id as any)
|
||||
const lastBill = serviceBills
|
||||
.filter(b => b.type === service.id)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]
|
||||
|
||||
return (
|
||||
<div key={service.id} className="bg-slate-900 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn("p-2 rounded-lg", service.bg)}>
|
||||
<Icon className={cn("w-6 h-6", service.color)} />
|
||||
</div>
|
||||
{trend !== 0 && (
|
||||
<div className={cn("flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full", trend > 0 ? "bg-red-500/10 text-red-400" : "bg-emerald-500/10 text-emerald-400")}>
|
||||
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||
{Math.abs(trend).toFixed(0)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-slate-400 text-sm font-medium">{service.label}</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h3 className="text-2xl font-bold text-white mt-1">
|
||||
{formatCurrency(prediction || (lastBill?.amount ?? 0))}
|
||||
</h3>
|
||||
{prediction > 0 && <span className="text-xs text-slate-500 font-mono">(est.)</span>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{lastBill
|
||||
? `Último: ${formatCurrency(lastBill.amount)}`
|
||||
: 'Sin historial'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* History List */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||||
<div className="p-5 border-b border-slate-800 flex items-center gap-2">
|
||||
<History size={18} className="text-slate-400" />
|
||||
<h3 className="text-lg font-semibold text-white">Historial de Pagos</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-800">
|
||||
{serviceBills.length === 0 ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">
|
||||
No hay facturas registradas. Comienza agregando una para ver predicciones.
|
||||
</div>
|
||||
) : (
|
||||
serviceBills
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.map((bill) => {
|
||||
const service = SERVICES.find(s => s.id === bill.type)
|
||||
const Icon = service?.icon || Zap
|
||||
|
||||
return (
|
||||
<div key={bill.id} className="p-4 flex items-center justify-between hover:bg-slate-800/50 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={cn("p-2 rounded-lg", service?.bg || 'bg-slate-800')}>
|
||||
<Icon className={cn("w-5 h-5", service?.color || 'text-slate-400')} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium capitalize">{service?.label || bill.type}</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
||||
<p className="text-xs text-slate-500 capitalize">{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}</p>
|
||||
{bill.usage && (
|
||||
<>
|
||||
<span className="hidden sm:inline text-slate-700">•</span>
|
||||
<p className="text-xs text-slate-400">
|
||||
Consumo: <span className="text-slate-300 font-medium">{bill.usage} {bill.unit}</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-mono font-medium">{formatCurrency(bill.amount)}</p>
|
||||
<div className="flex flex-col items-end">
|
||||
<p className="text-xs text-slate-500 uppercase">{bill.period}</p>
|
||||
{bill.usage && bill.amount && (
|
||||
<p className="text-[10px] text-cyan-500/80 font-mono">
|
||||
{formatCurrency(bill.amount / bill.usage)} / {bill.unit}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useFinanzasStore } from '@/lib/store'
|
||||
import { predictNextBill, calculateTrend } from '@/lib/predictions'
|
||||
import { formatCurrency } from '@/lib/utils'
|
||||
import { Zap, Droplets, Flame, Wifi, TrendingUp, TrendingDown, Plus, History } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AddServiceModal } from '@/components/modals/AddServiceModal'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
|
||||
const SERVICES = [
|
||||
{ id: 'electricity', label: 'Luz (Electricidad)', icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-400/10' },
|
||||
{ id: 'water', label: 'Agua', icon: Droplets, color: 'text-blue-400', bg: 'bg-blue-400/10' },
|
||||
{ id: 'gas', label: 'Gas', icon: Flame, color: 'text-orange-400', bg: 'bg-orange-400/10' },
|
||||
{ id: 'internet', label: 'Internet', icon: Wifi, color: 'text-cyan-400', bg: 'bg-cyan-400/10' },
|
||||
]
|
||||
|
||||
export default function ServicesPage() {
|
||||
const serviceBills = useFinanzasStore((state) => state.serviceBills)
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<DashboardLayout title="Servicios">
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Servicios y Predicciones</h2>
|
||||
<p className="text-slate-400 text-sm">Gestiona tus consumos de Luz, Agua y Gas.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cyan-500 hover:bg-cyan-400 text-white rounded-lg transition shadow-lg shadow-cyan-500/20 font-medium self-start sm:self-auto"
|
||||
>
|
||||
<Plus size={18} /> Nuevo Pago
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Service Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{SERVICES.map((service) => {
|
||||
const Icon = service.icon
|
||||
const prediction = predictNextBill(serviceBills, service.id as any)
|
||||
const trend = calculateTrend(serviceBills, service.id as any)
|
||||
const lastBill = serviceBills
|
||||
.filter(b => b.type === service.id)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]
|
||||
|
||||
return (
|
||||
<div key={service.id} className="bg-slate-900 border border-slate-800 rounded-xl p-5 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className={cn("p-2 rounded-lg", service.bg)}>
|
||||
<Icon className={cn("w-6 h-6", service.color)} />
|
||||
</div>
|
||||
{trend !== 0 && (
|
||||
<div className={cn("flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-full", trend > 0 ? "bg-red-500/10 text-red-400" : "bg-emerald-500/10 text-emerald-400")}>
|
||||
{trend > 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||
{Math.abs(trend).toFixed(0)}%
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-slate-400 text-sm font-medium">{service.label}</p>
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h3 className="text-2xl font-bold text-white mt-1">
|
||||
{formatCurrency(prediction || (lastBill?.amount ?? 0))}
|
||||
</h3>
|
||||
{prediction > 0 && <span className="text-xs text-slate-500 font-mono">(est.)</span>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{lastBill
|
||||
? `Último: ${formatCurrency(lastBill.amount)}`
|
||||
: 'Sin historial'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* History List */}
|
||||
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||||
<div className="p-5 border-b border-slate-800 flex items-center gap-2">
|
||||
<History size={18} className="text-slate-400" />
|
||||
<h3 className="text-lg font-semibold text-white">Historial de Pagos</h3>
|
||||
</div>
|
||||
<div className="divide-y divide-slate-800">
|
||||
{serviceBills.length === 0 ? (
|
||||
<div className="p-8 text-center text-slate-500 text-sm">
|
||||
No hay facturas registradas. Comienza agregando una para ver predicciones.
|
||||
</div>
|
||||
) : (
|
||||
serviceBills
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||
.map((bill) => {
|
||||
const service = SERVICES.find(s => s.id === bill.type)
|
||||
const Icon = service?.icon || Zap
|
||||
|
||||
return (
|
||||
<div key={bill.id} className="p-4 flex items-center justify-between hover:bg-slate-800/50 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={cn("p-2 rounded-lg", service?.bg || 'bg-slate-800')}>
|
||||
<Icon className={cn("w-5 h-5", service?.color || 'text-slate-400')} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-white font-medium capitalize">{service?.label || bill.type}</p>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
||||
<p className="text-xs text-slate-500 capitalize">{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}</p>
|
||||
{bill.usage && (
|
||||
<>
|
||||
<span className="hidden sm:inline text-slate-700">•</span>
|
||||
<p className="text-xs text-slate-400">
|
||||
Consumo: <span className="text-slate-300 font-medium">{bill.usage} {bill.unit}</span>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-white font-mono font-medium">{formatCurrency(bill.amount)}</p>
|
||||
<div className="flex flex-col items-end">
|
||||
<p className="text-xs text-slate-500 uppercase">{bill.period}</p>
|
||||
{bill.usage && bill.amount && (
|
||||
<p className="text-[10px] text-cyan-500/80 font-mono">
|
||||
{formatCurrency(bill.amount / bill.usage)} / {bill.unit}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
||||
import { Save, Plus, Trash2, Bot, MessageSquare, Key, Link as LinkIcon, Lock, Send, CheckCircle2, XCircle, Loader2, Sparkles, Box } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AIServiceConfig, AppSettings } from '@/lib/types'
|
||||
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -159,211 +160,213 @@ export default function SettingsPage() {
|
||||
if (loading) return <div className="p-8 text-center text-slate-400">Cargando configuración...</div>
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto space-y-8 pb-10">
|
||||
<DashboardLayout title="Configuración">
|
||||
<div className="max-w-4xl mx-auto space-y-8 pb-10">
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-white">Configuración</h1>
|
||||
<p className="text-slate-400 text-sm">Gestiona la integración con Telegram e Inteligencia Artificial.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-6 py-2 bg-emerald-500 hover:bg-emerald-400 text-white rounded-lg transition shadow-lg shadow-emerald-500/20 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Save size={18} />
|
||||
{saving ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={cn(
|
||||
"p-4 rounded-lg text-sm font-medium border flex items-center gap-2 animate-in fade-in slide-in-from-top-2",
|
||||
message.type === 'success' ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400" : "bg-red-500/10 border-red-500/20 text-red-400"
|
||||
)}>
|
||||
{message.type === 'success' ? <CheckCircle2 size={18} /> : <XCircle size={18} />}
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Telegram Configuration */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between text-white border-b border-slate-800 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="text-cyan-400" />
|
||||
<h2 className="text-lg font-semibold">Telegram Bot</h2>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white">Configuración</h2>
|
||||
<p className="text-slate-400 text-sm">Gestiona la integración con Telegram e Inteligencia Artificial.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={testTelegram}
|
||||
disabled={testingTelegram || !settings.telegram.botToken || !settings.telegram.chatId}
|
||||
className="text-xs flex items-center gap-1.5 bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/20 px-3 py-1.5 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center gap-2 px-6 py-2 bg-emerald-500 hover:bg-emerald-400 text-white rounded-lg transition shadow-lg shadow-emerald-500/20 font-medium disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{testingTelegram ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
||||
Probar Envío
|
||||
<Save size={18} />
|
||||
{saving ? 'Guardando...' : 'Guardar Cambios'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 bg-slate-900 border border-slate-800 rounded-xl">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Key size={12} /> Bot Token
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
value={settings.telegram.botToken}
|
||||
onChange={(e) => setSettings({ ...settings, telegram: { ...settings.telegram, botToken: e.target.value } })}
|
||||
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white font-mono text-sm outline-none transition-all placeholder:text-slate-700"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500">El token que te da @BotFather.</p>
|
||||
{message && (
|
||||
<div className={cn(
|
||||
"p-4 rounded-lg text-sm font-medium border flex items-center gap-2 animate-in fade-in slide-in-from-top-2",
|
||||
message.type === 'success' ? "bg-emerald-500/10 border-emerald-500/20 text-emerald-400" : "bg-red-500/10 border-red-500/20 text-red-400"
|
||||
)}>
|
||||
{message.type === 'success' ? <CheckCircle2 size={18} /> : <XCircle size={18} />}
|
||||
{message.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<MessageSquare size={12} /> Chat ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456789"
|
||||
value={settings.telegram.chatId}
|
||||
onChange={(e) => setSettings({ ...settings, telegram: { ...settings.telegram, chatId: e.target.value } })}
|
||||
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white font-mono text-sm outline-none transition-all placeholder:text-slate-700"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500">Tu ID numérico de Telegram (o el ID del grupo).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI Providers Configuration */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between text-white border-b border-slate-800 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="text-purple-400" />
|
||||
<h2 className="text-lg font-semibold">Proveedores de IA</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={addProvider}
|
||||
disabled={settings.aiProviders.length >= 3}
|
||||
className="text-xs flex items-center gap-1 bg-slate-800 hover:bg-slate-700 text-slate-200 px-3 py-1.5 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus size={14} /> Agregar Provider ({settings.aiProviders.length}/3)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{settings.aiProviders.length === 0 && (
|
||||
<div className="p-8 text-center text-slate-500 border border-dashed border-slate-800 rounded-xl">
|
||||
No hay proveedores de IA configurados. Agrega uno para empezar.
|
||||
{/* Telegram Configuration */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between text-white border-b border-slate-800 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="text-cyan-400" />
|
||||
<h2 className="text-lg font-semibold">Telegram Bot</h2>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={testTelegram}
|
||||
disabled={testingTelegram || !settings.telegram.botToken || !settings.telegram.chatId}
|
||||
className="text-xs flex items-center gap-1.5 bg-cyan-500/10 hover:bg-cyan-500/20 text-cyan-400 border border-cyan-500/20 px-3 py-1.5 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{testingTelegram ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
||||
Probar Envío
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{settings.aiProviders.map((provider, index) => (
|
||||
<div key={provider.id} className="p-6 bg-slate-900 border border-slate-800 rounded-xl relative group">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-sm font-semibold text-slate-300 bg-slate-950 inline-block px-3 py-1 rounded-md border border-slate-800">
|
||||
Provider #{index + 1}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => testAI(provider)}
|
||||
disabled={testingAI === provider.id || !provider.endpoint || !provider.token || !provider.model}
|
||||
className={cn("text-xs flex items-center gap-1 bg-slate-800 hover:bg-slate-700 text-purple-300 border border-purple-500/20 px-2 py-1.5 rounded-lg transition disabled:opacity-50", !provider.model && "opacity-50")}
|
||||
title="Verificar conexión"
|
||||
>
|
||||
{testingAI === provider.id ? <Loader2 size={12} className="animate-spin" /> : <LinkIcon size={12} />}
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeProvider(provider.id)}
|
||||
className="text-slate-500 hover:text-red-400 transition-colors p-1.5 hover:bg-red-500/10 rounded-lg"
|
||||
title="Eliminar"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 bg-slate-900 border border-slate-800 rounded-xl">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Key size={12} /> Bot Token
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
value={settings.telegram.botToken}
|
||||
onChange={(e) => setSettings({ ...settings, telegram: { ...settings.telegram, botToken: e.target.value } })}
|
||||
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white font-mono text-sm outline-none transition-all placeholder:text-slate-700"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500">El token que te da @BotFather.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<MessageSquare size={12} /> Chat ID
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="123456789"
|
||||
value={settings.telegram.chatId}
|
||||
onChange={(e) => setSettings({ ...settings, telegram: { ...settings.telegram, chatId: e.target.value } })}
|
||||
className="w-full px-4 py-3 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500 text-white font-mono text-sm outline-none transition-all placeholder:text-slate-700"
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500">Tu ID numérico de Telegram (o el ID del grupo).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* AI Providers Configuration */}
|
||||
<section className="space-y-4">
|
||||
<div className="flex items-center justify-between text-white border-b border-slate-800 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bot className="text-purple-400" />
|
||||
<h2 className="text-lg font-semibold">Proveedores de IA</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={addProvider}
|
||||
disabled={settings.aiProviders.length >= 3}
|
||||
className="text-xs flex items-center gap-1 bg-slate-800 hover:bg-slate-700 text-slate-200 px-3 py-1.5 rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<Plus size={14} /> Agregar Provider ({settings.aiProviders.length}/3)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{settings.aiProviders.length === 0 && (
|
||||
<div className="p-8 text-center text-slate-500 border border-dashed border-slate-800 rounded-xl">
|
||||
No hay proveedores de IA configurados. Agrega uno para empezar.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Nombre</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: MiniMax, Z.ai"
|
||||
value={provider.name}
|
||||
onChange={(e) => updateProvider(provider.id, 'name', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<LinkIcon size={12} /> Endpoint URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://api.example.com/v1"
|
||||
value={provider.endpoint}
|
||||
onChange={(e) => updateProvider(provider.id, 'endpoint', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Lock size={12} /> API Key / Token
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
value={provider.token}
|
||||
onChange={(e) => updateProvider(provider.id, 'token', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Box size={12} /> Model
|
||||
</label>
|
||||
{settings.aiProviders.map((provider, index) => (
|
||||
<div key={provider.id} className="p-6 bg-slate-900 border border-slate-800 rounded-xl relative group">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<h3 className="text-sm font-semibold text-slate-300 bg-slate-950 inline-block px-3 py-1 rounded-md border border-slate-800">
|
||||
Provider #{index + 1}
|
||||
</h3>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => detectModels(provider)}
|
||||
disabled={detectingModels === provider.id || !provider.endpoint || !provider.token}
|
||||
className="text-[10px] flex items-center gap-1 text-cyan-400 hover:text-cyan-300 disabled:opacity-50"
|
||||
onClick={() => testAI(provider)}
|
||||
disabled={testingAI === provider.id || !provider.endpoint || !provider.token || !provider.model}
|
||||
className={cn("text-xs flex items-center gap-1 bg-slate-800 hover:bg-slate-700 text-purple-300 border border-purple-500/20 px-2 py-1.5 rounded-lg transition disabled:opacity-50", !provider.model && "opacity-50")}
|
||||
title="Verificar conexión"
|
||||
>
|
||||
{detectingModels === provider.id ? <Loader2 size={10} className="animate-spin" /> : <Sparkles size={10} />}
|
||||
Auto Detectar
|
||||
{testingAI === provider.id ? <Loader2 size={12} className="animate-spin" /> : <LinkIcon size={12} />}
|
||||
Test
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeProvider(provider.id)}
|
||||
className="text-slate-500 hover:text-red-400 transition-colors p-1.5 hover:bg-red-500/10 rounded-lg"
|
||||
title="Eliminar"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
{availableModels[provider.id] ? (
|
||||
<select
|
||||
value={provider.model || ''}
|
||||
onChange={(e) => updateProvider(provider.id, 'model', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white text-sm outline-none"
|
||||
>
|
||||
<option value="" disabled>Selecciona un modelo</option>
|
||||
{availableModels[provider.id].map(m => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: gpt-3.5-turbo, glm-4"
|
||||
value={provider.model || ''}
|
||||
onChange={(e) => updateProvider(provider.id, 'model', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Nombre</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: MiniMax, Z.ai"
|
||||
value={provider.name}
|
||||
onChange={(e) => updateProvider(provider.id, 'name', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<LinkIcon size={12} /> Endpoint URL
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="https://api.example.com/v1"
|
||||
value={provider.endpoint}
|
||||
onChange={(e) => updateProvider(provider.id, 'endpoint', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Lock size={12} /> API Key / Token
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="sk-..."
|
||||
value={provider.token}
|
||||
onChange={(e) => updateProvider(provider.id, 'token', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Model Selection */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||
<Box size={12} /> Model
|
||||
</label>
|
||||
<button
|
||||
onClick={() => detectModels(provider)}
|
||||
disabled={detectingModels === provider.id || !provider.endpoint || !provider.token}
|
||||
className="text-[10px] flex items-center gap-1 text-cyan-400 hover:text-cyan-300 disabled:opacity-50"
|
||||
>
|
||||
{detectingModels === provider.id ? <Loader2 size={10} className="animate-spin" /> : <Sparkles size={10} />}
|
||||
Auto Detectar
|
||||
</button>
|
||||
</div>
|
||||
{availableModels[provider.id] ? (
|
||||
<select
|
||||
value={provider.model || ''}
|
||||
onChange={(e) => updateProvider(provider.id, 'model', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white text-sm outline-none"
|
||||
>
|
||||
<option value="" disabled>Selecciona un modelo</option>
|
||||
{availableModels[provider.id].map(m => (
|
||||
<option key={m} value={m}>{m}</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ej: gpt-3.5-turbo, glm-4"
|
||||
value={provider.model || ''}
|
||||
onChange={(e) => updateProvider(provider.id, 'model', e.target.value)}
|
||||
className="w-full px-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg focus:ring-2 focus:ring-purple-500/50 focus:border-purple-500 text-white font-mono text-sm outline-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
||||
64
components/DataSync.tsx
Normal file
64
components/DataSync.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useFinanzasStore } from '@/lib/store';
|
||||
|
||||
export function DataSync() {
|
||||
// const isHydrated = useFinanzasStore(state => state._hasHydrated);
|
||||
const store = useFinanzasStore();
|
||||
const initialized = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function init() {
|
||||
if (initialized.current) return;
|
||||
initialized.current = true;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/sync');
|
||||
if (!res.ok) return;
|
||||
const serverData = await res.json();
|
||||
|
||||
// Simple logic: if server has data (debts or cards or something), trust server.
|
||||
const hasServerData = serverData.fixedDebts?.length > 0 || serverData.creditCards?.length > 0;
|
||||
|
||||
if (hasServerData) {
|
||||
console.log("Sync: Hydrating from Server");
|
||||
useFinanzasStore.setState(serverData);
|
||||
} else {
|
||||
// Server is empty, but we might have local data.
|
||||
// Push local data to server to initialize it.
|
||||
console.log("Sync: Initializing Server with Local Data");
|
||||
syncToServer(useFinanzasStore.getState());
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Sync init error", e);
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
}, []);
|
||||
|
||||
// Sync on change
|
||||
useEffect(() => {
|
||||
if (!initialized.current) return;
|
||||
|
||||
const unsub = useFinanzasStore.subscribe((state) => {
|
||||
syncToServer(state);
|
||||
});
|
||||
return () => unsub();
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
function syncToServer(state: any) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
fetch('/api/sync', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(state)
|
||||
}).catch(e => console.error("Sync error", e));
|
||||
}, 2000); // Debounce 2s
|
||||
}
|
||||
60
components/layout/DashboardLayout.tsx
Normal file
60
components/layout/DashboardLayout.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode, useEffect } from 'react'
|
||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
||||
import { useSidebar } from '@/app/providers'
|
||||
import { useAlerts } from '@/components/alerts'
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
children: ReactNode
|
||||
title: string
|
||||
}
|
||||
|
||||
export function DashboardLayout({ children, title }: DashboardLayoutProps) {
|
||||
const { isOpen, toggle, close, open } = useSidebar()
|
||||
const { unreadCount } = useAlerts()
|
||||
|
||||
// Ensure sidebar is open on desktop mount
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) {
|
||||
open()
|
||||
} else {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
// Initial check
|
||||
handleResize()
|
||||
|
||||
window.addEventListener('resize', handleResize)
|
||||
return () => window.removeEventListener('resize', handleResize)
|
||||
}, [open, close])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-slate-950">
|
||||
{/* Sidebar */}
|
||||
<Sidebar
|
||||
isOpen={isOpen}
|
||||
onClose={close}
|
||||
unreadAlertsCount={unreadCount}
|
||||
/>
|
||||
|
||||
{/* Main content wrapper */}
|
||||
<div className="flex flex-1 flex-col lg:ml-0 min-w-0">
|
||||
{/* Header */}
|
||||
<Header onMenuClick={toggle} title={title} />
|
||||
|
||||
{/* Page content */}
|
||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-24 lg:pb-8">
|
||||
<div className="mx-auto max-w-7xl h-full">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Mobile Navigation */}
|
||||
<MobileNav unreadAlertsCount={unreadCount} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Bell,
|
||||
Lightbulb,
|
||||
Settings,
|
||||
TrendingUp,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
@@ -22,6 +23,7 @@ interface SidebarProps {
|
||||
|
||||
const navigationItems = [
|
||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||
{ name: 'Ingresos', href: '/incomes', icon: TrendingUp },
|
||||
{ name: 'Deudas', href: '/debts', icon: Wallet },
|
||||
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
|
||||
{ name: 'Presupuesto', href: '/budget', icon: PiggyBank },
|
||||
|
||||
29
data/db.json
Normal file
29
data/db.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"fixedDebts": [],
|
||||
"variableDebts": [
|
||||
{
|
||||
"name": "netflix",
|
||||
"amount": 5000,
|
||||
"date": "2026-01-29T00:00:00.000Z",
|
||||
"category": "shopping",
|
||||
"isPaid": false,
|
||||
"id": "f2e424ca-5f07-4f57-9386-822354e0ee1e"
|
||||
},
|
||||
{
|
||||
"name": "youtube",
|
||||
"amount": 2500,
|
||||
"date": "2026-01-29T00:00:00.000Z",
|
||||
"category": "shopping",
|
||||
"isPaid": false,
|
||||
"id": "621c3caf-529b-4c33-b46f-3d86b119dd75"
|
||||
}
|
||||
],
|
||||
"creditCards": [],
|
||||
"cardPayments": [],
|
||||
"monthlyBudgets": [],
|
||||
"currentMonth": 1,
|
||||
"currentYear": 2026,
|
||||
"alerts": [],
|
||||
"serviceBills": [],
|
||||
"incomes": []
|
||||
}
|
||||
1
dist/404.html
vendored
Normal file
1
dist/404.html
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/4SrcMtBIfNF-pqHyUpitS/_buildManifest.js
vendored
Normal file
1
dist/_next/static/4SrcMtBIfNF-pqHyUpitS/_buildManifest.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
self.__BUILD_MANIFEST={__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/_error":["static/chunks/pages/_error-7ba65e1336b92748.js"],sortedPages:["/_app","/_error"]},self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();
|
||||
1
dist/_next/static/4SrcMtBIfNF-pqHyUpitS/_ssgManifest.js
vendored
Normal file
1
dist/_next/static/4SrcMtBIfNF-pqHyUpitS/_ssgManifest.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()
|
||||
2
dist/_next/static/chunks/117-14b82d0a7edfd352.js
vendored
Normal file
2
dist/_next/static/chunks/117-14b82d0a7edfd352.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/489-4b7fe4fae3b6ef80.js
vendored
Normal file
1
dist/_next/static/chunks/489-4b7fe4fae3b6ef80.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/697-93a9cd29100d0d50.js
vendored
Normal file
1
dist/_next/static/chunks/697-93a9cd29100d0d50.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/71-fccb418814321416.js
vendored
Normal file
1
dist/_next/static/chunks/71-fccb418814321416.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[71],{2489:function(e,t,r){r.d(t,{Z:function(){return n}});let n=(0,r(8755).Z)("x",[["path",{d:"M18 6 6 18",key:"1bl5f8"}],["path",{d:"m6 6 12 12",key:"d8bk6v"}]])},4147:function(e,t,r){let n;r.d(t,{Z:function(){return u}});var o={randomUUID:"undefined"!=typeof crypto&&crypto.randomUUID&&crypto.randomUUID.bind(crypto)};let a=new Uint8Array(16),i=[];for(let e=0;e<256;++e)i.push((e+256).toString(16).slice(1));var u=function(e,t,r){return!o.randomUUID||t||e?function(e,t,r){let o=(e=e||{}).random??e.rng?.()??function(){if(!n){if("undefined"==typeof crypto||!crypto.getRandomValues)throw Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported");n=crypto.getRandomValues.bind(crypto)}return n(a)}();if(o.length<16)throw Error("Random bytes length must be >= 16");if(o[6]=15&o[6]|64,o[8]=63&o[8]|128,t){if((r=r||0)<0||r+16>t.length)throw RangeError(`UUID byte range ${r}:${r+15} is out of buffer bounds`);for(let e=0;e<16;++e)t[r+e]=o[e];return t}return function(e,t=0){return(i[e[t+0]]+i[e[t+1]]+i[e[t+2]]+i[e[t+3]]+"-"+i[e[t+4]]+i[e[t+5]]+"-"+i[e[t+6]]+i[e[t+7]]+"-"+i[e[t+8]]+i[e[t+9]]+"-"+i[e[t+10]]+i[e[t+11]]+i[e[t+12]]+i[e[t+13]]+i[e[t+14]]+i[e[t+15]]).toLowerCase()}(o)}(e,t,r):o.randomUUID()}},6885:function(e,t,r){r.d(t,{tJ:function(){return o}});let n=e=>t=>{try{let r=e(t);if(r instanceof Promise)return r;return{then:e=>n(e)(r),catch(e){return this}}}catch(e){return{then(e){return this},catch:t=>n(t)(e)}}},o=(e,t)=>(r,o,a)=>{let i,u={storage:function(e,t){let r;try{r=e()}catch(e){return}return{getItem:e=>{var t;let n=e=>null===e?null:JSON.parse(e,void 0),o=null!=(t=r.getItem(e))?t:null;return o instanceof Promise?o.then(n):n(o)},setItem:(e,t)=>r.setItem(e,JSON.stringify(t,void 0)),removeItem:e=>r.removeItem(e)}}(()=>localStorage),partialize:e=>e,version:0,merge:(e,t)=>({...t,...e}),...t},l=!1,s=0,c=new Set,d=new Set,f=u.storage;if(!f)return e((...e)=>{console.warn(`[zustand persist middleware] Unable to update item '${u.name}', the given storage is currently unavailable.`),r(...e)},o,a);let m=()=>{let e=u.partialize({...o()});return f.setItem(u.name,{state:e,version:u.version})},g=a.setState;a.setState=(e,t)=>(g(e,t),m());let h=e((...e)=>(r(...e),m()),o,a);a.getInitialState=()=>h;let p=()=>{var e,t;if(!f)return;let a=++s;l=!1,c.forEach(e=>{var t;return e(null!=(t=o())?t:h)});let g=(null==(t=u.onRehydrateStorage)?void 0:t.call(u,null!=(e=o())?e:h))||void 0;return n(f.getItem.bind(f))(u.name).then(e=>{if(e){if("number"!=typeof e.version||e.version===u.version)return[!1,e.state];if(u.migrate){let t=u.migrate(e.state,e.version);return t instanceof Promise?t.then(e=>[!0,e]):[!0,t]}console.error("State loaded from storage couldn't be migrated since no migrate function was provided")}return[!1,void 0]}).then(e=>{var t;if(a!==s)return;let[n,l]=e;if(r(i=u.merge(l,null!=(t=o())?t:h),!0),n)return m()}).then(()=>{a===s&&(null==g||g(i,void 0),i=o(),l=!0,d.forEach(e=>e(i)))}).catch(e=>{a===s&&(null==g||g(void 0,e))})};return a.persist={setOptions:e=>{u={...u,...e},e.storage&&(f=e.storage)},clearStorage:()=>{null==f||f.removeItem(u.name)},getOptions:()=>u,rehydrate:()=>p(),hasHydrated:()=>l,onHydrate:e=>(c.add(e),()=>{c.delete(e)}),onFinishHydration:e=>(d.add(e),()=>{d.delete(e)})},u.skipHydration||p(),i||h}},3011:function(e,t,r){r.d(t,{U:function(){return l}});var n=r(2265);let o=e=>{let t;let r=new Set,n=(e,n)=>{let o="function"==typeof e?e(t):e;if(!Object.is(o,t)){let e=t;t=(null!=n?n:"object"!=typeof o||null===o)?o:Object.assign({},t,o),r.forEach(r=>r(t,e))}},o=()=>t,a={setState:n,getState:o,getInitialState:()=>i,subscribe:e=>(r.add(e),()=>r.delete(e))},i=t=e(n,o,a);return a},a=e=>e?o(e):o,i=e=>e,u=e=>{let t=a(e),r=e=>(function(e,t=i){let r=n.useSyncExternalStore(e.subscribe,n.useCallback(()=>t(e.getState()),[e,t]),n.useCallback(()=>t(e.getInitialState()),[e,t]));return n.useDebugValue(r),r})(t,e);return Object.assign(r,t),r},l=e=>e?u(e):u}}]);
|
||||
1
dist/_next/static/chunks/796-9d56fd2c469c90a6.js
vendored
Normal file
1
dist/_next/static/chunks/796-9d56fd2c469c90a6.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/838-3b22f3c21887b523.js
vendored
Normal file
1
dist/_next/static/chunks/838-3b22f3c21887b523.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/_not-found/page-e30cecccd190b7d4.js
vendored
Normal file
1
dist/_next/static/chunks/app/_not-found/page-e30cecccd190b7d4.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[409],{7589:function(e,t,n){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_not-found/page",function(){return n(3634)}])},3634:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"default",{enumerable:!0,get:function(){return s}}),n(7043);let i=n(7437);n(2265);let o={fontFamily:'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',height:"100vh",textAlign:"center",display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center"},l={display:"inline-block"},r={display:"inline-block",margin:"0 20px 0 0",padding:"0 23px 0 0",fontSize:24,fontWeight:500,verticalAlign:"top",lineHeight:"49px"},d={fontSize:14,fontWeight:400,lineHeight:"49px",margin:0};function s(){return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)("title",{children:"404: This page could not be found."}),(0,i.jsx)("div",{style:o,children:(0,i.jsxs)("div",{children:[(0,i.jsx)("style",{dangerouslySetInnerHTML:{__html:"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}),(0,i.jsx)("h1",{className:"next-error-h1",style:r,children:"404"}),(0,i.jsx)("div",{style:l,children:(0,i.jsx)("h2",{style:d,children:"This page could not be found."})})]})})]})}("function"==typeof t.default||"object"==typeof t.default&&null!==t.default)&&void 0===t.default.__esModule&&(Object.defineProperty(t.default,"__esModule",{value:!0}),Object.assign(t.default,t),e.exports=t.default)}},function(e){e.O(0,[971,117,744],function(){return e(e.s=7589)}),_N_E=e.O()}]);
|
||||
1
dist/_next/static/chunks/app/alerts/page-6f170ee2c75a0961.js
vendored
Normal file
1
dist/_next/static/chunks/app/alerts/page-6f170ee2c75a0961.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[78],{4033:function(e,s,n){Promise.resolve().then(n.bind(n,5949))},5949:function(e,s,n){"use strict";n.r(s),n.d(s,{default:function(){return c}});var l=n(7437),t=n(553),i=n(3263),a=n(9294);let r=(0,n(8755).Z)("refresh-cw",[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]]);function c(){let{isOpen:e,toggle:s,close:n}=(0,a.A)(),{regenerateAlerts:c,dismissAll:o,unreadCount:u}=(0,i.Z7)();return(0,l.jsxs)("div",{className:"min-h-screen bg-slate-950",children:[(0,l.jsx)(t.YE,{isOpen:e,onClose:n,unreadAlertsCount:u}),(0,l.jsxs)("div",{className:"lg:ml-64 min-h-screen flex flex-col",children:[(0,l.jsx)(t.h4,{onMenuClick:s,title:"Alertas"}),(0,l.jsx)("main",{className:"flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8",children:(0,l.jsxs)("div",{className:"max-w-4xl mx-auto",children:[(0,l.jsxs)("div",{className:"flex flex-wrap gap-3 mb-6",children:[(0,l.jsxs)("button",{onClick:()=>{c()},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",children:[(0,l.jsx)(r,{className:"h-4 w-4"}),"Regenerar Alertas"]}),(0,l.jsx)("button",{onClick:()=>{o()},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",children:"Limpiar Todas"})]}),(0,l.jsx)("div",{className:"w-full",children:(0,l.jsx)(i.KG,{})})]})}),(0,l.jsx)(t.zM,{unreadAlertsCount:u})]})]})}}},function(e){e.O(0,[697,71,796,489,971,117,744],function(){return e(e.s=4033)}),_N_E=e.O()}]);
|
||||
1
dist/_next/static/chunks/app/budget/page-1c1157916eee3b45.js
vendored
Normal file
1
dist/_next/static/chunks/app/budget/page-1c1157916eee3b45.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/cards/page-a9843d3be56b680d.js
vendored
Normal file
1
dist/_next/static/chunks/app/cards/page-a9843d3be56b680d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/debts/page-28aedff94a342b70.js
vendored
Normal file
1
dist/_next/static/chunks/app/debts/page-28aedff94a342b70.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/layout-639440f4d8b9b6b3.js
vendored
Normal file
1
dist/_next/static/chunks/app/layout-639440f4d8b9b6b3.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[185],{1556:function(e,n,t){Promise.resolve().then(t.bind(t,9294)),Promise.resolve().then(t.t.bind(t,8925,23)),Promise.resolve().then(t.t.bind(t,7960,23))},9294:function(e,n,t){"use strict";t.d(n,{A:function(){return u},Providers:function(){return s}});var r=t(7437),o=t(2265);let i=(0,o.createContext)(void 0);function s(e){let{children:n}=e,[t,s]=(0,o.useState)(!0);return(0,r.jsx)(i.Provider,{value:{isOpen:t,toggle:()=>s(e=>!e),close:()=>s(!1),open:()=>s(!0)},children:n})}function u(){let e=(0,o.useContext)(i);if(void 0===e)throw Error("useSidebar must be used within a Providers");return e}},7960:function(){},8925:function(e){e.exports={style:{fontFamily:"'__Inter_f367f3', '__Inter_Fallback_f367f3'",fontStyle:"normal"},className:"__className_f367f3",variable:"__variable_f367f3"}}},function(e){e.O(0,[832,971,117,744],function(){return e(e.s=1556)}),_N_E=e.O()}]);
|
||||
1
dist/_next/static/chunks/app/login/page-6be400e8521677b4.js
vendored
Normal file
1
dist/_next/static/chunks/app/login/page-6be400e8521677b4.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/page-c63080d1806e3966.js
vendored
Normal file
1
dist/_next/static/chunks/app/page-c63080d1806e3966.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/services/page-624950d2fabe2b7b.js
vendored
Normal file
1
dist/_next/static/chunks/app/services/page-624950d2fabe2b7b.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/app/settings/page-88d3fd0faab66996.js
vendored
Normal file
1
dist/_next/static/chunks/app/settings/page-88d3fd0faab66996.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/fd9d1056-307a36020502e7d7.js
vendored
Normal file
1
dist/_next/static/chunks/fd9d1056-307a36020502e7d7.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/framework-f66176bb897dc684.js
vendored
Normal file
1
dist/_next/static/chunks/framework-f66176bb897dc684.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/main-478a66ed427fba46.js
vendored
Normal file
1
dist/_next/static/chunks/main-478a66ed427fba46.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/_next/static/chunks/main-app-4119cbfe984dfca9.js
vendored
Normal file
1
dist/_next/static/chunks/main-app-4119cbfe984dfca9.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[744],{2300:function(e,n,t){Promise.resolve().then(t.t.bind(t,2846,23)),Promise.resolve().then(t.t.bind(t,9107,23)),Promise.resolve().then(t.t.bind(t,1060,23)),Promise.resolve().then(t.t.bind(t,4707,23)),Promise.resolve().then(t.t.bind(t,80,23)),Promise.resolve().then(t.t.bind(t,6423,23))}},function(e){var n=function(n){return e(e.s=n)};e.O(0,[971,117],function(){return n(4278),n(2300)}),_N_E=e.O()}]);
|
||||
1
dist/_next/static/chunks/pages/_app-72b849fbd24ac258.js
vendored
Normal file
1
dist/_next/static/chunks/pages/_app-72b849fbd24ac258.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[888],{1597:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_app",function(){return u(8141)}])}},function(n){var _=function(_){return n(n.s=_)};n.O(0,[774,179],function(){return _(1597),_(7253)}),_N_E=n.O()}]);
|
||||
1
dist/_next/static/chunks/pages/_error-7ba65e1336b92748.js
vendored
Normal file
1
dist/_next/static/chunks/pages/_error-7ba65e1336b92748.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{1981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(8529)}])}},function(n){n.O(0,[888,774,179],function(){return n(n.s=1981)}),_N_E=n.O()}]);
|
||||
1
dist/_next/static/chunks/webpack-785ddfc5aaaa81eb.js
vendored
Normal file
1
dist/_next/static/chunks/webpack-785ddfc5aaaa81eb.js
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function s(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,s),r=!1}finally{r&&delete l[e]}return n.exports}s.m=a,e=[],s.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(s.O).every(function(e){return s.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},s.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);s.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},s.d(o,u),o},s.d=function(e,t){for(var n in t)s.o(t,n)&&!s.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},s.f={},s.e=function(e){return Promise.all(Object.keys(s.f).reduce(function(t,n){return s.f[n](e,t),t},[]))},s.u=function(e){},s.miniCssF=function(e){},s.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",s.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,s.nc&&i.setAttribute("nonce",s.nc),i.setAttribute("data-webpack",o+n),i.src=s.tu(e)),r[e]=[t];var d=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=d.bind(null,i.onerror),i.onload=d.bind(null,i.onload),c&&document.head.appendChild(i)},s.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},s.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},s.tu=function(e){return s.tt().createScriptURL(e)},s.p="/_next/",i={272:0,832:0},s.f.j=function(e,t){var n=s.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(/^(27|83)2$/.test(e))i[e]=0;else{var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=s.p+s.u(e),u=Error();s.l(o,function(t){if(s.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}}},s.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)s.o(u,n)&&(s.m[n]=u[n]);if(c)var a=c(s)}for(e&&e(t);f<o.length;f++)r=o[f],s.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return s.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();
|
||||
3
dist/_next/static/css/9effa8aa186c096c.css
vendored
Normal file
3
dist/_next/static/css/9effa8aa186c096c.css
vendored
Normal file
File diff suppressed because one or more lines are too long
54
dist/alerts.html
vendored
Normal file
54
dist/alerts.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/alerts.txt
vendored
Normal file
8
dist/alerts.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[5949,["697","static/chunks/697-93a9cd29100d0d50.js","71","static/chunks/71-fccb418814321416.js","796","static/chunks/796-9d56fd2c469c90a6.js","489","static/chunks/489-4b7fe4fae3b6ef80.js","78","static/chunks/app/alerts/page-6f170ee2c75a0961.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["alerts",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["alerts",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","alerts","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
25
dist/app-build-manifest.json
vendored
25
dist/app-build-manifest.json
vendored
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"pages": {
|
||||
"/layout": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/css/app/layout.css",
|
||||
"static/chunks/app/layout.js"
|
||||
],
|
||||
"/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/page.js"
|
||||
],
|
||||
"/settings/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/settings/page.js"
|
||||
],
|
||||
"/_not-found/page": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js",
|
||||
"static/chunks/app/_not-found/page.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
54
dist/budget.html
vendored
Normal file
54
dist/budget.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/budget.txt
vendored
Normal file
8
dist/budget.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[7268,["697","static/chunks/697-93a9cd29100d0d50.js","71","static/chunks/71-fccb418814321416.js","796","static/chunks/796-9d56fd2c469c90a6.js","489","static/chunks/489-4b7fe4fae3b6ef80.js","379","static/chunks/app/budget/page-1c1157916eee3b45.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["budget",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["budget",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","budget","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
19
dist/build-manifest.json
vendored
19
dist/build-manifest.json
vendored
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"polyfillFiles": [
|
||||
"static/chunks/polyfills.js"
|
||||
],
|
||||
"devFiles": [],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [
|
||||
"static/development/_buildManifest.js",
|
||||
"static/development/_ssgManifest.js"
|
||||
],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"pages": {
|
||||
"/_app": []
|
||||
},
|
||||
"ampFirstPages": []
|
||||
}
|
||||
BIN
dist/cache/webpack/client-development/0.pack.gz
vendored
BIN
dist/cache/webpack/client-development/0.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/client-development/1.pack.gz
vendored
BIN
dist/cache/webpack/client-development/1.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/client-development/2.pack.gz
vendored
BIN
dist/cache/webpack/client-development/2.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/client-development/index.pack.gz
vendored
BIN
dist/cache/webpack/client-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
BIN
dist/cache/webpack/server-development/0.pack.gz
vendored
BIN
dist/cache/webpack/server-development/0.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/server-development/1.pack.gz
vendored
BIN
dist/cache/webpack/server-development/1.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/server-development/2.pack.gz
vendored
BIN
dist/cache/webpack/server-development/2.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/server-development/3.pack.gz
vendored
BIN
dist/cache/webpack/server-development/3.pack.gz
vendored
Binary file not shown.
BIN
dist/cache/webpack/server-development/index.pack.gz
vendored
BIN
dist/cache/webpack/server-development/index.pack.gz
vendored
Binary file not shown.
Binary file not shown.
54
dist/cards.html
vendored
Normal file
54
dist/cards.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/cards.txt
vendored
Normal file
8
dist/cards.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[3234,["697","static/chunks/697-93a9cd29100d0d50.js","71","static/chunks/71-fccb418814321416.js","796","static/chunks/796-9d56fd2c469c90a6.js","489","static/chunks/489-4b7fe4fae3b6ef80.js","137","static/chunks/app/cards/page-a9843d3be56b680d.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["cards",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["cards",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","cards","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
54
dist/debts.html
vendored
Normal file
54
dist/debts.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/debts.txt
vendored
Normal file
8
dist/debts.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[9220,["697","static/chunks/697-93a9cd29100d0d50.js","71","static/chunks/71-fccb418814321416.js","796","static/chunks/796-9d56fd2c469c90a6.js","489","static/chunks/489-4b7fe4fae3b6ef80.js","273","static/chunks/app/debts/page-28aedff94a342b70.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["debts",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["debts",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","debts","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
69
dist/index.html
vendored
Normal file
69
dist/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/index.txt
vendored
Normal file
8
dist/index.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[291,["697","static/chunks/697-93a9cd29100d0d50.js","71","static/chunks/71-fccb418814321416.js","796","static/chunks/796-9d56fd2c469c90a6.js","838","static/chunks/838-3b22f3c21887b523.js","489","static/chunks/489-4b7fe4fae3b6ef80.js","931","static/chunks/app/page-c63080d1806e3966.js"],"default",1]
|
||||
4:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
5:I[4707,[],""]
|
||||
6:I[6423,[],""]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["__PAGE__",{}]},"$undefined","$undefined",true],["",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L4",null,{"children":["$","$L5",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L6",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
1
dist/login.html
vendored
Normal file
1
dist/login.html
vendored
Normal file
File diff suppressed because one or more lines are too long
8
dist/login.txt
vendored
Normal file
8
dist/login.txt
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
2:I[9107,[],"ClientPageRoot"]
|
||||
3:I[6374,["626","static/chunks/app/login/page-6be400e8521677b4.js"],"default",1]
|
||||
4:I[4707,[],""]
|
||||
5:I[6423,[],""]
|
||||
6:I[9294,["185","static/chunks/app/layout-639440f4d8b9b6b3.js"],"Providers"]
|
||||
0:["4SrcMtBIfNF-pqHyUpitS",[[["",{"children":["login",{"children":["__PAGE__",{}]}]},"$undefined","$undefined",true],["",{"children":["login",{"children":["__PAGE__",{},[["$L1",["$","$L2",null,{"props":{"params":{},"searchParams":{}},"Component":"$3"}],null],null],null]},[null,["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children","login","children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":"$undefined","notFoundStyles":"$undefined"}]],null]},[[[["$","link","0",{"rel":"stylesheet","href":"/_next/static/css/9effa8aa186c096c.css","precedence":"next","crossOrigin":"$undefined"}]],["$","html",null,{"lang":"es","className":"__variable_f367f3","suppressHydrationWarning":true,"children":["$","body",null,{"className":"__className_f367f3 antialiased min-h-screen bg-slate-950 text-slate-50","children":["$","$L6",null,{"children":["$","$L4",null,{"parallelRouterKey":"children","segmentPath":["children"],"error":"$undefined","errorStyles":"$undefined","errorScripts":"$undefined","template":["$","$L5",null,{}],"templateStyles":"$undefined","templateScripts":"$undefined","notFound":[["$","title",null,{"children":"404: This page could not be found."}],["$","div",null,{"style":{"fontFamily":"system-ui,\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\"","height":"100vh","textAlign":"center","display":"flex","flexDirection":"column","alignItems":"center","justifyContent":"center"},"children":["$","div",null,{"children":[["$","style",null,{"dangerouslySetInnerHTML":{"__html":"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}"}}],["$","h1",null,{"className":"next-error-h1","style":{"display":"inline-block","margin":"0 20px 0 0","padding":"0 23px 0 0","fontSize":24,"fontWeight":500,"verticalAlign":"top","lineHeight":"49px"},"children":"404"}],["$","div",null,{"style":{"display":"inline-block"},"children":["$","h2",null,{"style":{"fontSize":14,"fontWeight":400,"lineHeight":"49px","margin":0},"children":"This page could not be found."}]}]]}]}]],"notFoundStyles":[]}]}]}]}]],null],null],["$L7",null]]]]
|
||||
7:[["$","meta","0",{"name":"viewport","content":"width=device-width, initial-scale=1"}],["$","meta","1",{"charSet":"utf-8"}],["$","title","2",{"children":"Finanzas Personales"}],["$","meta","3",{"name":"description","content":"Gestiona tus finanzas personales de forma inteligente"}],["$","meta","4",{"name":"keywords","content":"finanzas,presupuesto,gastos,ingresos,ahorro"}],["$","meta","5",{"name":"next-size-adjust"}]]
|
||||
1:null
|
||||
1
dist/package.json
vendored
1
dist/package.json
vendored
@@ -1 +0,0 @@
|
||||
{"type": "commonjs"}
|
||||
1
dist/react-loadable-manifest.json
vendored
1
dist/react-loadable-manifest.json
vendored
@@ -1 +0,0 @@
|
||||
{}
|
||||
7
dist/server/app-paths-manifest.json
vendored
7
dist/server/app-paths-manifest.json
vendored
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"/page": "app/page.js",
|
||||
"/api/settings/route": "app/api/settings/route.js",
|
||||
"/settings/page": "app/settings/page.js",
|
||||
"/api/test/ai/route": "app/api/test/ai/route.js",
|
||||
"/api/proxy/models/route": "app/api/proxy/models/route.js"
|
||||
}
|
||||
155
dist/server/app/_not-found/page.js
vendored
155
dist/server/app/_not-found/page.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
66
dist/server/app/api/proxy/models/route.js
vendored
66
dist/server/app/api/proxy/models/route.js
vendored
File diff suppressed because one or more lines are too long
86
dist/server/app/api/settings/route.js
vendored
86
dist/server/app/api/settings/route.js
vendored
File diff suppressed because one or more lines are too long
66
dist/server/app/api/test/ai/route.js
vendored
66
dist/server/app/api/test/ai/route.js
vendored
File diff suppressed because one or more lines are too long
557
dist/server/app/page.js
vendored
557
dist/server/app/page.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
205
dist/server/app/settings/page.js
vendored
205
dist/server/app/settings/page.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"
|
||||
21
dist/server/middleware-build-manifest.js
vendored
21
dist/server/middleware-build-manifest.js
vendored
@@ -1,21 +0,0 @@
|
||||
self.__BUILD_MANIFEST = {
|
||||
"polyfillFiles": [
|
||||
"static/chunks/polyfills.js"
|
||||
],
|
||||
"devFiles": [],
|
||||
"ampDevFiles": [],
|
||||
"lowPriorityFiles": [],
|
||||
"rootMainFiles": [
|
||||
"static/chunks/webpack.js",
|
||||
"static/chunks/main-app.js"
|
||||
],
|
||||
"pages": {
|
||||
"/_app": []
|
||||
},
|
||||
"ampFirstPages": []
|
||||
};
|
||||
self.__BUILD_MANIFEST.lowPriorityFiles = [
|
||||
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
|
||||
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
|
||||
|
||||
];
|
||||
6
dist/server/middleware-manifest.json
vendored
6
dist/server/middleware-manifest.json
vendored
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"version": 3,
|
||||
"middleware": {},
|
||||
"functions": {},
|
||||
"sortedMiddleware": []
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
self.__REACT_LOADABLE_MANIFEST="{}"
|
||||
1
dist/server/next-font-manifest.js
vendored
1
dist/server/next-font-manifest.js
vendored
@@ -1 +0,0 @@
|
||||
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"
|
||||
1
dist/server/next-font-manifest.json
vendored
1
dist/server/next-font-manifest.json
vendored
@@ -1 +0,0 @@
|
||||
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}
|
||||
1
dist/server/pages-manifest.json
vendored
1
dist/server/pages-manifest.json
vendored
@@ -1 +0,0 @@
|
||||
{}
|
||||
1
dist/server/server-reference-manifest.js
vendored
1
dist/server/server-reference-manifest.js
vendored
@@ -1 +0,0 @@
|
||||
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"
|
||||
5
dist/server/server-reference-manifest.json
vendored
5
dist/server/server-reference-manifest.json
vendored
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"node": {},
|
||||
"edge": {},
|
||||
"encryptionKey": "bNoGWlCooOUGQMrF3XDKlehaXh5PleSvIPcww3mfySw="
|
||||
}
|
||||
35
dist/server/vendor-chunks/@reduxjs.js
vendored
35
dist/server/vendor-chunks/@reduxjs.js
vendored
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user