This commit is contained in:
Matt 2025-05-04 19:58:46 +02:00
parent 5cc1188c5e
commit 9053de0e51
10 changed files with 1693 additions and 0 deletions

View File

@ -0,0 +1,16 @@
FROM node:16-alpine as build
# Set working directory
WORKDIR /app
# Install a simple HTTP server for serving static content
RUN npm install -g http-server
# Copy application files
COPY . .
# Expose port
EXPOSE 8080
# Command to run the application
CMD ["http-server", ".", "-p", "8080", "--cors"]

View File

@ -0,0 +1,111 @@
# Puffin Carbon Offset Calculator (iFrame version)
A lightweight, embeddable version of the Puffin Carbon Offset Calculator for yachts and marine vessels.
## Features
- Carbon emissions calculator based on fuel consumption or trip distance
- Support for multiple currencies
- Responsive design that works in any iFrame
- Automatic height adjustment
- Dark/light theme support
- Docker-ready for easy deployment
## Getting Started
### Running Locally
1. Clone this repository
2. Open the project directory
3. Open `index.html` in your browser
### Using Docker
Build and run using Docker Compose:
```bash
docker-compose up -d
```
This will start a server on port 8080. You can then access the calculator at http://localhost:8080.
## Embedding in Your Website
Add the following code to your website:
```html
<iframe
src="http://your-server:8080"
id="puffin-calculator-iframe"
width="100%"
height="600"
frameborder="0"
allow="clipboard-write"
title="Puffin Carbon Offset Calculator"
></iframe>
<script>
// Optional: Handle iframe resizing for responsive behavior
window.addEventListener('message', function(event) {
if (event.data.type === 'puffin-calculator-resize') {
const iframe = document.getElementById('puffin-calculator-iframe');
if (iframe) {
// Add a bit of extra space to avoid scrollbars
iframe.style.height = (event.data.height + 50) + 'px';
}
}
});
</script>
```
See `iframe-example.html` for a complete integration example.
## Advanced Usage
The calculator exposes a JavaScript API that you can use to control it from the parent page:
```javascript
// Access the iframe's window object
const calculatorFrame = document.getElementById('puffin-calculator-iframe');
// Set the theme (once the iframe has loaded)
calculatorFrame.addEventListener('load', function() {
// Access the API methods
calculatorFrame.contentWindow.PuffinCalculator.setTheme('dark');
// Reset the calculator to initial state
calculatorFrame.contentWindow.PuffinCalculator.resetCalculator();
});
```
## Project Structure
```
puffin-calculator-iframe/
├── index.html # Main HTML file
├── iframe-example.html # Example of embedding the calculator
├── styles.css # Main stylesheet
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── README.md # This file
└── js/
├── app.js # Main application code
├── components.js # React components
└── utils.js # Utility functions
```
## Carbon Calculation Methods
This calculator offers three methods for calculating carbon emissions:
1. **Fuel-Based**: Calculate emissions based on the amount of fuel consumed (in liters or gallons)
2. **Distance-Based**: Calculate emissions based on distance traveled, vessel speed, and fuel consumption rate
3. **Custom Amount**: Enter a monetary amount directly for offsetting without a specific carbon calculation
## Integration with WordPress
To integrate with WordPress, see the companion PHP file `puffin-calculator-integrated.php` in the parent directory.
## License
Copyright (c) Puffin Offset. All rights reserved.

View File

@ -0,0 +1,12 @@
version: '3.8'
services:
puffin-calculator:
build: .
container_name: puffin-calculator-iframe
ports:
- "8080:8080"
restart: unless-stopped
volumes:
- ./:/app
- /app/node_modules

View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puffin Calculator iFrame Integration Example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
line-height: 1.6;
max-width: 900px;
margin: 0 auto;
padding: 20px;
color: #333;
}
h1 {
margin-bottom: 1rem;
color: #0f4c81;
}
.container {
margin-top: 2rem;
}
.example-code {
background-color: #f5f5f5;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
}
pre {
margin: 0;
}
code {
font-family: 'Courier New', Courier, monospace;
}
iframe {
width: 100%;
border: none;
min-height: 600px;
margin-top: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<h1>Puffin Calculator iFrame Integration</h1>
<p>This page demonstrates how to embed the Puffin Carbon Offset Calculator into your website using an iFrame.</p>
<div class="container">
<h2>Integration Code</h2>
<div class="example-code">
<pre><code>&lt;iframe
src="http://localhost:8080"
id="puffin-calculator-iframe"
width="100%"
height="600"
frameborder="0"
allow="clipboard-write"
title="Puffin Carbon Offset Calculator"
&gt;&lt;/iframe&gt;
&lt;script&gt;
// Optional: Handle iframe resizing for responsive behavior
window.addEventListener('message', function(event) {
if (event.data.type === 'puffin-calculator-resize') {
const iframe = document.getElementById('puffin-calculator-iframe');
if (iframe) {
// Add a bit of extra space to avoid scrollbars
iframe.style.height = (event.data.height + 50) + 'px';
}
}
});
&lt;/script&gt;</code></pre>
</div>
</div>
<div class="container">
<h2>Live Example</h2>
<p>Here's the calculator embedded in this page:</p>
<iframe
src="index.html"
id="puffin-calculator-iframe"
width="100%"
height="600"
frameborder="0"
allow="clipboard-write"
title="Puffin Carbon Offset Calculator"
></iframe>
<script>
// Handle iframe resizing
window.addEventListener('message', function(event) {
if (event.data.type === 'puffin-calculator-resize') {
const iframe = document.getElementById('puffin-calculator-iframe');
if (iframe) {
// Add a bit of extra space to avoid scrollbars
iframe.style.height = (event.data.height + 50) + 'px';
}
}
});
</script>
</div>
<div class="container">
<h2>Advanced Usage</h2>
<p>You can also communicate with the calculator through the iframe to set options or perform actions:</p>
<div class="example-code">
<pre><code>// Access the iframe's window object
const calculatorFrame = document.getElementById('puffin-calculator-iframe');
// Set the theme (once the iframe has loaded)
calculatorFrame.addEventListener('load', function() {
// Access the API methods
calculatorFrame.contentWindow.PuffinCalculator.setTheme('dark');
// Reset the calculator to initial state
calculatorFrame.contentWindow.PuffinCalculator.resetCalculator();
});</code></pre>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Puffin Carbon Offset Calculator</title>
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="styles.css">
</head>
<body class="bg-gray-50 min-h-screen">
<div id="root" class="container mx-auto p-4"></div>
<!-- React and ReactDOM -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<!-- Babel for JSX (for development, would be compiled in production) -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<!-- Axios for API requests -->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<!-- Application Scripts -->
<script src="js/utils.js"></script>
<script src="js/components.js"></script>
<script type="text/babel" src="js/app.js"></script>
</body>
</html>

View 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'
}, '*');
}
});

File diff suppressed because it is too large Load Diff

View 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);
});
}

View File

@ -0,0 +1,62 @@
/* Puffin Calculator Styles */
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
.puffin-calculator {
max-width: 100%;
margin: 0 auto;
}
/* Custom focus styles for better accessibility */
input:focus, select:focus, button:focus {
outline: 3px solid rgba(59, 130, 246, 0.5);
outline-offset: 2px;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f5f9;
}
::-webkit-scrollbar-thumb {
background: #94a3b8;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Safari input number button styling */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
opacity: 1;
height: 24px;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.puffin-calculator {
padding: 1rem;
}
}
/* Animation for loading state */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.animate-spin {
animation: spin 1s linear infinite;
}

Binary file not shown.