Calc App
This commit is contained in:
116
puffin-calculator-iframe/js/app.js
Normal file
116
puffin-calculator-iframe/js/app.js
Normal file
@@ -0,0 +1,116 @@
|
||||
// Main App Component
|
||||
function App() {
|
||||
const [showOffsetOrder, setShowOffsetOrder] = React.useState(false);
|
||||
const [offsetTons, setOffsetTons] = React.useState(0);
|
||||
const [monetaryAmount, setMonetaryAmount] = React.useState(undefined);
|
||||
const [calculatorType, setCalculatorType] = React.useState('trip');
|
||||
|
||||
React.useEffect(() => {
|
||||
// Log page view when component mounts
|
||||
analytics.pageView('calculator');
|
||||
|
||||
// Make the app available globally for iframe communication
|
||||
window.PuffinCalculator = {
|
||||
setTheme: (theme) => {
|
||||
document.body.className = theme === 'dark' ? 'dark bg-gray-900' : 'bg-gray-50';
|
||||
},
|
||||
resetCalculator: () => {
|
||||
setShowOffsetOrder(false);
|
||||
setOffsetTons(0);
|
||||
setMonetaryAmount(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle iframe resize
|
||||
const resizeObserver = new ResizeObserver(entries => {
|
||||
for (let entry of entries) {
|
||||
// Notify parent if using postMessage API
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'puffin-calculator-resize',
|
||||
height: entry.contentRect.height
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
resizeObserver.observe(rootElement);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (rootElement) {
|
||||
resizeObserver.unobserve(rootElement);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleOffsetClick = (tons, monetaryAmount) => {
|
||||
setOffsetTons(tons);
|
||||
setMonetaryAmount(monetaryAmount);
|
||||
setShowOffsetOrder(true);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
|
||||
// Track event
|
||||
analytics.event('calculator', 'offset_click', `${tons} tons`);
|
||||
};
|
||||
|
||||
// Render the appropriate component based on state
|
||||
const renderContent = () => {
|
||||
if (showOffsetOrder) {
|
||||
return React.createElement(OffsetOrder, {
|
||||
tons: offsetTons,
|
||||
monetaryAmount: monetaryAmount,
|
||||
onBack: () => {
|
||||
setShowOffsetOrder(false);
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
},
|
||||
calculatorType: calculatorType
|
||||
});
|
||||
}
|
||||
|
||||
return React.createElement(TripCalculator, {
|
||||
vesselData: sampleVessel,
|
||||
onOffsetClick: handleOffsetClick
|
||||
});
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'puffin-calculator min-h-[600px]'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'content',
|
||||
className: 'px-4 sm:px-6 flex justify-center'
|
||||
}, renderContent())
|
||||
]);
|
||||
}
|
||||
|
||||
// Initialize the application when the DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const rootElement = document.getElementById('root');
|
||||
|
||||
if (!rootElement) {
|
||||
console.error('Root element not found. Cannot mount Puffin Calculator.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for React and ReactDOM
|
||||
if (!window.React || !window.ReactDOM) {
|
||||
console.error('React or ReactDOM not loaded. Cannot mount Puffin Calculator.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize React app
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(React.createElement(App, {}));
|
||||
|
||||
// Notify when ready
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: 'puffin-calculator-ready'
|
||||
}, '*');
|
||||
}
|
||||
});
|
||||
1039
puffin-calculator-iframe/js/components.js
Normal file
1039
puffin-calculator-iframe/js/components.js
Normal file
File diff suppressed because it is too large
Load Diff
186
puffin-calculator-iframe/js/utils.js
Normal file
186
puffin-calculator-iframe/js/utils.js
Normal file
@@ -0,0 +1,186 @@
|
||||
// Constants for carbon calculations
|
||||
const EMISSION_FACTOR = 3.206; // tons of CO₂ per ton of fuel
|
||||
const FUEL_DENSITY = 0.85; // tons per m³ (or metric tons per kiloliter)
|
||||
const GALLONS_TO_LITERS = 3.78541; // 1 US gallon = 3.78541 liters
|
||||
const LITERS_TO_CUBIC_METERS = 0.001; // 1 liter = 0.001 m³
|
||||
|
||||
// Available currencies
|
||||
const currencies = {
|
||||
USD: { code: 'USD', symbol: '$', rate: 1 },
|
||||
EUR: { code: 'EUR', symbol: '€', rate: 0.92 },
|
||||
GBP: { code: 'GBP', symbol: '£', rate: 0.79 },
|
||||
CHF: { code: 'CHF', symbol: 'CHF', rate: 0.88 },
|
||||
};
|
||||
|
||||
// Format currency
|
||||
function formatCurrency(amountUSD, currency) {
|
||||
if (!currency) {
|
||||
currency = currencies.USD;
|
||||
}
|
||||
|
||||
// Convert USD amount to target currency
|
||||
const convertedAmount = amountUSD * currency.rate;
|
||||
|
||||
return `${currency.symbol}${convertedAmount.toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0,
|
||||
})}`;
|
||||
}
|
||||
|
||||
// Get currency by code
|
||||
function getCurrencyByCode(code) {
|
||||
return currencies[code] || currencies.USD;
|
||||
}
|
||||
|
||||
// Calculate trip carbon
|
||||
function calculateTripCarbon(vesselData, distance, speed, fuelRateLitersPerHour) {
|
||||
const tripHours = distance / speed;
|
||||
|
||||
// Calculate total fuel consumption in liters
|
||||
const fuelConsumptionLiters = fuelRateLitersPerHour * tripHours;
|
||||
|
||||
// Convert liters to tons for CO₂ calculation
|
||||
const fuelConsumptionTons = (fuelConsumptionLiters * LITERS_TO_CUBIC_METERS) * FUEL_DENSITY;
|
||||
|
||||
// Calculate CO₂ emissions
|
||||
const fuelRateTonsPerHour = (fuelRateLitersPerHour * LITERS_TO_CUBIC_METERS) * FUEL_DENSITY;
|
||||
const emissionsPerNM = (fuelRateTonsPerHour * EMISSION_FACTOR) / speed;
|
||||
const totalEmissions = emissionsPerNM * distance;
|
||||
|
||||
return {
|
||||
distance,
|
||||
duration: tripHours,
|
||||
fuelConsumption: Math.round(fuelConsumptionLiters),
|
||||
co2Emissions: Number(totalEmissions.toFixed(2))
|
||||
};
|
||||
}
|
||||
|
||||
// Calculate carbon from fuel
|
||||
function calculateCarbonFromFuel(fuelAmount, isGallons = false) {
|
||||
// Convert to liters if input is in gallons
|
||||
const liters = isGallons ? fuelAmount * GALLONS_TO_LITERS : fuelAmount;
|
||||
|
||||
// Convert liters to cubic meters (m³)
|
||||
const cubicMeters = liters * LITERS_TO_CUBIC_METERS;
|
||||
|
||||
// Convert volume to mass (tons)
|
||||
const fuelTons = cubicMeters * FUEL_DENSITY;
|
||||
|
||||
// Calculate CO₂ emissions
|
||||
const co2Emissions = fuelTons * EMISSION_FACTOR;
|
||||
|
||||
return Number(co2Emissions.toFixed(2));
|
||||
}
|
||||
|
||||
// Sample vessel data
|
||||
const sampleVessel = {
|
||||
imo: '1234567',
|
||||
vesselName: 'Sample Yacht',
|
||||
type: 'Yacht',
|
||||
length: 50,
|
||||
width: 9,
|
||||
estimatedEnginePower: 2250
|
||||
};
|
||||
|
||||
// Default portfolio for fallback
|
||||
const DEFAULT_PORTFOLIO = {
|
||||
id: 1,
|
||||
name: "Puffin Maritime Portfolio",
|
||||
description: "A curated selection of high-impact carbon removal projects focused on maritime sustainability.",
|
||||
projects: [
|
||||
{
|
||||
id: "dac-1",
|
||||
name: "Direct Air Capture",
|
||||
description: "Permanent carbon removal through direct air capture technology",
|
||||
shortDescription: "Direct air capture in Iceland",
|
||||
imageUrl: "https://images.unsplash.com/photo-1553547274-0df401ae03c9",
|
||||
pricePerTon: 200,
|
||||
location: "Iceland",
|
||||
type: "Direct Air Capture",
|
||||
verificationStandard: "Verified Carbon Standard",
|
||||
impactMetrics: {
|
||||
co2Reduced: 1000
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "ocean-1",
|
||||
name: "Ocean Carbon Removal",
|
||||
description: "Enhanced ocean carbon capture through marine permaculture",
|
||||
shortDescription: "Marine carbon capture",
|
||||
imageUrl: "https://images.unsplash.com/photo-1498623116890-37e912163d5d",
|
||||
pricePerTon: 200,
|
||||
location: "Global Oceans",
|
||||
type: "Ocean-Based",
|
||||
verificationStandard: "Gold Standard",
|
||||
impactMetrics: {
|
||||
co2Reduced: 5000
|
||||
}
|
||||
}
|
||||
],
|
||||
pricePerTon: 200,
|
||||
currency: 'USD'
|
||||
};
|
||||
|
||||
// Simplified analytics
|
||||
const analytics = {
|
||||
pageView: function(path) {
|
||||
console.log(`Analytics pageView: ${path}`);
|
||||
},
|
||||
event: function(category, action, label) {
|
||||
console.log(`Analytics event: ${category} - ${action} - ${label}`);
|
||||
},
|
||||
error: function(error, message) {
|
||||
console.error(`Analytics error: ${message}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create portfolio API call (with fallback to default)
|
||||
async function getPortfolios() {
|
||||
try {
|
||||
// In a real implementation, this would be an API call
|
||||
// For demo purposes, we'll just return the default portfolio after a small delay
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve([DEFAULT_PORTFOLIO]), 1000);
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch portfolios from API, using fallback');
|
||||
return [DEFAULT_PORTFOLIO];
|
||||
}
|
||||
}
|
||||
|
||||
// Create offset order API call (simulated)
|
||||
async function createOffsetOrder(portfolioId, tons, dryRun = false) {
|
||||
try {
|
||||
// Simulate API call
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve({
|
||||
id: `order-${Math.floor(Math.random() * 1000000)}`,
|
||||
amountCharged: tons * 200 * 100, // in cents
|
||||
currency: 'USD',
|
||||
tons: tons,
|
||||
portfolio: DEFAULT_PORTFOLIO,
|
||||
status: 'completed',
|
||||
createdAt: new Date().toISOString(),
|
||||
dryRun: dryRun
|
||||
});
|
||||
}, 1500);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to create offset order', error);
|
||||
throw new Error('Failed to create offset order. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
// Email validation
|
||||
function validateEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// Send form submission (simulated)
|
||||
async function sendFormSubmission(data) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve({ success: true }), 1000);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user