Calc App
This commit is contained in:
parent
5cc1188c5e
commit
9053de0e51
|
|
@ -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"]
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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><iframe
|
||||||
|
src="http://localhost: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></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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -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.
Loading…
Reference in New Issue