diff --git a/src/app/page.tsx b/src/app/page.tsx index 92740c4..6df85b3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -90,7 +90,10 @@ export default function BudgetDashboard() { })); setTransactions(transactionsWithDates); setSetupCompleted(userData.setupCompleted || false); - setShowSetupWizard(!(userData.setupCompleted || false)); + + // Show setup wizard if setup is not completed OR if there are no accounts + const shouldShowWizard = !(userData.setupCompleted || false) || (userData.accounts || []).length === 0; + setShowSetupWizard(shouldShowWizard); } else { // New user - show setup wizard setShowSetupWizard(true); @@ -686,18 +689,22 @@ export default function BudgetDashboard() { /> )} - {/* Always visible components */} - ({ id: env.id, name: env.name }))} - accounts={accounts.map(acc => ({ id: acc.id, name: acc.name }))} - /> - + {/* Components visible after setup completion */} + {setupCompleted && ( + <> + ({ id: env.id, name: env.name }))} + accounts={accounts.map(acc => ({ id: acc.id, name: acc.name }))} + /> + + + )} ); } diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index 73935ac..8867d17 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -37,7 +37,14 @@ export default function AuthModal({ isOpen, onClose, onAuthSuccess }: AuthModalP onAuthSuccess(); onClose(); } catch (error: any) { - setError(error.message); + // Provide more user-friendly error messages + let errorMessage = error.message; + if (error.message === 'Invalid credentials') { + errorMessage = 'The email or password you entered is incorrect. Please try again.'; + } else if (error.message === 'User already exists') { + errorMessage = 'An account with this email already exists. Please try logging in instead.'; + } + setError(errorMessage); } finally { setLoading(false); } diff --git a/src/components/SetupWizard.tsx b/src/components/SetupWizard.tsx index 7ae97ce..a4224f2 100644 --- a/src/components/SetupWizard.tsx +++ b/src/components/SetupWizard.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { DataService } from '../lib/dataService'; interface Account { @@ -12,6 +12,7 @@ interface Account { accountNumber?: string; color: string; isActive: boolean; + defaultPaycheckAmount?: number; } interface Envelope { @@ -35,7 +36,6 @@ type WizardStep = 'welcome' | 'data-source' | 'accounts' | 'envelopes' | 'income export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardProps) { const [currentStep, setCurrentStep] = useState('welcome'); - const [dataSource, setDataSource] = useState<'import' | 'manual' | null>(null); const [accounts, setAccounts] = useState([]); const [envelopes, setEnvelopes] = useState([]); @@ -64,6 +64,32 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP const [incomeAllocations, setIncomeAllocations] = useState<{ [envelopeId: string]: { type: 'percentage' | 'fixed'; value: number } }>({}); const [defaultPaycheckAmounts, setDefaultPaycheckAmounts] = useState<{ [accountId: string]: number }>({}); + // Loading and error states + const [isSaving, setIsSaving] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + // Modal ESC key handling and click outside + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + if (showAddAccount || showAddEnvelope) { + setShowAddAccount(false); + setShowAddEnvelope(false); + } else { + onSkip(); // Close the entire wizard + } + } + }; + document.addEventListener('keydown', handleKeyDown); + return () => document.removeEventListener('keydown', handleKeyDown); + }, [showAddAccount, showAddEnvelope, onSkip]); + + const handleBackdropClick = (event: React.MouseEvent) => { + if (event.target === event.currentTarget) { + onSkip(); + } + }; + const accountTypes = [ { value: 'checking', label: 'Checking Account', icon: '🏦' }, { value: 'savings', label: 'Savings Account', icon: '💰' }, @@ -79,50 +105,76 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP ]; const handleAddAccount = () => { - if (newAccount.name && newAccount.type) { - const account: Account = { - id: `${newAccount.type}-${Date.now()}`, - name: newAccount.name, - type: newAccount.type as Account['type'], - balance: newAccount.balance || 0, - institution: newAccount.institution, - accountNumber: newAccount.accountNumber, - color: newAccount.color || 'bg-blue-500', - isActive: true, - }; - setAccounts(prev => [...prev, account]); - setNewAccount({ - type: 'checking', - balance: 0, - color: 'bg-blue-500', - isActive: true, - }); - setShowAddAccount(false); + if (!newAccount.name || !newAccount.type) return; + + // Validation: unique name + if (accounts.some(acc => acc.name.toLowerCase() === newAccount.name!.toLowerCase())) { + alert('Account name must be unique'); + return; } + + // Validation: balance >= 0 + const balance = newAccount.balance || 0; + if (balance < 0) { + alert('Balance cannot be negative'); + return; + } + + const account: Account = { + id: `${newAccount.type}-${Date.now()}`, + name: newAccount.name, + type: newAccount.type as Account['type'], + balance, + institution: newAccount.institution, + accountNumber: newAccount.accountNumber, + color: newAccount.color || 'bg-blue-500', + isActive: true, + }; + setAccounts(prev => [...prev, account]); + setNewAccount({ + type: 'checking', + balance: 0, + color: 'bg-blue-500', + isActive: true, + }); + setShowAddAccount(false); }; const handleAddEnvelope = () => { - if (newEnvelope.name && newEnvelope.accountId && newEnvelope.allocated !== undefined) { - const envelope: Envelope = { - id: `env-${Date.now()}`, - name: newEnvelope.name, - allocated: newEnvelope.allocated, - spent: 0, - color: newEnvelope.color || 'bg-green-500', - accountId: newEnvelope.accountId, - }; - setEnvelopes(prev => [...prev, envelope]); - setNewEnvelope({ - allocated: 0, - spent: 0, - color: 'bg-green-500', - }); - setShowAddEnvelope(false); + if (!newEnvelope.name || !newEnvelope.accountId || newEnvelope.allocated === undefined) return; + + // Validation: unique name + if (envelopes.some(env => env.name.toLowerCase() === newEnvelope.name!.toLowerCase())) { + alert('Envelope name must be unique'); + return; } + + const envelope: Envelope = { + id: `env-${Date.now()}`, + name: newEnvelope.name, + allocated: newEnvelope.allocated, + spent: 0, + color: newEnvelope.color || 'bg-green-500', + accountId: newEnvelope.accountId, + }; + setEnvelopes(prev => [...prev, envelope]); + setNewEnvelope({ + allocated: 0, + spent: 0, + color: 'bg-green-500', + }); + setShowAddEnvelope(false); }; const handleComplete = async () => { + if (!userId) { + setErrorMessage('No user ID provided'); + return; + } + setIsSaving(true); + setErrorMessage(null); try { + DataService.setUserId(userId); // Apply income allocation settings to envelopes const envelopesWithAllocations = envelopes.map(envelope => { const allocation = incomeAllocations[envelope.id]; @@ -148,7 +200,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP return account; }); - await DataService.saveUserData(userId, { + await DataService.saveUserData({ accounts: accountsWithDefaults, envelopes: envelopesWithAllocations, transactions: [], @@ -157,6 +209,9 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP onComplete(accountsWithDefaults, envelopesWithAllocations); } catch (error) { console.error('Error saving setup data:', error); + setErrorMessage('Failed to save setup data. Please try again.'); + } finally { + setIsSaving(false); } }; @@ -165,7 +220,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
💰

Welcome to Envelope Budgeting

-

Let's set up your budget to get started with secure, organized financial tracking.

+

Let's set up your budget to get started with secure, organized financial tracking.

@@ -189,13 +244,12 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP

How would you like to set up your data?

-

Choose how you'd like to get started with your budget.

+

Choose how you'd like to get started with your budget.

@@ -488,7 +541,8 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP } setCurrentStep('income-setup'); }} - className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700" + disabled={includeStandardEnvelopes && !standardEnvelopeAccountId} + className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed" > Next: Set Up Income → @@ -668,6 +722,27 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP ); })}
+ + {/* Total Percentage Warning */} + {(() => { + const totalPercentage = envelopes.reduce((sum, envelope) => { + const allocation = incomeAllocations[envelope.id]; + if (allocation?.type === 'percentage') { + return sum + (allocation.value || 0); + } + return sum; + }, 0); + if (totalPercentage > 100) { + return ( +
+

+ Warning: Total percentage allocation ({totalPercentage.toFixed(1)}%) exceeds 100%. Please adjust your allocations. +

+
+ ); + } + return null; + })()} {/* Navigation */} @@ -809,11 +884,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
+ {errorMessage && ( +
+

{errorMessage}

+
+ )} + {/* Progress Indicator */}
diff --git a/src/lib/authService.ts b/src/lib/authService.ts index 1901d0c..5411008 100644 --- a/src/lib/authService.ts +++ b/src/lib/authService.ts @@ -33,7 +33,10 @@ export class AuthService { return result.user; } catch (error) { - console.error('Sign up error:', error); + // Only log non-authentication errors to avoid console spam + if (!(error instanceof Error) || !error.message.includes('User already exists')) { + console.error('Sign up error:', error); + } throw error; } } @@ -50,12 +53,13 @@ export class AuthService { if (!response.ok) { const error = await response.json(); + // Don't log authentication errors to console as they may contain sensitive info throw new Error(error.error || 'Login failed'); } const result = await response.json(); this.currentUser = result.user; - + // Store in localStorage for persistence if (typeof window !== 'undefined') { localStorage.setItem('user', JSON.stringify(result.user)); @@ -63,7 +67,10 @@ export class AuthService { return result.user; } catch (error) { - console.error('Sign in error:', error); + // Only log non-authentication errors to avoid console spam + if (!(error instanceof Error) || !error.message.includes('Invalid credentials')) { + console.error('Sign in error:', error); + } throw error; } }