Refactor: Implement DashboardLayout, fix mobile nav, and resolve scroll issues
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -32,3 +32,8 @@ next-env.d.ts
|
|||||||
|
|
||||||
# Security
|
# Security
|
||||||
server-settings.json
|
server-settings.json
|
||||||
|
.env
|
||||||
|
auth-otp.json
|
||||||
|
|
||||||
|
# Local dev
|
||||||
|
local_Caddyfile
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
import { AlertPanel, useAlerts } from '@/components/alerts'
|
import { AlertPanel, useAlerts } from '@/components/alerts'
|
||||||
import { useSidebar } from '@/app/providers'
|
|
||||||
import { RefreshCw } from 'lucide-react'
|
import { RefreshCw } from 'lucide-react'
|
||||||
|
|
||||||
export default function AlertsPage() {
|
export default function AlertsPage() {
|
||||||
const { isOpen, toggle, close } = useSidebar()
|
const { regenerateAlerts, dismissAll } = useAlerts()
|
||||||
const { regenerateAlerts, dismissAll, unreadCount } = useAlerts()
|
|
||||||
|
|
||||||
const handleRegenerateAlerts = () => {
|
const handleRegenerateAlerts = () => {
|
||||||
regenerateAlerts()
|
regenerateAlerts()
|
||||||
@@ -18,41 +16,31 @@ export default function AlertsPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950">
|
<DashboardLayout title="Alertas">
|
||||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
<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">
|
<button
|
||||||
<Header onMenuClick={toggle} title="Alertas" />
|
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">
|
{/* Alert Panel */}
|
||||||
<div className="max-w-4xl mx-auto">
|
<div className="w-full">
|
||||||
{/* Action Buttons */}
|
<AlertPanel />
|
||||||
<div className="flex flex-wrap gap-3 mb-6">
|
</div>
|
||||||
<button
|
|
||||||
onClick={handleRegenerateAlerts}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/20"
|
|
||||||
>
|
|
||||||
<RefreshCw className="h-4 w-4" />
|
|
||||||
Regenerar Alertas
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleDismissAll}
|
|
||||||
className="inline-flex items-center gap-2 px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white text-sm font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-slate-500/20"
|
|
||||||
>
|
|
||||||
Limpiar Todas
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Alert Panel */}
|
|
||||||
<div className="w-full">
|
|
||||||
<AlertPanel />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<MobileNav unreadAlertsCount={unreadCount} />
|
|
||||||
</div>
|
</div>
|
||||||
</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'
|
'use client'
|
||||||
|
|
||||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
import { BudgetSection } from '@/components/budget'
|
import { BudgetSection } from '@/components/budget'
|
||||||
import { useSidebar } from '@/app/providers'
|
|
||||||
import { useAlerts } from '@/components/alerts'
|
|
||||||
|
|
||||||
export default function BudgetPage() {
|
export default function BudgetPage() {
|
||||||
const { isOpen, close, toggle } = useSidebar()
|
|
||||||
const { unreadCount } = useAlerts()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-950">
|
<DashboardLayout title="Presupuesto">
|
||||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
<BudgetSection />
|
||||||
|
</DashboardLayout>
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Sidebar, Header, MobileNav } from '@/components/layout';
|
import { DashboardLayout } from '@/components/layout/DashboardLayout';
|
||||||
import { CardSection } from '@/components/cards';
|
import { CardSection } from '@/components/cards';
|
||||||
import { useSidebar } from '@/app/providers';
|
|
||||||
import { useAlerts } from '@/components/alerts';
|
|
||||||
|
|
||||||
export default function CardsPage() {
|
export default function CardsPage() {
|
||||||
const { isOpen, toggle, close } = useSidebar();
|
|
||||||
const { unreadCount } = useAlerts();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950">
|
<DashboardLayout title="Tarjetas de Crédito">
|
||||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
<CardSection />
|
||||||
|
</DashboardLayout>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
import { DebtSection } from '@/components/debts'
|
import { DebtSection } from '@/components/debts'
|
||||||
import { useSidebar } from '@/app/providers'
|
|
||||||
import { useAlerts } from '@/components/alerts'
|
|
||||||
|
|
||||||
export default function DebtsPage() {
|
export default function DebtsPage() {
|
||||||
const { isOpen, close, open } = useSidebar()
|
|
||||||
const { unreadCount } = useAlerts()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-950">
|
<DashboardLayout title="Deudas">
|
||||||
{/* Sidebar */}
|
<DebtSection />
|
||||||
<Sidebar isOpen={isOpen} onClose={close} unreadAlertsCount={unreadCount} />
|
</DashboardLayout>
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
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'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Sidebar, Header, MobileNav } from '@/components/layout'
|
|
||||||
import { SummarySection, QuickActions, RecentActivity } from '@/components/dashboard'
|
import { SummarySection, QuickActions, RecentActivity } from '@/components/dashboard'
|
||||||
import { useSidebar } from '@/app/providers'
|
|
||||||
import { useFinanzasStore } from '@/lib/store'
|
import { useFinanzasStore } from '@/lib/store'
|
||||||
import { AlertBanner, useAlerts } from '@/components/alerts'
|
import { AlertBanner, useAlerts } from '@/components/alerts'
|
||||||
import { AddDebtModal } from '@/components/modals/AddDebtModal'
|
import { AddDebtModal } from '@/components/modals/AddDebtModal'
|
||||||
import { AddCardModal } from '@/components/modals/AddCardModal'
|
import { AddCardModal } from '@/components/modals/AddCardModal'
|
||||||
import { AddPaymentModal } from '@/components/modals/AddPaymentModal'
|
import { AddPaymentModal } from '@/components/modals/AddPaymentModal'
|
||||||
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
// Sidebar control
|
|
||||||
const sidebar = useSidebar()
|
|
||||||
|
|
||||||
// Datos del store
|
// Datos del store
|
||||||
const markAlertAsRead = useFinanzasStore((state) => state.markAlertAsRead)
|
const markAlertAsRead = useFinanzasStore((state) => state.markAlertAsRead)
|
||||||
const deleteAlert = useFinanzasStore((state) => state.deleteAlert)
|
const deleteAlert = useFinanzasStore((state) => state.deleteAlert)
|
||||||
|
|
||||||
// Alertas
|
// Alertas
|
||||||
const { unreadAlerts, unreadCount, regenerateAlerts } = useAlerts()
|
const { unreadAlerts, regenerateAlerts } = useAlerts()
|
||||||
|
|
||||||
// Estados locales para modales
|
// Estados locales para modales
|
||||||
const [isAddDebtModalOpen, setIsAddDebtModalOpen] = useState(false)
|
const [isAddDebtModalOpen, setIsAddDebtModalOpen] = useState(false)
|
||||||
@@ -31,23 +27,6 @@ export default function Home() {
|
|||||||
regenerateAlerts()
|
regenerateAlerts()
|
||||||
}, [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
|
// Handlers para modales
|
||||||
const handleAddDebt = () => {
|
const handleAddDebt = () => {
|
||||||
setIsAddDebtModalOpen(true)
|
setIsAddDebtModalOpen(true)
|
||||||
@@ -65,54 +44,35 @@ export default function Home() {
|
|||||||
const topAlerts = unreadAlerts.slice(0, 3)
|
const topAlerts = unreadAlerts.slice(0, 3)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-950">
|
<DashboardLayout title="Dashboard">
|
||||||
{/* Sidebar */}
|
<div className="space-y-6">
|
||||||
<Sidebar
|
{/* Alertas destacadas */}
|
||||||
isOpen={sidebar.isOpen}
|
{topAlerts.length > 0 && (
|
||||||
onClose={sidebar.close}
|
<div className="space-y-3">
|
||||||
unreadAlertsCount={unreadCount}
|
{topAlerts.map((alert) => (
|
||||||
/>
|
<AlertBanner
|
||||||
|
key={alert.id}
|
||||||
{/* Main content */}
|
alert={alert}
|
||||||
<div className="flex flex-1 flex-col lg:ml-0">
|
onDismiss={() => deleteAlert(alert.id)}
|
||||||
{/* Header */}
|
onMarkRead={() => markAlertAsRead(alert.id)}
|
||||||
<Header onMenuClick={sidebar.toggle} title="Dashboard" />
|
/>
|
||||||
|
))}
|
||||||
{/* Main content area */}
|
|
||||||
<main className="flex-1 p-4 md:p-6 lg:p-8 pb-20 lg:pb-8">
|
|
||||||
<div className="mx-auto max-w-7xl space-y-6">
|
|
||||||
{/* Alertas destacadas */}
|
|
||||||
{topAlerts.length > 0 && (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{topAlerts.map((alert) => (
|
|
||||||
<AlertBanner
|
|
||||||
key={alert.id}
|
|
||||||
alert={alert}
|
|
||||||
onDismiss={() => deleteAlert(alert.id)}
|
|
||||||
onMarkRead={() => markAlertAsRead(alert.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Sección de resumen */}
|
|
||||||
<SummarySection />
|
|
||||||
|
|
||||||
{/* Acciones rápidas */}
|
|
||||||
<QuickActions
|
|
||||||
onAddDebt={handleAddDebt}
|
|
||||||
onAddCard={handleAddCard}
|
|
||||||
onAddPayment={handleAddPayment}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Actividad reciente */}
|
|
||||||
<RecentActivity limit={5} />
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile navigation */}
|
{/* Sección de resumen */}
|
||||||
<MobileNav unreadAlertsCount={unreadCount} />
|
<SummarySection />
|
||||||
|
|
||||||
|
{/* Acciones rápidas */}
|
||||||
|
<QuickActions
|
||||||
|
onAddDebt={handleAddDebt}
|
||||||
|
onAddCard={handleAddCard}
|
||||||
|
onAddPayment={handleAddPayment}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Actividad reciente */}
|
||||||
|
<RecentActivity limit={5} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Modales */}
|
{/* Modales */}
|
||||||
<AddDebtModal
|
<AddDebtModal
|
||||||
@@ -129,6 +89,6 @@ export default function Home() {
|
|||||||
isOpen={isAddPaymentModalOpen}
|
isOpen={isAddPaymentModalOpen}
|
||||||
onClose={() => setIsAddPaymentModalOpen(false)}
|
onClose={() => setIsAddPaymentModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</DashboardLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { createContext, useContext, useState, ReactNode } from "react";
|
import { createContext, useContext, useState, ReactNode } from "react";
|
||||||
|
import { DataSync } from "@/components/DataSync";
|
||||||
|
|
||||||
interface SidebarContextType {
|
interface SidebarContextType {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -27,6 +28,7 @@ export function Providers({ children }: { children: ReactNode }) {
|
|||||||
open: openSidebar,
|
open: openSidebar,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<DataSync />
|
||||||
{children}
|
{children}
|
||||||
</SidebarContext.Provider>
|
</SidebarContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatCurrency } from '@/lib/utils'
|
|||||||
import { Zap, Droplets, Flame, Wifi, TrendingUp, TrendingDown, Plus, History } from 'lucide-react'
|
import { Zap, Droplets, Flame, Wifi, TrendingUp, TrendingDown, Plus, History } from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { AddServiceModal } from '@/components/modals/AddServiceModal'
|
import { AddServiceModal } from '@/components/modals/AddServiceModal'
|
||||||
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
|
|
||||||
const SERVICES = [
|
const SERVICES = [
|
||||||
{ id: 'electricity', label: 'Luz (Electricidad)', icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-400/10' },
|
{ id: 'electricity', label: 'Luz (Electricidad)', icon: Zap, color: 'text-yellow-400', bg: 'bg-yellow-400/10' },
|
||||||
@@ -20,123 +21,125 @@ export default function ServicesPage() {
|
|||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<DashboardLayout title="Servicios">
|
||||||
|
<div className="space-y-6">
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<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>
|
<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>
|
</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 */}
|
{/* Service Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{SERVICES.map((service) => {
|
{SERVICES.map((service) => {
|
||||||
const Icon = service.icon
|
const Icon = service.icon
|
||||||
const prediction = predictNextBill(serviceBills, service.id as any)
|
const prediction = predictNextBill(serviceBills, service.id as any)
|
||||||
const trend = calculateTrend(serviceBills, service.id as any)
|
const trend = calculateTrend(serviceBills, service.id as any)
|
||||||
const lastBill = serviceBills
|
const lastBill = serviceBills
|
||||||
.filter(b => b.type === service.id)
|
.filter(b => b.type === service.id)
|
||||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={service.id} className="bg-slate-900 border border-slate-800 rounded-xl p-5 space-y-4">
|
<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="flex items-center justify-between">
|
||||||
<div className={cn("p-2 rounded-lg", service.bg)}>
|
<div className={cn("p-2 rounded-lg", service.bg)}>
|
||||||
<Icon className={cn("w-6 h-6", service.color)} />
|
<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>
|
||||||
)}
|
{trend !== 0 && (
|
||||||
</div>
|
<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} />}
|
||||||
<div>
|
{Math.abs(trend).toFixed(0)}%
|
||||||
<p className="text-slate-400 text-sm font-medium">{service.label}</p>
|
</div>
|
||||||
<div className="flex items-baseline gap-2">
|
)}
|
||||||
<h3 className="text-2xl font-bold text-white mt-1">
|
</div>
|
||||||
{formatCurrency(prediction || (lastBill?.amount ?? 0))}
|
|
||||||
</h3>
|
<div>
|
||||||
{prediction > 0 && <span className="text-xs text-slate-500 font-mono">(est.)</span>}
|
<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>
|
||||||
<p className="text-xs text-slate-500 mt-1">
|
|
||||||
{lastBill
|
|
||||||
? `Último: ${formatCurrency(lastBill.amount)}`
|
|
||||||
: 'Sin historial'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</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>
|
||||||
<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 (
|
{/* History List */}
|
||||||
<div key={bill.id} className="p-4 flex items-center justify-between hover:bg-slate-800/50 transition-colors">
|
<div className="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden">
|
||||||
<div className="flex items-center gap-4">
|
<div className="p-5 border-b border-slate-800 flex items-center gap-2">
|
||||||
<div className={cn("p-2 rounded-lg", service?.bg || 'bg-slate-800')}>
|
<History size={18} className="text-slate-400" />
|
||||||
<Icon className={cn("w-5 h-5", service?.color || '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>
|
||||||
<div>
|
<div className="text-right">
|
||||||
<p className="text-white font-medium capitalize">{service?.label || bill.type}</p>
|
<p className="text-white font-mono font-medium">{formatCurrency(bill.amount)}</p>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2">
|
<div className="flex flex-col items-end">
|
||||||
<p className="text-xs text-slate-500 capitalize">{new Date(bill.date).toLocaleDateString('es-AR', { dateStyle: 'long' })}</p>
|
<p className="text-xs text-slate-500 uppercase">{bill.period}</p>
|
||||||
{bill.usage && (
|
{bill.usage && bill.amount && (
|
||||||
<>
|
<p className="text-[10px] text-cyan-500/80 font-mono">
|
||||||
<span className="hidden sm:inline text-slate-700">•</span>
|
{formatCurrency(bill.amount / bill.usage)} / {bill.unit}
|
||||||
<p className="text-xs text-slate-400">
|
</p>
|
||||||
Consumo: <span className="text-slate-300 font-medium">{bill.usage} {bill.unit}</span>
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{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>
|
||||||
</div>
|
|
||||||
|
|
||||||
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
|
<AddServiceModal isOpen={isAddModalOpen} onClose={() => setIsAddModalOpen(false)} />
|
||||||
</div>
|
</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 { 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 { cn } from '@/lib/utils'
|
||||||
import { AIServiceConfig, AppSettings } from '@/lib/types'
|
import { AIServiceConfig, AppSettings } from '@/lib/types'
|
||||||
|
import { DashboardLayout } from '@/components/layout/DashboardLayout'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [loading, setLoading] = useState(true)
|
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>
|
if (loading) return <div className="p-8 text-center text-slate-400">Cargando configuración...</div>
|
||||||
|
|
||||||
return (
|
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 className="flex items-center justify-between">
|
||||||
<div>
|
<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>
|
<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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={testTelegram}
|
onClick={handleSave}
|
||||||
disabled={testingTelegram || !settings.telegram.botToken || !settings.telegram.chatId}
|
disabled={saving}
|
||||||
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"
|
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} />}
|
<Save size={18} />
|
||||||
Probar Envío
|
{saving ? 'Guardando...' : 'Guardar Cambios'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 bg-slate-900 border border-slate-800 rounded-xl">
|
{message && (
|
||||||
<div className="space-y-2">
|
<div className={cn(
|
||||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
"p-4 rounded-lg text-sm font-medium border flex items-center gap-2 animate-in fade-in slide-in-from-top-2",
|
||||||
<Key size={12} /> Bot Token
|
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"
|
||||||
</label>
|
)}>
|
||||||
<input
|
{message.type === 'success' ? <CheckCircle2 size={18} /> : <XCircle size={18} />}
|
||||||
type="text"
|
{message.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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* Telegram Configuration */}
|
||||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
<section className="space-y-4">
|
||||||
<MessageSquare size={12} /> Chat ID
|
<div className="flex items-center justify-between text-white border-b border-slate-800 pb-2">
|
||||||
</label>
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<Bot className="text-cyan-400" />
|
||||||
type="text"
|
<h2 className="text-lg font-semibold">Telegram Bot</h2>
|
||||||
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>
|
||||||
)}
|
<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 className="grid grid-cols-1 md:grid-cols-2 gap-6 p-6 bg-slate-900 border border-slate-800 rounded-xl">
|
||||||
<div key={provider.id} className="p-6 bg-slate-900 border border-slate-800 rounded-xl relative group">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-start mb-4">
|
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||||
<h3 className="text-sm font-semibold text-slate-300 bg-slate-950 inline-block px-3 py-1 rounded-md border border-slate-800">
|
<Key size={12} /> Bot Token
|
||||||
Provider #{index + 1}
|
</label>
|
||||||
</h3>
|
<input
|
||||||
<div className="flex gap-2">
|
type="text"
|
||||||
<button
|
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||||
onClick={() => testAI(provider)}
|
value={settings.telegram.botToken}
|
||||||
disabled={testingAI === provider.id || !provider.endpoint || !provider.token || !provider.model}
|
onChange={(e) => setSettings({ ...settings, telegram: { ...settings.telegram, botToken: e.target.value } })}
|
||||||
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")}
|
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"
|
||||||
title="Verificar conexión"
|
/>
|
||||||
>
|
<p className="text-[10px] text-slate-500">El token que te da @BotFather.</p>
|
||||||
{testingAI === provider.id ? <Loader2 size={12} className="animate-spin" /> : <LinkIcon size={12} />}
|
</div>
|
||||||
Test
|
|
||||||
</button>
|
<div className="space-y-2">
|
||||||
<button
|
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider flex items-center gap-2">
|
||||||
onClick={() => removeProvider(provider.id)}
|
<MessageSquare size={12} /> Chat ID
|
||||||
className="text-slate-500 hover:text-red-400 transition-colors p-1.5 hover:bg-red-500/10 rounded-lg"
|
</label>
|
||||||
title="Eliminar"
|
<input
|
||||||
>
|
type="text"
|
||||||
<Trash2 size={16} />
|
placeholder="123456789"
|
||||||
</button>
|
value={settings.telegram.chatId}
|
||||||
</div>
|
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>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
{settings.aiProviders.map((provider, index) => (
|
||||||
<div className="space-y-2">
|
<div key={provider.id} className="p-6 bg-slate-900 border border-slate-800 rounded-xl relative group">
|
||||||
<label className="text-xs font-medium text-slate-400 uppercase tracking-wider">Nombre</label>
|
<div className="flex justify-between items-start mb-4">
|
||||||
<input
|
<h3 className="text-sm font-semibold text-slate-300 bg-slate-950 inline-block px-3 py-1 rounded-md border border-slate-800">
|
||||||
type="text"
|
Provider #{index + 1}
|
||||||
placeholder="Ej: MiniMax, Z.ai"
|
</h3>
|
||||||
value={provider.name}
|
<div className="flex gap-2">
|
||||||
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
|
<button
|
||||||
onClick={() => detectModels(provider)}
|
onClick={() => testAI(provider)}
|
||||||
disabled={detectingModels === provider.id || !provider.endpoint || !provider.token}
|
disabled={testingAI === provider.id || !provider.endpoint || !provider.token || !provider.model}
|
||||||
className="text-[10px] flex items-center gap-1 text-cyan-400 hover:text-cyan-300 disabled:opacity-50"
|
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} />}
|
{testingAI === provider.id ? <Loader2 size={12} className="animate-spin" /> : <LinkIcon size={12} />}
|
||||||
Auto Detectar
|
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>
|
</button>
|
||||||
</div>
|
</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 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>
|
))}
|
||||||
))}
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
</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,
|
Bell,
|
||||||
Lightbulb,
|
Lightbulb,
|
||||||
Settings,
|
Settings,
|
||||||
|
TrendingUp,
|
||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@@ -22,6 +23,7 @@ interface SidebarProps {
|
|||||||
|
|
||||||
const navigationItems = [
|
const navigationItems = [
|
||||||
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
{ name: 'Dashboard', href: '/', icon: LayoutDashboard },
|
||||||
|
{ name: 'Ingresos', href: '/incomes', icon: TrendingUp },
|
||||||
{ name: 'Deudas', href: '/debts', icon: Wallet },
|
{ name: 'Deudas', href: '/debts', icon: Wallet },
|
||||||
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
|
{ name: 'Tarjetas', href: '/cards', icon: CreditCard },
|
||||||
{ name: 'Presupuesto', href: '/budget', icon: PiggyBank },
|
{ 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