Compare commits

...

5 Commits

Author SHA1 Message Date
Renato97
f7b5d3777d chore: clean sensitive data and markdown files for CV
- Remove data/ folder containing user credentials and personal info

- Remove unnecessary markdown files
2026-03-31 01:16:13 -03:00
ren
759f0a636b Fix: DataSync listener bug preventing updates from client to server 2026-01-29 15:13:09 +01:00
ren
e5c9de2df5 Fix: Multi-user data isolation and Legacy migration logic 2026-01-29 15:03:07 +01:00
ren
020218275f Feat: Add complete auth system (Login, Register, OTP/2FA via Telegram, Session management) 2026-01-29 14:57:19 +01:00
ren
811c78ffa5 Refactor: Implement DashboardLayout, fix mobile nav, and resolve scroll issues 2026-01-29 14:41:46 +01:00
177 changed files with 2228 additions and 24509 deletions

5
.gitignore vendored
View File

@@ -32,3 +32,8 @@ next-env.d.ts
# Security
server-settings.json
.env
auth-otp.json
# Local dev
local_Caddyfile

View File

@@ -1,136 +0,0 @@
# 🚀 Deployment Guideline: Finanzas Personales (Next.js + Telegram Bot)
This document contains step-by-step instructions to deploy the "Finanzas Personales" application to a Linux VPS (Ubuntu/Debian recommended).
## 1. Prerequisites
- **VPS**: A server with Ubuntu 22.04 or later.
- **Domain**: A domain pointing to your VPS IP (e.g., `finanzas.tusitio.com`).
- **Gitea Access**: Ensure the VPS can pull from your private Gitea repository.
## 2. Server Setup (Run as root/sudo)
```bash
# Update system
apt update && apt upgrade -y
# Install Node.js 18+ (using NVM is recommended, but apt works for deployment)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
apt install -y nodejs nginx git
# Install Process Manager (PM2)
npm install -g pm2
```
## 3. Clone Repository & Install Dependencies
```bash
# Navigate to web directory
cd /var/www
# Clone your repo (Use your Gitea token if private)
git clone https://<YOUR_GITEA_URL>/<USER>/finanzas.git
cd finanzas
# Install dependencies
npm install
# Build the Next.js app
npm run build
```
## 4. Environment Configuration
Since we use a custom `server-settings.json`, you need to configure it manually on the server once.
```bash
# Create the settings file manually (or copy from local if you have sw access)
nano server-settings.json
```
Paste your JSON configuration (get this from your local `server-settings.json`):
```json
{
"telegram": {
"botToken": "YOUR_BOT_TOKEN",
"chatId": "YOUR_CHAT_ID"
},
"aiProviders": [
{
"id": "uuid...",
"name": "Service Name",
"endpoint": "https://api...",
"token": "sk-...",
"model": "gpt-4o"
}
]
}
```
## 5. Startup with PM2 (Keep Apps Alive)
We need to run two processes: The Next.js Web App and the Telegram Bot.
```bash
# 1. Start Next.js (Port 3000)
pm2 start npm --name "finanzas-web" -- start -- -p 3000
# 2. Start Telegram Bot
pm2 start npm --name "finanzas-bot" -- run bot
# Save list so they revive on reboot
pm2 save
pm2 startup
```
## 6. Nginx Reverse Proxy (Expose to Internet)
Configure Nginx to safely expose port 3000 to the web.
```bash
nano /etc/nginx/sites-available/finanzas
```
Add this content:
```nginx
server {
listen 80;
server_name finanzas.tusitio.com; # REPLACE THIS
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
```
Enable site and restart Nginx:
```bash
ln -s /etc/nginx/sites-available/finanzas /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx
```
## 7. SSL Certificate (HTTPS)
Use Certbot to secure your site for free.
```bash
apt install -y certbot python3-certbot-nginx
certbot --nginx -d finanzas.tusitio.com
```
---
## 8. Updates
When you push new code to Gitea, update the server:
```bash
cd /var/www/finanzas
git pull
npm install
npm run build
pm2 restart finanzas-web
pm2 restart finanzas-bot
```

View File

@@ -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,16 +16,10 @@ export default function AlertsPage() {
}
return (
<div className="min-h-screen bg-slate-950">
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
<div className="lg:ml-64 min-h-screen flex flex-col">
<Header onMenuClick={toggle} title="Alertas" />
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8">
<div className="max-w-4xl mx-auto">
<DashboardLayout title="Alertas">
<div className="space-y-6">
{/* Action Buttons */}
<div className="flex flex-wrap gap-3 mb-6">
<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"
@@ -49,10 +41,6 @@ export default function AlertsPage() {
<AlertPanel />
</div>
</div>
</main>
<MobileNav unreadAlertsCount={unreadCount} />
</div>
</div>
</DashboardLayout>
)
}

View File

@@ -0,0 +1,54 @@
import { NextResponse } from 'next/server';
import { findUser, verifyPassword, createSession } from '@/lib/auth';
import { generateOTP } from '@/lib/otp';
import TelegramBot from 'node-telegram-bot-api';
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
async function sendTelegramOTP(chatId: string, otp: string) {
if (!BOT_TOKEN) return false;
try {
const bot = new TelegramBot(BOT_TOKEN, { polling: false });
await bot.sendMessage(chatId, `🔐 *Código de Acceso Finanzas*\n\nTu código es: \`${otp}\`\n\nSi no intentaste ingresar, ignora este mensaje.`, { parse_mode: 'Markdown' });
return true;
} catch (e) {
console.error('Telegram send error:', e);
return false;
}
}
export async function POST(req: Request) {
try {
const { username, password } = await req.json();
const ip = req.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown';
const user = findUser(username);
if (!user || !(await verifyPassword(password, user.passwordHash))) {
return NextResponse.json({ error: 'Credenciales inválidas' }, { status: 401 });
}
// Check IP
const isKnownIp = user.knownIps.includes(ip);
if (isKnownIp) {
// Login success directly
await createSession(user);
return NextResponse.json({ success: true, requireOtp: false });
} else {
// Require OTP
const otp = generateOTP(user.username);
const sent = await sendTelegramOTP(user.chatId, otp);
if (!sent) {
return NextResponse.json({ error: 'No se pudo enviar el código OTP a Telegram' }, { status: 500 });
}
return NextResponse.json({ success: true, requireOtp: true });
}
} catch (error) {
console.error('Login error:', error);
return NextResponse.json({ error: 'Error interno' }, { status: 500 });
}
}

View File

@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
import { destroySession } from '@/lib/auth';
export async function POST() {
destroySession();
return NextResponse.json({ success: true });
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import { saveUser, findUser, hashPassword, createSession } from '@/lib/auth';
import { randomUUID } from 'crypto';
export async function POST(req: Request) {
try {
const { username, password, chatId } = await req.json();
if (!username || !password || !chatId) {
return NextResponse.json({ error: 'Faltan datos requeridos' }, { status: 400 });
}
if (findUser(username)) {
return NextResponse.json({ error: 'El usuario ya existe' }, { status: 409 });
}
// Hash password
const passwordHash = await hashPassword(password);
// Get IP
const ip = req.headers.get('x-forwarded-for') || '127.0.0.1';
const newUser = {
id: randomUUID(),
username,
passwordHash,
chatId,
knownIps: [ip] // Register current IP as known initially
};
saveUser(newUser);
// Auto login after register
await createSession(newUser);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Register error:', error);
return NextResponse.json({ error: 'Error interno del servidor' }, { status: 500 });
}
}

View File

@@ -0,0 +1,34 @@
import { NextResponse } from 'next/server';
import { findUser, saveUser, createSession } from '@/lib/auth';
import { verifyOTP } from '@/lib/otp';
export async function POST(req: Request) {
try {
const { username, otp } = await req.json();
const ip = req.headers.get('x-forwarded-for')?.split(',')[0].trim() || 'unknown';
if (!verifyOTP(username, otp)) {
return NextResponse.json({ error: 'Código inválido o expirado' }, { status: 401 });
}
const user = findUser(username);
if (!user) {
return NextResponse.json({ error: 'Usuario no encontrado' }, { status: 404 });
}
// Add IP to known list if not exists
if (!user.knownIps.includes(ip) && ip !== 'unknown') {
user.knownIps.push(ip);
saveUser(user);
}
// Login success
await createSession(user);
return NextResponse.json({ success: true });
} catch (error) {
console.error('OTP Verify error:', error);
return NextResponse.json({ error: 'Error interno' }, { status: 500 });
}
}

28
app/api/sync/route.ts Normal file
View File

@@ -0,0 +1,28 @@
import { NextResponse } from 'next/server';
import { getDatabase, saveDatabase } from '@/lib/server-db';
import { verifySession } from '@/lib/auth';
export async function GET() {
const session = await verifySession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const data = getDatabase(session.username);
return NextResponse.json(data);
}
export async function POST(req: Request) {
const session = await verifySession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const body = await req.json();
saveDatabase(session.username, body);
return NextResponse.json({ success: true });
} catch (error) {
return NextResponse.json({ error: 'Invalid Data' }, { status: 400 });
}
}

View File

@@ -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">
<DashboardLayout title="Presupuesto">
<BudgetSection />
</main>
<MobileNav unreadAlertsCount={unreadCount} />
</div>
</div>
</DashboardLayout>
)
}

View File

@@ -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">
<DashboardLayout title="Tarjetas de Crédito">
<CardSection />
</main>
</div>
<MobileNav unreadAlertsCount={unreadCount} />
</div>
</DashboardLayout>
);
}

View File

@@ -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">
<DashboardLayout title="Deudas">
<DebtSection />
</main>
</div>
{/* Mobile Navigation */}
<MobileNav unreadAlertsCount={unreadCount} />
</div>
</DashboardLayout>
)
}

165
app/incomes/page.tsx Normal file
View 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>
)
}

193
app/login/page.tsx Normal file
View File

@@ -0,0 +1,193 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Lock, User, Key, ArrowRight, ShieldCheck, Loader2 } from 'lucide-react';
import Link from 'next/link';
export default function LoginPage() {
const router = useRouter();
const [step, setStep] = useState<'credentials' | 'otp'>('credentials');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
username: '',
password: '',
otp: ''
});
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: formData.username, password: formData.password })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Error al iniciar sesión');
if (data.requireOtp) {
setStep('otp');
} else {
router.push('/');
router.refresh();
}
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
const handleVerifyOtp = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('/api/auth/verify-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: formData.username, otp: formData.otp })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Código incorrecto');
router.push('/');
router.refresh();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-slate-900 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
{/* Header */}
<div className="bg-slate-950/50 p-6 text-center border-b border-slate-800">
<div className="mx-auto w-12 h-12 bg-emerald-500/10 rounded-xl flex items-center justify-center mb-4">
<Lock className="w-6 h-6 text-emerald-500" />
</div>
<h1 className="text-2xl font-bold text-white mb-1">Bienvenido</h1>
<p className="text-slate-400 text-sm">Sistema de Finanzas Personales</p>
</div>
<div className="p-6">
{error && (
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm text-center">
{error}
</div>
)}
{step === 'credentials' ? (
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Usuario</label>
<div className="relative">
<User className="absolute left-3 top-2.5 w-5 h-5 text-slate-500" />
<input
type="text"
required
value={formData.username}
onChange={e => setFormData({...formData, username: e.target.value})}
className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all placeholder:text-slate-600"
placeholder="Ingresa tu usuario"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Contraseña</label>
<div className="relative">
<Key className="absolute left-3 top-2.5 w-5 h-5 text-slate-500" />
<input
type="password"
required
value={formData.password}
onChange={e => setFormData({...formData, password: e.target.value})}
className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-emerald-500 focus:border-emerald-500 outline-none transition-all placeholder:text-slate-600"
placeholder="••••••••"
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-medium py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{loading ? <Loader2 className="animate-spin w-5 h-5" /> : (
<>
Ingresar <ArrowRight className="w-4 h-4" />
</>
)}
</button>
</form>
) : (
<form onSubmit={handleVerifyOtp} className="space-y-4 animate-in slide-in-from-right-8 fade-in duration-300">
<div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-500/10 rounded-full mb-4">
<ShieldCheck className="w-8 h-8 text-blue-500" />
</div>
<h3 className="text-lg font-medium text-white">Verificación de Identidad</h3>
<p className="text-slate-400 text-sm mt-1">
Hemos enviado un código a tu Telegram.
<br />Ingrésalo para continuar.
</p>
</div>
<div className="space-y-2">
<input
type="text"
required
autoFocus
maxLength={6}
value={formData.otp}
onChange={e => setFormData({...formData, otp: e.target.value.replace(/\D/g, '')})}
className="w-full text-center text-2xl tracking-[0.5em] font-mono py-3 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all"
placeholder="000000"
/>
</div>
<button
type="submit"
disabled={loading || formData.otp.length !== 6}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? <Loader2 className="animate-spin w-5 h-5" /> : 'Verificar Código'}
</button>
<button
type="button"
onClick={() => setStep('credentials')}
className="w-full text-slate-500 text-sm hover:text-slate-300 transition-colors"
>
Volver atrás
</button>
</form>
)}
</div>
<div className="bg-slate-950/50 p-4 text-center border-t border-slate-800">
<p className="text-slate-500 text-sm">
¿No tienes cuenta?{' '}
<Link href="/register" className="text-emerald-500 hover:text-emerald-400 font-medium hover:underline">
Regístrate aquí
</Link>
</p>
</div>
</div>
</div>
);
}

View File

@@ -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,22 +44,8 @@ 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">
<DashboardLayout title="Dashboard">
<div className="space-y-6">
{/* Alertas destacadas */}
{topAlerts.length > 0 && (
<div className="space-y-3">
@@ -108,11 +73,6 @@ export default function Home() {
{/* Actividad reciente */}
<RecentActivity limit={5} />
</div>
</main>
</div>
{/* Mobile navigation */}
<MobileNav unreadAlertsCount={unreadCount} />
{/* Modales */}
<AddDebtModal
@@ -129,6 +89,6 @@ export default function Home() {
isOpen={isAddPaymentModalOpen}
onClose={() => setIsAddPaymentModalOpen(false)}
/>
</div>
</DashboardLayout>
)
}

View File

@@ -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>
);

135
app/register/page.tsx Normal file
View File

@@ -0,0 +1,135 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { UserPlus, User, Key, MessageSquare, Loader2 } from 'lucide-react';
import Link from 'next/link';
export default function RegisterPage() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [formData, setFormData] = useState({
username: '',
password: '',
chatId: ''
});
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Error al registrarse');
router.push('/');
router.refresh();
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-slate-900 border border-slate-800 rounded-2xl shadow-xl overflow-hidden">
{/* Header */}
<div className="bg-slate-950/50 p-6 text-center border-b border-slate-800">
<div className="mx-auto w-12 h-12 bg-blue-500/10 rounded-xl flex items-center justify-center mb-4">
<UserPlus className="w-6 h-6 text-blue-500" />
</div>
<h1 className="text-2xl font-bold text-white mb-1">Crear Cuenta</h1>
<p className="text-slate-400 text-sm">Configura tu acceso a Finanzas</p>
</div>
<div className="p-6">
{error && (
<div className="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm text-center">
{error}
</div>
)}
<form onSubmit={handleRegister} className="space-y-4">
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Usuario</label>
<div className="relative">
<User className="absolute left-3 top-2.5 w-5 h-5 text-slate-500" />
<input
type="text"
required
value={formData.username}
onChange={e => setFormData({...formData, username: e.target.value})}
className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600"
placeholder="Elige un usuario"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Contraseña</label>
<div className="relative">
<Key className="absolute left-3 top-2.5 w-5 h-5 text-slate-500" />
<input
type="password"
required
value={formData.password}
onChange={e => setFormData({...formData, password: e.target.value})}
className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600"
placeholder="Mínimo 6 caracteres"
minLength={6}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Telegram Chat ID</label>
<div className="relative">
<MessageSquare className="absolute left-3 top-2.5 w-5 h-5 text-slate-500" />
<input
type="text"
required
value={formData.chatId}
onChange={e => setFormData({...formData, chatId: e.target.value})}
className="w-full pl-10 pr-4 py-2.5 bg-slate-950 border border-slate-800 rounded-lg text-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all placeholder:text-slate-600"
placeholder="Ej: 123456789"
/>
</div>
<p className="text-[10px] text-slate-500 flex gap-1">
<span></span>
Envía /start a tu bot para obtener este ID.
</p>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 hover:bg-blue-500 text-white font-medium py-2.5 rounded-lg transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
>
{loading ? <Loader2 className="animate-spin w-5 h-5" /> : 'Registrar Cuenta'}
</button>
</form>
</div>
<div className="bg-slate-950/50 p-4 text-center border-t border-slate-800">
<p className="text-slate-500 text-sm">
¿Ya tienes cuenta?{' '}
<Link href="/login" className="text-blue-500 hover:text-blue-400 font-medium hover:underline">
Inicia Sesión
</Link>
</p>
</div>
</div>
</div>
);
}

View File

@@ -7,6 +7,7 @@ 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' },
@@ -20,12 +21,13 @@ export default function ServicesPage() {
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>
<h1 className="text-2xl font-bold text-white">Servicios y Predicciones</h1>
<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
@@ -138,5 +140,6 @@ export default function ServicesPage() {
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
</div>
</DashboardLayout>
)
}

View File

@@ -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,11 +160,12 @@ export default function SettingsPage() {
if (loading) return <div className="p-8 text-center text-slate-400">Cargando configuración...</div>
return (
<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>
<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
@@ -365,5 +367,6 @@ export default function SettingsPage() {
</div>
</section>
</div>
</DashboardLayout>
)
}

83
components/DataSync.tsx Normal file
View File

@@ -0,0 +1,83 @@
'use client';
import { useEffect, useRef } from 'react';
import { useFinanzasStore } from '@/lib/store';
export function DataSync() {
const initialized = useRef(false);
useEffect(() => {
async function init() {
// Prevent double init in StrictMode
if (initialized.current) return;
try {
const res = await fetch('/api/sync');
// If 401 (unauthorized), stop here. User needs to login.
if (res.status === 401) return;
if (!res.ok) return;
const serverData = await res.json();
// Comprehensive check if server has ANY data
const hasServerData =
(serverData.fixedDebts?.length ?? 0) > 0 ||
(serverData.variableDebts?.length ?? 0) > 0 ||
(serverData.creditCards?.length ?? 0) > 0 ||
(serverData.incomes?.length ?? 0) > 0 ||
(serverData.serviceBills?.length ?? 0) > 0;
if (hasServerData) {
console.log("Sync: Hydrating from Server");
useFinanzasStore.setState(serverData);
} else {
// Server is empty. If we have local data, push it.
// But verify we actually have local data worth pushing to avoid overwriting with empty defaults unnecessarily
const localState = useFinanzasStore.getState();
const hasLocalData =
localState.fixedDebts.length > 0 ||
localState.variableDebts.length > 0 ||
localState.creditCards.length > 0 ||
localState.incomes.length > 0;
if (hasLocalData) {
console.log("Sync: Server empty, pushing Local Data");
syncToServer(localState);
}
}
} catch (e) {
console.error("Sync init error", e);
} finally {
// Mark as initialized so subsequent changes trigger sync
initialized.current = true;
}
}
init();
}, []);
// Sync on change
useEffect(() => {
const unsub = useFinanzasStore.subscribe((state) => {
// Only sync to server if we have finished initialization/hydration
if (initialized.current) {
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
}

View 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>
)
}

View File

@@ -8,9 +8,12 @@ import {
Bell,
Lightbulb,
Settings,
TrendingUp,
X,
LogOut,
} from 'lucide-react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { usePathname } from 'next/navigation';
import { Logo } from './Logo';
@@ -22,6 +25,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 },
@@ -36,6 +40,7 @@ export function Sidebar({
unreadAlertsCount = 0,
}: SidebarProps) {
const pathname = usePathname();
const router = useRouter();
const isActive = (href: string) => {
if (href === '/') {
@@ -44,6 +49,18 @@ export function Sidebar({
return pathname.startsWith(href);
};
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
// Clear local data to avoid leaking to other users
localStorage.removeItem('finanzas-storage');
router.push('/login');
router.refresh();
} catch (e) {
console.error('Logout failed', e);
}
};
return (
<>
{/* Mobile overlay */}
@@ -110,6 +127,16 @@ export function Sidebar({
);
})}
</ul>
<div className="mt-auto px-3 py-3 border-t border-slate-800">
<button
onClick={handleLogout}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-slate-400 hover:bg-red-500/10 hover:text-red-400 transition-colors"
>
<LogOut className="w-5 h-5 flex-shrink-0" />
<span className="flex-1 text-left">Cerrar Sesión</span>
</button>
</div>
</nav>
{/* Footer */}

1
dist/404.html vendored Normal file

File diff suppressed because one or more lines are too long

View 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();

View File

@@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set([]);self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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()}]);

View 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()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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()}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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()}]);

View 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()}]);

View 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()}]);

View 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))}();

File diff suppressed because one or more lines are too long

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
View 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

View File

@@ -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

File diff suppressed because one or more lines are too long

8
dist/budget.txt vendored Normal file
View 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

View File

@@ -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": []
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

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
View 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

File diff suppressed because one or more lines are too long

8
dist/debts.txt vendored Normal file
View 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

File diff suppressed because one or more lines are too long

8
dist/index.txt vendored Normal file
View 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

File diff suppressed because one or more lines are too long

8
dist/login.txt vendored Normal file
View 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
View File

@@ -1 +0,0 @@
{"type": "commonjs"}

View File

@@ -1 +0,0 @@
{}

View File

@@ -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"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@@ -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",
];

View File

@@ -1,6 +0,0 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@@ -1 +0,0 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@@ -1 +0,0 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"

View File

@@ -1 +0,0 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}

View File

@@ -1 +0,0 @@
{}

Some files were not shown because too many files have changed in this diff Show More