This commit is contained in:
nmemmert committed 2025-11-10 12:57:48 -05:00
1 parent 959249ab22
commit caa6020ae8
4 files changed
+175 -64

No files matched your search

+20 -13
View File
@@ -90,7 +90,10 @@ export default function BudgetDashboard() {
})); }));
setTransactions(transactionsWithDates); setTransactions(transactionsWithDates);
setSetupCompleted(userData.setupCompleted || false); 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 { } else {
// New user - show setup wizard // New user - show setup wizard
setShowSetupWizard(true); setShowSetupWizard(true);
@@ -686,18 +689,22 @@ export default function BudgetDashboard() {
/> />
)} )}
{/* Always visible components */} {/* Components visible after setup completion */}
<DataInput {setupCompleted && (
onTransactionsAdded={handleTransactionsAdded} <>
envelopes={envelopes.map(env => ({ id: env.id, name: env.name }))} <DataInput
accounts={accounts.map(acc => ({ id: acc.id, name: acc.name }))} onTransactionsAdded={handleTransactionsAdded}
/> envelopes={envelopes.map(env => ({ id: env.id, name: env.name }))}
<GetPaid accounts={accounts.map(acc => ({ id: acc.id, name: acc.name }))}
accounts={accounts} />
envelopes={envelopes} <GetPaid
onIncomeAdded={handleTransactionsAdded} accounts={accounts}
onAccountUpdate={handleAccountUpdate} envelopes={envelopes}
/> onIncomeAdded={handleTransactionsAdded}
onAccountUpdate={handleAccountUpdate}
/>
</>
)}
</div> </div>
); );
} }
+8 -1
View File
@@ -37,7 +37,14 @@ export default function AuthModal({ isOpen, onClose, onAuthSuccess }: AuthModalP
onAuthSuccess(); onAuthSuccess();
onClose(); onClose();
} catch (error: any) { } 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 { } finally {
setLoading(false); setLoading(false);
} }
+138 -48
View File
@@ -1,6 +1,6 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { DataService } from '../lib/dataService'; import { DataService } from '../lib/dataService';
interface Account { interface Account {
@@ -12,6 +12,7 @@ interface Account {
accountNumber?: string; accountNumber?: string;
color: string; color: string;
isActive: boolean; isActive: boolean;
defaultPaycheckAmount?: number;
} }
interface Envelope { interface Envelope {
@@ -35,7 +36,6 @@ type WizardStep = 'welcome' | 'data-source' | 'accounts' | 'envelopes' | 'income
export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardProps) { export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardProps) {
const [currentStep, setCurrentStep] = useState<WizardStep>('welcome'); const [currentStep, setCurrentStep] = useState<WizardStep>('welcome');
const [dataSource, setDataSource] = useState<'import' | 'manual' | null>(null);
const [accounts, setAccounts] = useState<Account[]>([]); const [accounts, setAccounts] = useState<Account[]>([]);
const [envelopes, setEnvelopes] = useState<Envelope[]>([]); const [envelopes, setEnvelopes] = useState<Envelope[]>([]);
@@ -64,6 +64,32 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
const [incomeAllocations, setIncomeAllocations] = useState<{ [envelopeId: string]: { type: 'percentage' | 'fixed'; value: number } }>({}); const [incomeAllocations, setIncomeAllocations] = useState<{ [envelopeId: string]: { type: 'percentage' | 'fixed'; value: number } }>({});
const [defaultPaycheckAmounts, setDefaultPaycheckAmounts] = useState<{ [accountId: string]: number }>({}); const [defaultPaycheckAmounts, setDefaultPaycheckAmounts] = useState<{ [accountId: string]: number }>({});
// Loading and error states
const [isSaving, setIsSaving] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(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 = [ const accountTypes = [
{ value: 'checking', label: 'Checking Account', icon: '🏦' }, { value: 'checking', label: 'Checking Account', icon: '🏦' },
{ value: 'savings', label: 'Savings Account', icon: '💰' }, { value: 'savings', label: 'Savings Account', icon: '💰' },
@@ -79,50 +105,76 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
]; ];
const handleAddAccount = () => { const handleAddAccount = () => {
if (newAccount.name && newAccount.type) { if (!newAccount.name || !newAccount.type) return;
const account: Account = {
id: `${newAccount.type}-${Date.now()}`, // Validation: unique name
name: newAccount.name, if (accounts.some(acc => acc.name.toLowerCase() === newAccount.name!.toLowerCase())) {
type: newAccount.type as Account['type'], alert('Account name must be unique');
balance: newAccount.balance || 0, return;
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);
} }
// 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 = () => { const handleAddEnvelope = () => {
if (newEnvelope.name && newEnvelope.accountId && newEnvelope.allocated !== undefined) { if (!newEnvelope.name || !newEnvelope.accountId || newEnvelope.allocated === undefined) return;
const envelope: Envelope = {
id: `env-${Date.now()}`, // Validation: unique name
name: newEnvelope.name, if (envelopes.some(env => env.name.toLowerCase() === newEnvelope.name!.toLowerCase())) {
allocated: newEnvelope.allocated, alert('Envelope name must be unique');
spent: 0, return;
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 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 () => { const handleComplete = async () => {
if (!userId) {
setErrorMessage('No user ID provided');
return;
}
setIsSaving(true);
setErrorMessage(null);
try { try {
DataService.setUserId(userId);
// Apply income allocation settings to envelopes // Apply income allocation settings to envelopes
const envelopesWithAllocations = envelopes.map(envelope => { const envelopesWithAllocations = envelopes.map(envelope => {
const allocation = incomeAllocations[envelope.id]; const allocation = incomeAllocations[envelope.id];
@@ -148,7 +200,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
return account; return account;
}); });
await DataService.saveUserData(userId, { await DataService.saveUserData({
accounts: accountsWithDefaults, accounts: accountsWithDefaults,
envelopes: envelopesWithAllocations, envelopes: envelopesWithAllocations,
transactions: [], transactions: [],
@@ -157,6 +209,9 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
onComplete(accountsWithDefaults, envelopesWithAllocations); onComplete(accountsWithDefaults, envelopesWithAllocations);
} catch (error) { } catch (error) {
console.error('Error saving setup data:', 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
<div className="mb-8"> <div className="mb-8">
<div className="text-6xl mb-4">💰</div> <div className="text-6xl mb-4">💰</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to Envelope Budgeting</h1> <h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome to Envelope Budgeting</h1>
<p className="text-gray-600 mb-4">Let's set up your budget to get started with secure, organized financial tracking.</p> <p className="text-gray-600 mb-4">Let&apos;s set up your budget to get started with secure, organized financial tracking.</p>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
@@ -189,13 +244,12 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
<div className="text-center"> <div className="text-center">
<div className="mb-8"> <div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">How would you like to set up your data?</h2> <h2 className="text-2xl font-bold text-gray-900 mb-2">How would you like to set up your data?</h2>
<p className="text-gray-600">Choose how you'd like to get started with your budget.</p> <p className="text-gray-600">Choose how you&apos;d like to get started with your budget.</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<button <button
onClick={() => { onClick={() => {
setDataSource('import');
setCurrentStep('accounts'); setCurrentStep('accounts');
}} }}
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors" className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
@@ -207,7 +261,6 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
<button <button
onClick={() => { onClick={() => {
setDataSource('manual');
setCurrentStep('accounts'); setCurrentStep('accounts');
}} }}
className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors" className="p-6 border-2 border-gray-200 rounded-lg hover:border-blue-500 transition-colors"
@@ -436,7 +489,7 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
<option key={account.id} value={account.id}>{account.name}</option> <option key={account.id} value={account.id}>{account.name}</option>
))} ))}
</select> </select>
<p className="text-sm text-gray-500 mt-2">We'll create a set of common envelopes (Rent, Groceries, Utilities, Savings, Transport, Entertainment, Emergency). You can edit amounts later.</p> <p className="text-sm text-gray-500 mt-2">We&apos;ll create a set of common envelopes (Rent, Groceries, Utilities, Savings, Transport, Entertainment, Emergency). You can edit amounts later.</p>
</div> </div>
</div> </div>
@@ -488,7 +541,8 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
} }
setCurrentStep('income-setup'); 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 → Next: Set Up Income →
</button> </button>
@@ -668,6 +722,27 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
); );
})} })}
</div> </div>
{/* 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 (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-700">
Warning: Total percentage allocation ({totalPercentage.toFixed(1)}%) exceeds 100%. Please adjust your allocations.
</p>
</div>
);
}
return null;
})()}
</div> </div>
{/* Navigation */} {/* Navigation */}
@@ -809,11 +884,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{errorMessage && (
<div className="p-3 bg-red-50 border border-red-200 rounded-md">
<p className="text-sm text-red-700">{errorMessage}</p>
</div>
)}
<button <button
onClick={handleComplete} onClick={handleComplete}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors" disabled={isSaving}
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
Start Budgeting! {isSaving ? 'Saving...' : 'Start Budgeting!'}
</button> </button>
<button <button
onClick={() => setCurrentStep('envelopes')} onClick={() => setCurrentStep('envelopes')}
@@ -826,8 +907,17 @@ export default function SetupWizard({ onComplete, onSkip, userId }: SetupWizardP
); );
return ( return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4" onClick={handleBackdropClick}>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-2xl w-full"> <div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-8 max-w-2xl w-full max-h-[90vh] overflow-y-auto" onClick={(e) => e.stopPropagation()}>
{/* Close button */}
<button
onClick={onSkip}
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
aria-label="Close setup wizard"
>
✕
</button>
{/* Progress Indicator */} {/* Progress Indicator */}
<div className="mb-8"> <div className="mb-8">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
+9 -2
View File
@@ -33,7 +33,10 @@ export class AuthService {
return result.user; return result.user;
} catch (error) { } 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; throw error;
} }
} }
@@ -50,6 +53,7 @@ export class AuthService {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); 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'); throw new Error(error.error || 'Login failed');
} }
@@ -63,7 +67,10 @@ export class AuthService {
return result.user; return result.user;
} catch (error) { } 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; throw error;
} }
} }