'100%', 'height' => 'auto', 'theme' => 'light', ), $atts); // Generate unique ID for this instance $calculator_id = 'puffin-calculator-' . uniqid(); // Start output buffer ob_start(); ?>
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'; ?>