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 { CurrencySelect } from './CurrencySelect';
|
||||
import { PWAInstallPrompt } from './PWAInstallPrompt';
|
||||
import { MobileOffsetOrder } from './MobileOffsetOrder';
|
||||
|
||||
interface Props {
|
||||
vesselData: VesselData;
|
||||
@ -25,6 +26,9 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
||||
const [offsetPercentage, setOffsetPercentage] = useState<number>(100);
|
||||
const [customPercentage, setCustomPercentage] = 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) => {
|
||||
e.preventDefault();
|
||||
@ -79,6 +83,17 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
||||
{ id: 'custom', icon: DollarSign, label: 'Custom Amount', color: 'purple' }
|
||||
];
|
||||
|
||||
if (showOffsetOrder) {
|
||||
return (
|
||||
<MobileOffsetOrder
|
||||
tons={offsetTons}
|
||||
monetaryAmount={monetaryAmount}
|
||||
currency={currency}
|
||||
onBack={() => setShowOffsetOrder(false)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-cyan-50">
|
||||
{/* Mobile Header */}
|
||||
@ -210,7 +225,11 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
||||
|
||||
{customAmount && Number(customAmount) > 0 && (
|
||||
<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"
|
||||
whileHover={{ scale: 1.02 }}
|
||||
whileTap={{ scale: 0.98 }}
|
||||
@ -435,7 +454,11 @@ export function MobileCalculator({ vesselData, onOffsetClick, onBack }: Props) {
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>
|
||||
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