Add MobileOffsetOrder component for CO₂ offset ordering process
This commit is contained in:
parent
8cc4284140
commit
fc828becdc
@ -6,6 +6,7 @@ import { calculateTripCarbon, calculateCarbonFromFuel } from '../utils/carbonCal
|
|||||||
import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies';
|
import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies';
|
||||||
import { CurrencySelect } from './CurrencySelect';
|
import { CurrencySelect } from './CurrencySelect';
|
||||||
import { PWAInstallPrompt } from './PWAInstallPrompt';
|
import { PWAInstallPrompt } from './PWAInstallPrompt';
|
||||||
|
import { MobileOffsetOrder } from './MobileOffsetOrder';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
vesselData: VesselData;
|
vesselData: VesselData;
|
||||||
@ -25,6 +26,9 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
|||||||
const [offsetPercentage, setOffsetPercentage] = useState<number>(100);
|
const [offsetPercentage, setOffsetPercentage] = useState<number>(100);
|
||||||
const [customPercentage, setCustomPercentage] = useState<string>('');
|
const [customPercentage, setCustomPercentage] = useState<string>('');
|
||||||
const [customAmount, setCustomAmount] = useState<string>('');
|
const [customAmount, setCustomAmount] = useState<string>('');
|
||||||
|
const [showOffsetOrder, setShowOffsetOrder] = useState(false);
|
||||||
|
const [offsetTons, setOffsetTons] = useState(0);
|
||||||
|
const [monetaryAmount, setMonetaryAmount] = useState<number | undefined>();
|
||||||
|
|
||||||
const handleCalculate = useCallback((e: React.FormEvent) => {
|
const handleCalculate = useCallback((e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -79,6 +83,17 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
|||||||
{ id: 'custom', icon: DollarSign, label: 'Custom Amount', color: 'purple' }
|
{ id: 'custom', icon: DollarSign, label: 'Custom Amount', color: 'purple' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (showOffsetOrder) {
|
||||||
|
return (
|
||||||
|
<MobileOffsetOrder
|
||||||
|
tons={offsetTons}
|
||||||
|
monetaryAmount={monetaryAmount}
|
||||||
|
currency={currency}
|
||||||
|
onBack={() => setShowOffsetOrder(false)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
|
||||||
{/* Mobile Header */}
|
{/* Mobile Header */}
|
||||||
@ -210,7 +225,11 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
|||||||
|
|
||||||
{customAmount && Number(customAmount) > 0 && (
|
{customAmount && Number(customAmount) > 0 && (
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => onOffsetClick?.(0, Number(customAmount))}
|
onClick={() => {
|
||||||
|
setOffsetTons(0);
|
||||||
|
setMonetaryAmount(Number(customAmount));
|
||||||
|
setShowOffsetOrder(true);
|
||||||
|
}}
|
||||||
className="w-full mt-6 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all"
|
className="w-full mt-6 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
@ -435,7 +454,11 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => onOffsetClick?.(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage))}
|
onClick={() => {
|
||||||
|
setOffsetTons(calculateOffsetAmount(tripEstimate.co2Emissions, offsetPercentage));
|
||||||
|
setMonetaryAmount(undefined);
|
||||||
|
setShowOffsetOrder(true);
|
||||||
|
}}
|
||||||
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all"
|
className="w-full bg-gradient-to-r from-green-500 to-green-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all"
|
||||||
>
|
>
|
||||||
Offset Your Impact
|
Offset Your Impact
|
||||||
|
|||||||
375
src/components/MobileOffsetOrder.tsx
Normal file
375
src/components/MobileOffsetOrder.tsx
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Check, ArrowLeft, Loader2, CreditCard, User, Mail, Phone } from 'lucide-react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import type { CurrencyCode } from '../types';
|
||||||
|
import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tons: number;
|
||||||
|
monetaryAmount?: number;
|
||||||
|
currency: CurrencyCode;
|
||||||
|
onBack: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Props) {
|
||||||
|
const [currentStep, setCurrentStep] = useState<'summary' | 'payment' | 'confirmation'>('summary');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [orderData, setOrderData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate cost (using dummy pricing)
|
||||||
|
const pricePerTon = 18; // $18 per ton
|
||||||
|
const totalCost = monetaryAmount || (tons * pricePerTon);
|
||||||
|
const targetCurrency = getCurrencyByCode(currency);
|
||||||
|
|
||||||
|
const handleInputChange = (field: keyof typeof orderData, value: string) => {
|
||||||
|
setOrderData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProceedToPayment = () => {
|
||||||
|
setCurrentStep('payment');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlaceOrder = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
// Simulate API call
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
setCurrentStep('confirmation');
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateOrderId = () => {
|
||||||
|
return 'PO-' + Date.now().toString().slice(-8);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSummaryStep = () => (
|
||||||
|
<motion.div
|
||||||
|
className="space-y-6"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Offset Summary</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex justify-between items-center p-4 bg-blue-50 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-blue-900">CO₂ to Offset</div>
|
||||||
|
<div className="text-sm text-blue-600">From your yacht emissions</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-blue-900">
|
||||||
|
{tons.toFixed(2)} tons
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center p-4 bg-green-50 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-green-900">Price per Ton</div>
|
||||||
|
<div className="text-sm text-green-600">Verified carbon credits</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xl font-bold text-green-900">
|
||||||
|
{formatCurrency(pricePerTon, targetCurrency)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t pt-4">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-lg font-semibold text-gray-900">Total Cost</span>
|
||||||
|
<span className="text-2xl font-bold text-gray-900">
|
||||||
|
{formatCurrency(totalCost, targetCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<h3 className="text-lg font-semibold mb-4">Your Details</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<User size={16} className="inline mr-2" />
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={orderData.name}
|
||||||
|
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||||
|
placeholder="Enter your full name"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<Mail size={16} className="inline mr-2" />
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={orderData.email}
|
||||||
|
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||||
|
placeholder="Enter your email"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
<Phone size={16} className="inline mr-2" />
|
||||||
|
Phone Number (Optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
value={orderData.phone}
|
||||||
|
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||||
|
placeholder="Enter your phone number"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleProceedToPayment}
|
||||||
|
disabled={!orderData.name || !orderData.email}
|
||||||
|
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Proceed to Payment
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPaymentStep = () => (
|
||||||
|
<motion.div
|
||||||
|
className="space-y-6"
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: -20 }}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 flex items-center">
|
||||||
|
<CreditCard size={20} className="mr-2" />
|
||||||
|
Payment Information
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Card Number
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="1234 5678 9012 3456"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Expiry
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="MM/YY"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
CVV
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="123"
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Cardholder Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={orderData.name}
|
||||||
|
className="w-full py-3 px-4 border border-gray-300 rounded-xl bg-gray-50"
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-2xl p-6">
|
||||||
|
<h4 className="font-semibold mb-3">Order Summary</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">CO₂ Offset:</span>
|
||||||
|
<span className="font-medium">{tons.toFixed(2)} tons</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Total:</span>
|
||||||
|
<span className="font-bold text-lg">
|
||||||
|
{formatCurrency(totalCost, targetCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentStep('summary')}
|
||||||
|
className="py-3 px-4 border border-gray-300 text-gray-700 rounded-xl font-medium hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handlePlaceOrder}
|
||||||
|
disabled={loading}
|
||||||
|
className="py-3 px-4 bg-gradient-to-r from-green-500 to-green-600 text-white rounded-xl font-semibold hover:shadow-lg transition-all disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<Loader2 className="animate-spin mr-2" size={20} />
|
||||||
|
Processing...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
'Place Order'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderConfirmationStep = () => (
|
||||||
|
<motion.div
|
||||||
|
className="space-y-6 text-center"
|
||||||
|
initial={{ opacity: 0, scale: 0.9 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.9 }}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-2xl p-8 shadow-sm">
|
||||||
|
<div className="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6">
|
||||||
|
<Check className="text-green-500" size={40} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">
|
||||||
|
Order Confirmed!
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
Congratulations! You've successfully offset {tons.toFixed(2)} tons of CO₂ from your yacht emissions.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="bg-gray-50 rounded-xl p-6 mb-6">
|
||||||
|
<h4 className="font-semibold mb-4">Order Details</h4>
|
||||||
|
<div className="space-y-3 text-left">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Order ID:</span>
|
||||||
|
<span className="font-mono font-medium">{generateOrderId()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">CO₂ Offset:</span>
|
||||||
|
<span className="font-medium">{tons.toFixed(2)} tons</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Amount Paid:</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{formatCurrency(totalCost, targetCurrency)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">Email:</span>
|
||||||
|
<span className="font-medium">{orderData.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-blue-50 rounded-xl p-4 mb-6">
|
||||||
|
<p className="text-sm text-blue-800">
|
||||||
|
📧 A confirmation email has been sent to {orderData.email} with your certificate and receipt.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onBack}
|
||||||
|
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 px-6 rounded-xl font-semibold text-lg shadow-md hover:shadow-lg transition-all"
|
||||||
|
>
|
||||||
|
Back to Calculator
|
||||||
|
</button>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
|
||||||
|
{/* Mobile Header */}
|
||||||
|
<motion.header
|
||||||
|
className="bg-white/80 backdrop-blur-md shadow-sm sticky top-0 z-50"
|
||||||
|
initial={{ y: -100 }}
|
||||||
|
animate={{ y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between p-4">
|
||||||
|
<motion.button
|
||||||
|
onClick={currentStep === 'confirmation' ? onBack : () => {
|
||||||
|
if (currentStep === 'payment') {
|
||||||
|
setCurrentStep('summary');
|
||||||
|
} else {
|
||||||
|
onBack();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 rounded-xl bg-gray-100 hover:bg-gray-200 transition-colors"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={20} />
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<h1 className="text-lg font-bold text-gray-900">
|
||||||
|
{currentStep === 'summary' ? 'Order Summary' :
|
||||||
|
currentStep === 'payment' ? 'Payment' :
|
||||||
|
'Order Confirmed'}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-10"></div> {/* Spacer */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{currentStep !== 'confirmation' && (
|
||||||
|
<div className="px-4 pb-3">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<div className={`flex-1 h-2 rounded-full ${
|
||||||
|
currentStep === 'summary' ? 'bg-blue-200' : 'bg-blue-500'
|
||||||
|
}`} />
|
||||||
|
<div className={`flex-1 h-2 rounded-full ${
|
||||||
|
currentStep === 'payment' ? 'bg-blue-500' : 'bg-gray-200'
|
||||||
|
}`} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.header>
|
||||||
|
|
||||||
|
<div className="p-4 pb-20">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{currentStep === 'summary' && renderSummaryStep()}
|
||||||
|
{currentStep === 'payment' && renderPaymentStep()}
|
||||||
|
{currentStep === 'confirmation' && renderConfirmationStep()}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user