Made Calculator into Wordpress Plugin
This commit is contained in:
parent
633032ee32
commit
5cc1188c5e
|
|
@ -0,0 +1,996 @@
|
|||
<?php
|
||||
/**
|
||||
* Plugin Name: Puffin Offset Calculator
|
||||
* Description: Carbon Offsetting Calculator for Yachts
|
||||
* Version: 1.0.0
|
||||
* Author: Puffin Offset
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
class Puffin_Calculator_Integrated {
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
*/
|
||||
public function __construct() {
|
||||
// Register shortcode
|
||||
add_shortcode('puffin_calculator', array($this, 'render_calculator'));
|
||||
|
||||
// Register widget
|
||||
add_action('widgets_init', array($this, 'register_calculator_widget'));
|
||||
|
||||
// Enqueue required scripts
|
||||
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the calculator via shortcode
|
||||
*
|
||||
* @param array $atts Shortcode attributes
|
||||
* @return string HTML output
|
||||
*/
|
||||
public function render_calculator($atts) {
|
||||
// Default attributes
|
||||
$attributes = shortcode_atts(array(
|
||||
'width' => '100%',
|
||||
'height' => 'auto',
|
||||
'theme' => 'light',
|
||||
), $atts);
|
||||
|
||||
// Generate unique ID for this instance
|
||||
$calculator_id = 'puffin-calculator-' . uniqid();
|
||||
|
||||
// Start output buffer
|
||||
ob_start();
|
||||
?>
|
||||
<div class="puffin-calculator-wrapper" style="width: <?php echo esc_attr($attributes['width']); ?>;">
|
||||
<div id="<?php echo esc_attr($calculator_id); ?>" class="puffin-calculator" data-theme="<?php echo esc_attr($attributes['theme']); ?>"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize the calculator when the DOM is fully loaded
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Find this specific calculator instance
|
||||
const calculatorElement = document.getElementById('<?php echo esc_attr($calculator_id); ?>');
|
||||
if (calculatorElement) {
|
||||
// Initialize the React app for this instance
|
||||
initPuffinCalculator(calculatorElement);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php
|
||||
|
||||
// Return the output buffer contents
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the widget
|
||||
*/
|
||||
public function register_calculator_widget() {
|
||||
register_widget('Puffin_Calculator_Widget_Class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue needed scripts and styles
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
// Enqueue React and ReactDOM from CDN
|
||||
wp_enqueue_script('react', 'https://unpkg.com/react@18/umd/react.production.min.js', array(), '18.0.0', true);
|
||||
wp_enqueue_script('react-dom', 'https://unpkg.com/react-dom@18/umd/react-dom.production.min.js', array('react'), '18.0.0', true);
|
||||
|
||||
// Enqueue Tailwind CSS
|
||||
wp_enqueue_style('tailwindcss', 'https://cdn.tailwindcss.com', array(), '3.0.0');
|
||||
|
||||
// Inline styles for the calculator
|
||||
wp_add_inline_style('tailwindcss', $this->get_calculator_styles());
|
||||
|
||||
// Register and enqueue the calculator script
|
||||
wp_register_script('puffin-calculator', '', array('react', 'react-dom'), '1.0.0', true);
|
||||
wp_enqueue_script('puffin-calculator');
|
||||
|
||||
// Add the calculator script as inline JS
|
||||
wp_add_inline_script('puffin-calculator', $this->get_calculator_script());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline calculator styles
|
||||
*/
|
||||
private function get_calculator_styles() {
|
||||
return "
|
||||
.puffin-calculator-wrapper {
|
||||
margin: 20px 0;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.puffin-calculator {
|
||||
border: none;
|
||||
width: 100%;
|
||||
min-height: 600px;
|
||||
background-color: #f9fafb;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.puffin-calculator-wrapper {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.puffin-calculator {
|
||||
min-height: 800px;
|
||||
}
|
||||
}
|
||||
";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline calculator script
|
||||
*/
|
||||
private function get_calculator_script() {
|
||||
return "
|
||||
// Initialize the Puffin Calculator
|
||||
function initPuffinCalculator(element) {
|
||||
'use strict';
|
||||
|
||||
// React and ReactDOM are loaded from CDN
|
||||
const React = window.React;
|
||||
const ReactDOM = window.ReactDOM;
|
||||
const { useState, useEffect, useCallback } = React;
|
||||
|
||||
// ==================== TYPE DEFINITIONS ====================
|
||||
|
||||
// VesselData interface
|
||||
// imo: string;
|
||||
// vesselName: string;
|
||||
// type: string;
|
||||
// length: number;
|
||||
// width: number;
|
||||
// estimatedEnginePower: number;
|
||||
|
||||
// TripEstimate interface
|
||||
// distance: number; // nautical miles
|
||||
// duration: number; // hours
|
||||
// fuelConsumption: number; // liters
|
||||
// co2Emissions: number; // tons
|
||||
|
||||
// Currency interface
|
||||
// code: string;
|
||||
// symbol: string;
|
||||
// rate: number; // Exchange rate relative to USD
|
||||
|
||||
// ==================== UTILITIES ====================
|
||||
|
||||
// Mock vessel data
|
||||
const sampleVessel = {
|
||||
imo: '1234567',
|
||||
vesselName: 'Sample Yacht',
|
||||
type: 'Yacht',
|
||||
length: 50,
|
||||
width: 9,
|
||||
estimatedEnginePower: 2250
|
||||
};
|
||||
|
||||
// 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,
|
||||
})}`;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// Analytics object (simplified)
|
||||
const analytics = {
|
||||
pageView: (path) => {
|
||||
// Simplified analytics - would integrate with WordPress analytics in a real implementation
|
||||
console.log(`Page view: ${path}`);
|
||||
},
|
||||
event: (category, action, label) => {
|
||||
console.log(`Event: ${category} - ${action} - ${label}`);
|
||||
}
|
||||
};
|
||||
|
||||
// ==================== COMPONENTS ====================
|
||||
|
||||
// CurrencySelect Component
|
||||
function CurrencySelect({ value, onChange }) {
|
||||
return React.createElement('select', {
|
||||
value: value,
|
||||
onChange: (e) => onChange(e.target.value),
|
||||
className: 'block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50 p-2'
|
||||
},
|
||||
Object.entries(currencies).map(([code, currency]) => (
|
||||
React.createElement('option', {
|
||||
key: code,
|
||||
value: code
|
||||
}, `${currency.symbol} ${code}`)
|
||||
)));
|
||||
}
|
||||
|
||||
// TripCalculator Component
|
||||
function TripCalculator({ vesselData, onOffsetClick }) {
|
||||
const [calculationType, setCalculationType] = useState('fuel');
|
||||
const [distance, setDistance] = useState('');
|
||||
const [speed, setSpeed] = useState('12');
|
||||
const [fuelRate, setFuelRate] = useState('100');
|
||||
const [fuelAmount, setFuelAmount] = useState('');
|
||||
const [fuelUnit, setFuelUnit] = useState('liters');
|
||||
const [tripEstimate, setTripEstimate] = useState(null);
|
||||
const [currency, setCurrency] = useState('USD');
|
||||
const [offsetPercentage, setOffsetPercentage] = useState(100);
|
||||
const [customPercentage, setCustomPercentage] = useState('');
|
||||
const [customAmount, setCustomAmount] = useState('');
|
||||
|
||||
const handleCalculate = useCallback((e) => {
|
||||
e.preventDefault();
|
||||
if (calculationType === 'distance') {
|
||||
const estimate = calculateTripCarbon(
|
||||
vesselData,
|
||||
Number(distance),
|
||||
Number(speed),
|
||||
Number(fuelRate)
|
||||
);
|
||||
setTripEstimate(estimate);
|
||||
} else if (calculationType === 'fuel') {
|
||||
const co2Emissions = calculateCarbonFromFuel(Number(fuelAmount), fuelUnit === 'gallons');
|
||||
setTripEstimate({
|
||||
distance: 0,
|
||||
duration: 0,
|
||||
fuelConsumption: Number(fuelAmount),
|
||||
co2Emissions
|
||||
});
|
||||
}
|
||||
}, [calculationType, distance, speed, fuelRate, fuelAmount, fuelUnit, vesselData]);
|
||||
|
||||
const handleCustomPercentageChange = useCallback((e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || (Number(value) >= 0 && Number(value) <= 100)) {
|
||||
setCustomPercentage(value);
|
||||
if (value !== '') {
|
||||
setOffsetPercentage(Number(value));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePresetPercentage = useCallback((percentage) => {
|
||||
setOffsetPercentage(percentage);
|
||||
setCustomPercentage('');
|
||||
}, []);
|
||||
|
||||
const calculateOffsetAmount = useCallback((emissions, percentage) => {
|
||||
return (emissions * percentage) / 100;
|
||||
}, []);
|
||||
|
||||
const handleCustomAmountChange = useCallback((e) => {
|
||||
const value = e.target.value;
|
||||
if (value === '' || Number(value) >= 0) {
|
||||
setCustomAmount(value);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full mt-8'
|
||||
}, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'flex items-center justify-between mb-6'
|
||||
}, [
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl font-bold text-gray-800'
|
||||
}, 'Carbon Offset Calculator'),
|
||||
// Route icon would be here
|
||||
]),
|
||||
|
||||
// Calculation Method
|
||||
React.createElement('div', {
|
||||
key: 'calc-method',
|
||||
className: 'mb-6'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
key: 'method-label',
|
||||
className: 'block text-sm font-medium text-gray-700 mb-2'
|
||||
}, 'Calculation Method'),
|
||||
React.createElement('div', {
|
||||
key: 'method-buttons',
|
||||
className: 'flex flex-wrap gap-3'
|
||||
}, [
|
||||
React.createElement('button', {
|
||||
key: 'fuel-btn',
|
||||
type: 'button',
|
||||
onClick: () => setCalculationType('fuel'),
|
||||
className: `px-4 py-2 rounded-lg transition-colors ${
|
||||
calculationType === 'fuel'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`
|
||||
}, 'Fuel Based'),
|
||||
React.createElement('button', {
|
||||
key: 'distance-btn',
|
||||
type: 'button',
|
||||
onClick: () => setCalculationType('distance'),
|
||||
className: `px-4 py-2 rounded-lg transition-colors ${
|
||||
calculationType === 'distance'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`
|
||||
}, 'Distance Based'),
|
||||
React.createElement('button', {
|
||||
key: 'custom-btn',
|
||||
type: 'button',
|
||||
onClick: () => setCalculationType('custom'),
|
||||
className: `px-4 py-2 rounded-lg transition-colors ${
|
||||
calculationType === 'custom'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`
|
||||
}, 'Custom Amount')
|
||||
])
|
||||
]),
|
||||
|
||||
// Custom Amount Form
|
||||
calculationType === 'custom' ? React.createElement('div', {
|
||||
key: 'custom-form',
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
// Currency Select
|
||||
React.createElement('div', {
|
||||
key: 'currency-select',
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700 mb-2'
|
||||
}, 'Select Currency'),
|
||||
React.createElement('div', {
|
||||
className: 'max-w-xs'
|
||||
}, [
|
||||
React.createElement(CurrencySelect, {
|
||||
value: currency,
|
||||
onChange: setCurrency
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Amount Input
|
||||
React.createElement('div', {
|
||||
key: 'amount-input',
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700 mb-2'
|
||||
}, 'Enter Amount to Offset'),
|
||||
React.createElement('div', {
|
||||
className: 'relative rounded-md shadow-sm'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
className: 'pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
className: 'text-gray-500 sm:text-sm'
|
||||
}, currencies[currency].symbol)
|
||||
]),
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
value: customAmount,
|
||||
onChange: handleCustomAmountChange,
|
||||
placeholder: 'Enter amount',
|
||||
min: '0',
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 pl-7 pr-12 focus:border-blue-500 focus:ring-blue-500 sm:text-sm p-2',
|
||||
required: true
|
||||
}),
|
||||
React.createElement('div', {
|
||||
className: 'pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
className: 'text-gray-500 sm:text-sm'
|
||||
}, currency)
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Offset Button
|
||||
customAmount && Number(customAmount) > 0 ? React.createElement('button', {
|
||||
key: 'offset-btn',
|
||||
onClick: () => onOffsetClick(0, Number(customAmount)),
|
||||
className: 'w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors mt-6'
|
||||
}, 'Offset Your Impact') : null
|
||||
]) :
|
||||
// Trip Calculator Form
|
||||
React.createElement('form', {
|
||||
key: 'trip-form',
|
||||
onSubmit: handleCalculate,
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
// Fuel Based Form
|
||||
calculationType === 'fuel' ? React.createElement('div', {
|
||||
key: 'fuel-form'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700'
|
||||
}, 'Fuel Consumption'),
|
||||
React.createElement('div', {
|
||||
className: 'flex space-x-4'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
className: 'flex-1'
|
||||
}, [
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
min: '1',
|
||||
value: fuelAmount,
|
||||
onChange: (e) => setFuelAmount(e.target.value),
|
||||
placeholder: 'Enter amount',
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 p-2',
|
||||
required: true
|
||||
})
|
||||
]),
|
||||
React.createElement('div', {}, [
|
||||
React.createElement('select', {
|
||||
value: fuelUnit,
|
||||
onChange: (e) => setFuelUnit(e.target.value),
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 p-2'
|
||||
}, [
|
||||
React.createElement('option', {
|
||||
key: 'liters',
|
||||
value: 'liters'
|
||||
}, 'Liters'),
|
||||
React.createElement('option', {
|
||||
key: 'gallons',
|
||||
value: 'gallons'
|
||||
}, 'Gallons')
|
||||
])
|
||||
])
|
||||
])
|
||||
]) : null,
|
||||
|
||||
// Distance Based Form
|
||||
calculationType === 'distance' ? React.createElement(React.Fragment, {
|
||||
key: 'distance-form'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'distance-input'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700'
|
||||
}, 'Distance (nautical miles)'),
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
min: '1',
|
||||
value: distance,
|
||||
onChange: (e) => setDistance(e.target.value),
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 p-2',
|
||||
required: true
|
||||
})
|
||||
]),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'speed-input'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700'
|
||||
}, 'Average Speed (knots)'),
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
min: '1',
|
||||
max: '50',
|
||||
value: speed,
|
||||
onChange: (e) => setSpeed(e.target.value),
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 p-2',
|
||||
required: true
|
||||
})
|
||||
]),
|
||||
|
||||
React.createElement('div', {
|
||||
key: 'fuel-rate-input'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700'
|
||||
}, 'Fuel Consumption Rate (liters per hour)'),
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
min: '1',
|
||||
step: '1',
|
||||
value: fuelRate,
|
||||
onChange: (e) => setFuelRate(e.target.value),
|
||||
className: 'mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 p-2',
|
||||
required: true
|
||||
}),
|
||||
React.createElement('p', {
|
||||
className: 'mt-1 text-sm text-gray-500'
|
||||
}, 'Typical range: 50 - 500 liters per hour for most yachts')
|
||||
])
|
||||
]) : null,
|
||||
|
||||
// Common Form Elements
|
||||
React.createElement('div', {
|
||||
key: 'currency-select'
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700'
|
||||
}, 'Select Currency'),
|
||||
React.createElement('div', {
|
||||
className: 'max-w-xs'
|
||||
}, [
|
||||
React.createElement(CurrencySelect, {
|
||||
value: currency,
|
||||
onChange: setCurrency
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
React.createElement('button', {
|
||||
key: 'calculate-btn',
|
||||
type: 'submit',
|
||||
className: 'w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors'
|
||||
}, 'Calculate Impact')
|
||||
]),
|
||||
|
||||
// Results Section
|
||||
tripEstimate && calculationType !== 'custom' ? React.createElement('div', {
|
||||
key: 'results',
|
||||
className: 'mt-6 space-y-6'
|
||||
}, [
|
||||
// Trip Details
|
||||
React.createElement('div', {
|
||||
key: 'trip-details',
|
||||
className: 'grid grid-cols-2 gap-4'
|
||||
}, [
|
||||
// Trip Duration (only for distance calculation)
|
||||
calculationType === 'distance' ? React.createElement('div', {
|
||||
key: 'duration',
|
||||
className: 'bg-gray-50 p-4 rounded-lg'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
className: 'text-sm text-gray-600'
|
||||
}, 'Trip Duration'),
|
||||
React.createElement('p', {
|
||||
className: 'text-xl font-bold text-gray-900'
|
||||
}, `${tripEstimate.duration.toFixed(1)} hours`)
|
||||
]) : null,
|
||||
|
||||
// Fuel Consumption
|
||||
React.createElement('div', {
|
||||
key: 'fuel',
|
||||
className: 'bg-gray-50 p-4 rounded-lg'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
className: 'text-sm text-gray-600'
|
||||
}, 'Fuel Consumption'),
|
||||
React.createElement('p', {
|
||||
className: 'text-xl font-bold text-gray-900'
|
||||
}, `${tripEstimate.fuelConsumption.toLocaleString()} ${fuelUnit}`)
|
||||
])
|
||||
]),
|
||||
|
||||
// Offset Percentage Selection
|
||||
React.createElement('div', {
|
||||
key: 'offset-percent',
|
||||
}, [
|
||||
React.createElement('label', {
|
||||
className: 'block text-sm font-medium text-gray-700 mb-2'
|
||||
}, 'Offset Percentage'),
|
||||
React.createElement('div', {
|
||||
className: 'flex flex-wrap gap-3 mb-3'
|
||||
}, [
|
||||
// Preset percentage buttons
|
||||
[100, 75, 50, 25].map((percent) => (
|
||||
React.createElement('button', {
|
||||
key: `percent-${percent}`,
|
||||
type: 'button',
|
||||
onClick: () => handlePresetPercentage(percent),
|
||||
className: `px-4 py-2 rounded-lg transition-colors ${
|
||||
offsetPercentage === percent && customPercentage === ''
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`
|
||||
}, `${percent}%`)
|
||||
)),
|
||||
|
||||
// Custom percentage input
|
||||
React.createElement('div', {
|
||||
className: 'flex items-center space-x-2'
|
||||
}, [
|
||||
React.createElement('input', {
|
||||
type: 'number',
|
||||
value: customPercentage,
|
||||
onChange: handleCustomPercentageChange,
|
||||
placeholder: 'Custom %',
|
||||
min: '0',
|
||||
max: '100',
|
||||
className: 'w-24 px-3 py-2 border rounded-lg focus:ring-blue-500 focus:border-blue-500'
|
||||
}),
|
||||
React.createElement('span', {
|
||||
className: 'text-gray-600'
|
||||
}, '%')
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
// Selected CO₂ Offset
|
||||
React.createElement('div', {
|
||||
key: 'co2-offset',
|
||||
className: 'bg-blue-50 p-4 rounded-lg'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
className: 'text-sm text-gray-600'
|
||||
}, 'Selected CO₂ Offset'),
|
||||
React.createElement('p', {
|
||||
className: 'text-2xl font-bold text-blue-900'
|
||||
}, `${calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage).toFixed(2)} tons`),
|
||||
React.createElement('p', {
|
||||
className: 'text-sm text-blue-600 mt-1'
|
||||
}, `${offsetPercentage}% of ${tripEstimate.co2Emissions.toFixed(2)} tons`)
|
||||
]),
|
||||
|
||||
// Offset Button
|
||||
React.createElement('button', {
|
||||
key: 'offset-btn',
|
||||
onClick: () => onOffsetClick(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage)),
|
||||
className: 'w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors'
|
||||
}, 'Offset Your Impact')
|
||||
]) : null
|
||||
]);
|
||||
}
|
||||
|
||||
// OffsetOrder Component
|
||||
function OffsetOrder({ tons, monetaryAmount, onBack, calculatorType }) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [currency, setCurrency] = useState('USD');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
// Send analytics event
|
||||
analytics.event('offset', 'purchase', `${tons} tons`);
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSuccess(true);
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
if (isSuccess) {
|
||||
return React.createElement('div', {
|
||||
className: 'bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
key: 'success-icon',
|
||||
className: 'flex justify-center mb-6'
|
||||
}, [
|
||||
// Success checkmark icon (simplified)
|
||||
React.createElement('div', {
|
||||
className: 'w-16 h-16 bg-green-100 rounded-full flex items-center justify-center'
|
||||
}, [
|
||||
React.createElement('span', {
|
||||
className: 'text-green-500 text-2xl'
|
||||
}, '✓')
|
||||
])
|
||||
]),
|
||||
React.createElement('h2', {
|
||||
key: 'success-title',
|
||||
className: 'text-2xl font-bold text-gray-800 text-center mb-4'
|
||||
}, 'Thank You for Your Contribution!'),
|
||||
React.createElement('p', {
|
||||
key: 'success-message',
|
||||
className: 'text-gray-600 text-center mb-6'
|
||||
}, monetaryAmount
|
||||
? `Your contribution of ${formatCurrency(monetaryAmount, currencies[currency])} has been received.`
|
||||
: `Your offset of ${tons.toFixed(2)} tons of CO₂ has been successfully processed.`),
|
||||
React.createElement('p', {
|
||||
key: 'success-email',
|
||||
className: 'text-gray-600 text-center mb-8'
|
||||
}, `A confirmation email has been sent to ${email}.`),
|
||||
React.createElement('button', {
|
||||
key: 'back-btn',
|
||||
onClick: onBack,
|
||||
className: 'block w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors'
|
||||
}, 'Return to Calculator')
|
||||
]);
|
||||
}
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'bg-white rounded-lg shadow-xl p-6 max-w-2xl w-full'
|
||||
}, [
|
||||
// Back button
|
||||
React.createElement('button', {
|
||||
key: 'back-button',
|
||||
onClick: onBack,
|
||||
className: 'text-sm text-blue-500 mb-6 flex items-center'
|
||||
}, '← Back to Calculator'),
|
||||
|
||||
React.createElement('h2', {
|
||||
key: 'title',
|
||||
className: 'text-2xl font-bold text-gray-800 mb-6'
|
||||
}, 'Complete Your Carbon Offset'),
|
||||
|
||||
// Description
|
||||
React.createElement('div', {
|
||||
key: 'description',
|
||||
className: 'mb-6'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
className: 'p-4 bg-blue-50 rounded-lg mb-4'
|
||||
}, [
|
||||
React.createElement('p', {
|
||||
className: 'font-semibold text-blue-800'
|
||||
}, monetaryAmount
|
||||
? `You're contributing: ${formatCurrency(monetaryAmount, currencies[currency])}`
|
||||
: `You're offsetting: ${tons.toFixed(2)} tons of CO₂`)
|
||||
]),
|
||||
React.createElement('p', {
|
||||
className: 'text-gray-600'
|
||||
}, 'Your offset will be invested in certified projects that reduce greenhouse gas emissions and support sustainable development.')
|
||||
]),
|
||||
|
||||
// Form
|
||||
React.createElement('form', {
|
||||
key: 'form',
|
||||
onSubmit: handleSubmit,
|
||||
className: 'space-y-4'
|
||||
}, [
|
||||
// Name field
|
||||
React.createElement('div', {}, [
|
||||
React.createElement('label', {
|
||||
htmlFor: 'name',
|
||||
className: 'block text-sm font-medium text-gray-700 mb-1'
|
||||
}, 'Full Name'),
|
||||
React.createElement('input', {
|
||||
id: 'name',
|
||||
type: 'text',
|
||||
value: name,
|
||||
onChange: (e) => setName(e.target.value),
|
||||
className: 'w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500',
|
||||
required: true
|
||||
})
|
||||
]),
|
||||
|
||||
// Email field
|
||||
React.createElement('div', {}, [
|
||||
React.createElement('label', {
|
||||
htmlFor: 'email',
|
||||
className: 'block text-sm font-medium text-gray-700 mb-1'
|
||||
}, 'Email Address'),
|
||||
React.createElement('input', {
|
||||
id: 'email',
|
||||
type: 'email',
|
||||
value: email,
|
||||
onChange: (e) => setEmail(e.target.value),
|
||||
className: 'w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500',
|
||||
required: true
|
||||
})
|
||||
]),
|
||||
|
||||
// Currency field
|
||||
React.createElement('div', {}, [
|
||||
React.createElement('label', {
|
||||
htmlFor: 'currency',
|
||||
className: 'block text-sm font-medium text-gray-700 mb-1'
|
||||
}, 'Currency'),
|
||||
React.createElement('div', {
|
||||
className: 'max-w-xs'
|
||||
}, [
|
||||
React.createElement(CurrencySelect, {
|
||||
value: currency,
|
||||
onChange: setCurrency
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
// Error message
|
||||
error ? React.createElement('div', {
|
||||
className: 'p-3 bg-red-50 text-red-700 rounded-md'
|
||||
}, error) : null,
|
||||
|
||||
// Submit button
|
||||
React.createElement('button', {
|
||||
type: 'submit',
|
||||
disabled: isSubmitting,
|
||||
className: `w-full bg-blue-500 text-white py-2 px-4 rounded-lg hover:bg-blue-600 transition-colors ${
|
||||
isSubmitting ? 'opacity-70 cursor-not-allowed' : ''
|
||||
}`
|
||||
}, isSubmitting ? 'Processing...' : 'Complete Offset')
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Main App Component
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState('calculator');
|
||||
const [showOffsetOrder, setShowOffsetOrder] = useState(false);
|
||||
const [offsetTons, setOffsetTons] = useState(0);
|
||||
const [monetaryAmount, setMonetaryAmount] = useState(undefined);
|
||||
const [calculatorType, setCalculatorType] = useState('trip');
|
||||
|
||||
useEffect(() => {
|
||||
analytics.pageView(currentPage);
|
||||
}, [currentPage]);
|
||||
|
||||
const handleOffsetClick = (tons, monetaryAmount) => {
|
||||
setOffsetTons(tons);
|
||||
setMonetaryAmount(monetaryAmount);
|
||||
setShowOffsetOrder(true);
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
if (showOffsetOrder) {
|
||||
return React.createElement(OffsetOrder, {
|
||||
tons: offsetTons,
|
||||
monetaryAmount: monetaryAmount,
|
||||
onBack: () => setShowOffsetOrder(false),
|
||||
calculatorType: calculatorType
|
||||
});
|
||||
}
|
||||
|
||||
return React.createElement(TripCalculator, {
|
||||
vesselData: sampleVessel,
|
||||
onOffsetClick: handleOffsetClick
|
||||
});
|
||||
};
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'min-h-[600px] bg-gradient-to-b from-blue-50 to-green-50'
|
||||
}, [
|
||||
// Header
|
||||
React.createElement('div', {
|
||||
key: 'header',
|
||||
className: 'bg-white shadow-sm py-4 px-6 mb-8'
|
||||
}, [
|
||||
React.createElement('div', {
|
||||
className: 'flex items-center space-x-2'
|
||||
}, [
|
||||
// Bird icon would be here in a real implementation
|
||||
React.createElement('h1', {
|
||||
className: 'text-xl font-bold text-gray-900'
|
||||
}, 'Puffin Offset')
|
||||
])
|
||||
]),
|
||||
|
||||
// Main content
|
||||
React.createElement('div', {
|
||||
key: 'main',
|
||||
className: 'px-4 sm:px-6 flex justify-center'
|
||||
}, renderContent()),
|
||||
|
||||
// Footer
|
||||
React.createElement('div', {
|
||||
key: 'footer',
|
||||
className: 'bg-white mt-16 py-4 px-6 text-center text-gray-500'
|
||||
}, 'Powered by Verified Carbon Offset Projects')
|
||||
]);
|
||||
}
|
||||
|
||||
// Initialize React application
|
||||
ReactDOM.createRoot(element).render(React.createElement(App, {}));
|
||||
}
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget class for the calculator
|
||||
*/
|
||||
class Puffin_Calculator_Widget_Class extends WP_Widget {
|
||||
|
||||
/**
|
||||
* Initialize the widget
|
||||
*/
|
||||
public function __construct() {
|
||||
parent::__construct(
|
||||
'puffin_calculator_widget',
|
||||
'Puffin Offset Calculator',
|
||||
array('description' => 'Adds the Puffin Offset Calculator to a widget area')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display of the widget
|
||||
*/
|
||||
public function widget($args, $instance) {
|
||||
echo $args['before_widget'];
|
||||
|
||||
if (!empty($instance['title'])) {
|
||||
echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
|
||||
}
|
||||
|
||||
echo do_shortcode('[puffin_calculator]');
|
||||
|
||||
echo $args['after_widget'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Back-end widget form
|
||||
*/
|
||||
public function form($instance) {
|
||||
$title = isset($instance['title']) ? $instance['title'] : 'Carbon Offset Calculator';
|
||||
?>
|
||||
<p>
|
||||
<label for="<?php echo $this->get_field_id('title'); ?>">Title:</label>
|
||||
<input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>">
|
||||
</p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize widget form values as they are saved
|
||||
*/
|
||||
public function update($new_instance, $old_instance) {
|
||||
$instance = array();
|
||||
$instance['title'] = (!empty($new_instance['title'])) ? sanitize_text_field($new_instance['title']) : '';
|
||||
|
||||
return $instance;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the plugin
|
||||
new Puffin_Calculator_Integrated();
|
||||
Loading…
Reference in New Issue