diff --git a/src/components/TripCalculator.tsx b/src/components/TripCalculator.tsx
index 48282b5..aae17c9 100644
--- a/src/components/TripCalculator.tsx
+++ b/src/components/TripCalculator.tsx
@@ -1,9 +1,10 @@
-import React, { useState, useCallback } from 'react';
+import React, { useState, useCallback, useEffect } from 'react';
import { Leaf, Droplet } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import type { VesselData, TripEstimate } from '../types';
import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCalculator';
import { FormSelect } from './forms/FormSelect';
+import { useCalculatorState } from '../hooks/useCalculatorState';
interface Props {
vesselData: VesselData;
@@ -11,11 +12,15 @@ interface Props {
}
export function TripCalculator({ vesselData, onOffsetClick }: Props) {
- const [calculationType, setCalculationType] = useState<'fuel' | 'distance' | 'custom'>('fuel');
- const [distance, setDistance] = useState
('');
- const [speed, setSpeed] = useState('12');
- const [fuelRate, setFuelRate] = useState('100');
- const [fuelAmount, setFuelAmount] = useState('');
+ const { state: savedState, saveState } = useCalculatorState();
+
+ const [calculationType, setCalculationType] = useState<'fuel' | 'distance' | 'custom'>(
+ savedState?.calculationType || 'fuel'
+ );
+ const [distance, setDistance] = useState(savedState?.distance || '');
+ const [speed, setSpeed] = useState(savedState?.speed || '12');
+ const [fuelRate, setFuelRate] = useState(savedState?.fuelRate || '100');
+ const [fuelAmount, setFuelAmount] = useState(savedState?.fuelAmount || '');
// Format number with commas
const formatNumber = (value: string): string => {
@@ -53,9 +58,11 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
setFuelRate(formatNumber(value));
}
};
- const [fuelUnit, setFuelUnit] = useState<'liters' | 'gallons'>('liters');
+ const [fuelUnit, setFuelUnit] = useState<'liters' | 'gallons'>(
+ savedState?.fuelUnit || 'liters'
+ );
const [tripEstimate, setTripEstimate] = useState(null);
- const [customAmount, setCustomAmount] = useState('');
+ const [customAmount, setCustomAmount] = useState(savedState?.customAmount || '');
const handleCalculate = useCallback((e: React.FormEvent) => {
e.preventDefault();
@@ -83,6 +90,20 @@ export function TripCalculator({ vesselData, onOffsetClick }: Props) {
}
}, []);
+ // Save state to localStorage whenever inputs change
+ useEffect(() => {
+ saveState({
+ calculationType,
+ distance,
+ speed,
+ fuelRate,
+ fuelAmount,
+ fuelUnit,
+ customAmount,
+ offsetPercentage: 100, // Default offset percentage for TripCalculator
+ });
+ }, [calculationType, distance, speed, fuelRate, fuelAmount, fuelUnit, customAmount, saveState]);
+
// Animation variants
const fadeIn = {
hidden: { opacity: 0 },
diff --git a/src/hooks/useCalculatorState.ts b/src/hooks/useCalculatorState.ts
new file mode 100644
index 0000000..e05e801
--- /dev/null
+++ b/src/hooks/useCalculatorState.ts
@@ -0,0 +1,130 @@
+/**
+ * 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(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) => {
+ 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,
+ };
+}
diff --git a/src/pages/CheckoutCancel.tsx b/src/pages/CheckoutCancel.tsx
index 7f39acc..d3ec06b 100644
--- a/src/pages/CheckoutCancel.tsx
+++ b/src/pages/CheckoutCancel.tsx
@@ -1,6 +1,14 @@
import { motion } from 'framer-motion';
-export default function CheckoutCancel() {
+interface CheckoutCancelProps {
+ onNavigateHome: () => void;
+ onNavigateCalculator: () => void;
+}
+
+export default function CheckoutCancel({
+ onNavigateHome,
+ onNavigateCalculator
+}: CheckoutCancelProps) {
return (
-
Try Again
-
-
+
+
{/* Help Section */}
diff --git a/src/pages/CheckoutSuccess.tsx b/src/pages/CheckoutSuccess.tsx
index 31b869d..ab3522e 100644
--- a/src/pages/CheckoutSuccess.tsx
+++ b/src/pages/CheckoutSuccess.tsx
@@ -2,11 +2,29 @@ import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { getOrderDetails } from '../api/checkoutClient';
import { OrderDetailsResponse } from '../types';
+import { CarbonImpactComparison } from '../components/CarbonImpactComparison';
+import { useCalculatorState } from '../hooks/useCalculatorState';
-export default function CheckoutSuccess() {
+interface CheckoutSuccessProps {
+ onNavigateHome: () => void;
+ onNavigateCalculator: () => void;
+}
+
+export default function CheckoutSuccess({
+ onNavigateHome,
+ onNavigateCalculator
+}: CheckoutSuccessProps) {
const [orderDetails, setOrderDetails] = useState
(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
+ const { clearState } = useCalculatorState();
+
+ // Clear calculator state on successful payment (per user preference)
+ useEffect(() => {
+ if (orderDetails && (orderDetails.order.status === 'paid' || orderDetails.order.status === 'fulfilled')) {
+ clearState();
+ }
+ }, [orderDetails, clearState]);
useEffect(() => {
const fetchOrderDetails = async () => {
@@ -60,12 +78,12 @@ export default function CheckoutSuccess() {
⚠️
Order Not Found
{error || 'Unable to retrieve order details'}
-
Return to Home
-
+
);
@@ -136,7 +154,7 @@ export default function CheckoutSuccess() {
{/* Processing Fee */}
- Processing Fee (3%)
+ Processing Fee (5%)
${processingFee.toFixed(2)}
@@ -182,18 +200,16 @@ export default function CheckoutSuccess() {
- {/* Impact Message */}
+ {/* Impact Comparisons */}
- 🌍 Making an Impact
-
- You've offset {order.tons} tons of CO₂ - equivalent to planting approximately{' '}
- {Math.round(order.tons * 50)} trees!
-
+
+
+
{/* Action Buttons */}
@@ -203,12 +219,18 @@ export default function CheckoutSuccess() {
transition={{ delay: 0.5 }}
className="flex flex-col sm:flex-row gap-4 justify-center"
>
-
Return to Home
-
+
+