Building canadawelco.me
The Canada Living Cost Calculator helps individuals and families estimate their living expenses across Canadian provinces. This post explores the technical implementation behind this project.
Technical Stack
The application is built using Next.js 13.5.1 with TypeScript, powered by React 18.2.0. For styling, we use Tailwind CSS and shadcn/ui components, ensuring a consistent and responsive interface.
Core Implementation
The heart of the application is the calculator component, which handles all user inputs and calculations. Here’s the main implementation:
"use client";
import { useState, useEffect } from 'react';
import { IncomeSection } from './calculator/income-section';
import { ExpensesSection } from './calculator/expenses-section';
import { AffordabilitySection } from './calculator/affordability-section';
import { ProvinceSelector } from './calculator/province-selector';
import { FamilySettings } from './calculator/family-settings';
import { getMonthlyExpenses, getPredefinedSalaries } from '@/lib/data';
import { calculateMonthlyTakeHome, calculateAffordability } from '@/lib/calculations';
import { calculateFamilyExpenses } from '@/lib/family-calculations';
interface CustomExpense {
id: string;
name: string;
amount: number;
}
export function Calculator() {
const [salary, setSalary] = useState<number>(0);
const [partnerIncome, setPartnerIncome] = useState<number>(0);
const [hasPartner, setHasPartner] = useState<boolean>(false);
const [childrenCount, setChildrenCount] = useState<number>(0);
const [selectedProvince, setSelectedProvince] = useState('ON');
const [selectedExpenses, setSelectedExpenses] = useState<Set<string>>(
new Set(getMonthlyExpenses('ON').filter(exp => exp.required).map(exp => exp.id))
);
const [customExpenses, setCustomExpenses] = useState<CustomExpense[]>([]);
const [customAmounts, setCustomAmounts] = useState<Record<string, number>>({});
useEffect(() => {
setSelectedExpenses(new Set(getMonthlyExpenses(selectedProvince).filter(exp => exp.required).map(exp => exp.id)));
}, [selectedProvince]);
const handlePredefinedSalary = (value: string) => {
const [profession, level] = value.split('-');
const predefinedSalaries = getPredefinedSalaries(selectedProvince);
setSalary(predefinedSalaries[profession][level as 'junior' | 'mid' | 'senior']);
};
const handleExpenseToggle = (expenseId: string) => {
const newSelected = new Set(selectedExpenses);
if (newSelected.has(expenseId)) {
if (!getMonthlyExpenses(selectedProvince).find(exp => exp.id === expenseId)?.required) {
newSelected.delete(expenseId);
}
} else {
newSelected.add(expenseId);
}
setSelectedExpenses(newSelected);
};
// Calculate base expenses including custom amounts
const baseExpenses = Array.from(selectedExpenses).reduce((total, expId) => {
const expense = getMonthlyExpenses(selectedProvince).find(exp => exp.id === expId);
if (expense) {
total[expId] = customAmounts[expId] ?? expense.averageCost;
}
return total;
}, {} as Record<string, number>);
// Calculate adjusted expenses based on family size
const adjustedExpenses = calculateFamilyExpenses(baseExpenses, childrenCount, selectedProvince, hasPartner);
// Calculate total monthly expenses including custom expenses
const totalMonthlyExpenses = Object.values(adjustedExpenses).reduce((sum, cost) => sum + (cost || 0), 0) +
customExpenses.reduce((total, exp) => total + exp.amount, 0);
// Calculate monthly take-home pay
const monthlyTakeHome = (salary > 0 ? calculateMonthlyTakeHome(salary, selectedProvince) : 0) +
(partnerIncome > 0 ? calculateMonthlyTakeHome(partnerIncome, selectedProvince) : 0);
// Calculate affordability only if there's income
const affordability = monthlyTakeHome > 0 ?
calculateAffordability(monthlyTakeHome, totalMonthlyExpenses || 0) :
{ canAfford: false, savingsPerMonth: 0, affordabilityRatio: 0 };
return (
<div className="space-y-8">
<ProvinceSelector
selectedProvince={selectedProvince}
onProvinceChange={setSelectedProvince}
/>
<FamilySettings
spouseIncome={partnerIncome}
onSpouseIncomeChange={setPartnerIncome}
childrenCount={childrenCount}
onChildrenCountChange={setChildrenCount}
hasPartner={hasPartner}
onPartnerStatusChange={setHasPartner}
/>
<div className="grid gap-8 md:grid-cols-2">
<IncomeSection
salary={salary}
monthlyTakeHome={monthlyTakeHome}
onSalaryChange={setSalary}
onPredefinedSalarySelect={handlePredefinedSalary}
selectedProvince={selectedProvince}
/>
<ExpensesSection
selectedProvince={selectedProvince}
selectedExpenses={selectedExpenses}
onExpenseToggle={handleExpenseToggle}
totalMonthlyExpenses={totalMonthlyExpenses}
customExpenses={customExpenses}
onAddCustomExpense={(expense) => {
setCustomExpenses(prev => [...prev, {
id: `custom-${Date.now()}`,
name: expense.name,
amount: expense.amount
}]);
}}
onRemoveCustomExpense={(id) => {
setCustomExpenses(prev => prev.filter(exp => exp.id !== id));
}}
customAmounts={customAmounts}
onCustomAmountChange={(expenseId, amount) => {
setCustomAmounts(prev => ({
...prev,
[expenseId]: amount
}));
}}
/>
</div>
{(salary > 0 || partnerIncome > 0) && (
<AffordabilitySection affordability={affordability} />
)}
</div>
);
}
Tax and Affordability Calculations
The application includes detailed tax calculations for each province, taking into account federal and provincial tax brackets, CPP, and EI deductions. Here’s the implementation:
import { provinces, type Province } from './province-data';
function calculateProvincialTax(salary: number, province: Province): number {
let tax = 0;
let remainingSalary = salary;
for (let i = 0; i < province.taxBrackets.length; i++) {
const currentBracket = province.taxBrackets[i];
const previousBracket = i > 0 ? province.taxBrackets[i - 1] : { threshold: 0, rate: 0 };
const bracketIncome = Math.min(
Math.max(0, remainingSalary),
currentBracket.threshold - previousBracket.threshold
);
tax += bracketIncome * currentBracket.rate;
remainingSalary -= bracketIncome;
if (remainingSalary <= 0) break;
}
// Provincial basic personal amount credit
const basicPersonalCredit = province.basicPersonalAmount * province.taxBrackets[0].rate;
return Math.max(0, tax - basicPersonalCredit);
}
export function calculateTakeHomePay(salary: number, provinceId: string): number {
// Federal tax brackets (2024)
let federalTax = 0;
if (salary <= 55867) {
federalTax = salary * 0.15;
} else if (salary <= 111733) {
federalTax = (55867 * 0.15) + ((salary - 55867) * 0.205);
} else if (salary <= 173205) {
federalTax = (55867 * 0.15) + ((111733 - 55867) * 0.205) + ((salary - 111733) * 0.26);
} else {
federalTax = (55867 * 0.15) + ((111733 - 55867) * 0.205) + ((173205 - 111733) * 0.26) + ((salary - 173205) * 0.29);
}
// Federal basic personal amount credit
const federalBasicPersonalCredit = 15000 * 0.15;
federalTax = Math.max(0, federalTax - federalBasicPersonalCredit);
// Provincial tax
const province = provinces.find(p => p.id === provinceId)!;
const provincialTax = calculateProvincialTax(salary, province);
// CPP and EI deductions (2024)
const cppRate = 0.0595;
const cppMaxContribution = 3867.50;
const cpp = Math.min(salary * cppRate, cppMaxContribution);
const eiRate = 0.0163;
const eiMaxContribution = 1049.12;
const ei = Math.min(salary * eiRate, eiMaxContribution);
const totalDeductions = federalTax + provincialTax + cpp + ei;
return salary - totalDeductions;
}
export function calculateMonthlyTakeHome(salary: number, provinceId: string): number {
return calculateTakeHomePay(salary, provinceId) / 12;
}
export function calculateAffordability(monthlyTakeHome: number, expenses: number): {
canAfford: boolean;
savingsPerMonth: number;
affordabilityRatio: number;
} {
const savingsPerMonth = monthlyTakeHome - expenses;
const affordabilityRatio = expenses / monthlyTakeHome;
return {
canAfford: affordabilityRatio <= 0.7,
savingsPerMonth,
affordabilityRatio
};
}
Key Features
- State Management: Uses React’s useState and useEffect hooks for efficient state handling
- Type Safety: Full TypeScript implementation with strict type checking
- Component Architecture: Modular design with separate components for income, expenses, and affordability
- Real-time Calculations: Instant updates as users modify inputs
- Tax Calculations: Accurate federal and provincial tax calculations with CPP and EI deductions
- Affordability Analysis: Smart affordability ratio calculation with savings projections
Performance
The application is optimized for performance with:
- Static site generation
- Client-side calculations
- Minimal bundle size (~200KB gzipped)
- Fast time-to-interactive (< 2s)
Future Plans
We’re planning to add:
- Server-side calculations
- Real-time data updates
- PWA support
- Advanced caching strategies
The project demonstrates how modern web technologies can create performant, maintainable applications while providing real value to users.