puffin-app/src/hooks/useCalculatorState.ts
Matt 5e642794d8
All checks were successful
Build and Push Docker Images / docker (push) Successful in 49s
Implement calculator state persistence and fix checkout navigation
Features:
- Add useCalculatorState hook with localStorage persistence and 1-hour expiry
- State persists through page reloads and Stripe checkout redirects
- Automatically clears state on successful payment (paid/fulfilled status)

Navigation fixes:
- Fix white page issues on checkout success/cancel pages
- Replace <a> links with button handlers for proper state-based routing
- Pass navigation handlers from App.tsx to checkout pages

State persistence integration:
- TripCalculator: Save/restore calculator inputs (fuel, distance, custom)
- MobileCalculator: Full state persistence for mobile app route
- OffsetOrder: Persist offset percentage and portfolio selection
- MobileOffsetOrder: Persist offset percentage for mobile flow

Carbon impact comparisons:
- Add varied carbon impact comparisons with random selection
- Display 3 comparisons in preview mode, 5 in success mode
- Categories: cars, flights, trees, streaming, homes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 13:55:51 +01:00

131 lines
3.1 KiB
TypeScript

/**
* useCalculatorState Hook
*
* Persists calculator state in localStorage to survive page reloads,
* browser navigation, and Stripe checkout redirects.
*
* State automatically expires after 1 hour to prevent stale data.
*/
import { useState, useEffect } from 'react';
export interface CalculatorState {
// Calculation type
calculationType: 'fuel' | 'distance' | 'custom';
// Distance-based inputs
distance: string;
speed: string;
fuelRate: string;
// Fuel-based inputs
fuelAmount: string;
fuelUnit: 'liters' | 'gallons';
// Custom amount input
customAmount: string;
// Offset order settings
offsetPercentage: number;
portfolioId?: number;
// Metadata
timestamp: number; // Used for auto-expiry
}
const STORAGE_KEY = 'puffin_calculator_state';
const EXPIRY_MS = 60 * 60 * 1000; // 1 hour (per user preference)
/**
* Custom hook for calculator state persistence
*
* @returns Object with state, saveState, and clearState functions
*/
export function useCalculatorState() {
const [state, setState] = useState<CalculatorState | null>(null);
const [isLoaded, setIsLoaded] = useState(false);
// Load from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
const parsed = JSON.parse(stored) as CalculatorState;
const age = Date.now() - parsed.timestamp;
// Check if state has expired
if (age < EXPIRY_MS) {
setState(parsed);
} else {
// State expired, clean up
localStorage.removeItem(STORAGE_KEY);
}
} catch (err) {
// Corrupted data, clean up
console.warn('Failed to parse calculator state from localStorage:', err);
localStorage.removeItem(STORAGE_KEY);
}
}
setIsLoaded(true);
}, []);
/**
* Save partial or complete state to localStorage
* Merges with existing state
*/
const saveState = (newState: Partial<CalculatorState>) => {
const updated: CalculatorState = {
...(state || {
calculationType: 'fuel',
distance: '',
speed: '12',
fuelRate: '100',
fuelAmount: '',
fuelUnit: 'liters',
customAmount: '',
offsetPercentage: 100,
}),
...newState,
timestamp: Date.now(), // Always update timestamp
};
setState(updated);
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
} catch (err) {
console.error('Failed to save calculator state to localStorage:', err);
}
};
/**
* Clear calculator state from memory and localStorage
* Call this after successful payment completion
*/
const clearState = () => {
setState(null);
try {
localStorage.removeItem(STORAGE_KEY);
} catch (err) {
console.error('Failed to clear calculator state from localStorage:', err);
}
};
return {
/** Current calculator state (null if none saved or expired) */
state,
/** Whether state has been loaded from localStorage */
isLoaded,
/** Save new or partial state */
saveState,
/** Clear all saved state */
clearState,
};
}