Refactor MobileOffsetOrder component for improved structure and clarity
This commit is contained in:
parent
1a9a1b9464
commit
e67e64947c
@ -1,8 +1,11 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Check, ArrowLeft, Loader2, CreditCard, User, Mail, Phone } from 'lucide-react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Check, ArrowLeft, Loader2, User, Mail, Phone, Globe2, TreePine, Waves, Factory, Wind, X, AlertCircle } from 'lucide-react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import type { CurrencyCode } from '../types';
|
||||
import { createOffsetOrder, getPortfolios } from '../api/wrenClient';
|
||||
import type { CurrencyCode, OffsetOrder as OffsetOrderType, Portfolio, OffsetProject } from '../types';
|
||||
import { currencies, formatCurrency, getCurrencyByCode } from '../utils/currencies';
|
||||
import { config } from '../utils/config';
|
||||
import { sendFormspreeEmail } from '../utils/email';
|
||||
|
||||
interface Props {
|
||||
tons: number;
|
||||
@ -11,89 +14,130 @@ interface Props {
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
location: string;
|
||||
pricePerTon: number;
|
||||
imageUrl: string;
|
||||
percentage: number;
|
||||
interface ProjectTypeIconProps {
|
||||
project: OffsetProject;
|
||||
}
|
||||
|
||||
const ProjectTypeIcon = ({ project }: ProjectTypeIconProps) => {
|
||||
if (!project || !project.type) {
|
||||
return <Globe2 className="text-blue-500" size={16} />;
|
||||
}
|
||||
|
||||
const type = project.type.toLowerCase();
|
||||
|
||||
switch (type) {
|
||||
case 'direct air capture':
|
||||
return <Factory className="text-purple-500" size={16} />;
|
||||
case 'blue carbon':
|
||||
return <Waves className="text-blue-500" size={16} />;
|
||||
case 'renewable energy':
|
||||
return <Wind className="text-green-500" size={16} />;
|
||||
case 'forestry':
|
||||
return <TreePine className="text-green-500" size={16} />;
|
||||
default:
|
||||
return <Globe2 className="text-blue-500" size={16} />;
|
||||
}
|
||||
};
|
||||
|
||||
export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Props) {
|
||||
const [currentStep, setCurrentStep] = useState<'summary' | 'projects' | 'payment' | 'confirmation'>('summary');
|
||||
const [currentStep, setCurrentStep] = useState<'summary' | 'projects' | 'confirmation'>('summary');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [orderData, setOrderData] = useState({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState(false);
|
||||
const [order, setOrder] = useState<OffsetOrderType | null>(null);
|
||||
const [portfolio, setPortfolio] = useState<Portfolio | null>(null);
|
||||
const [loadingPortfolio, setLoadingPortfolio] = useState(true);
|
||||
const [selectedProject, setSelectedProject] = useState<OffsetProject | null>(null);
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: ''
|
||||
phone: '',
|
||||
company: '',
|
||||
message: `I would like to offset ${tons.toFixed(2)} tons of CO2 from my yacht's emissions.`
|
||||
});
|
||||
|
||||
// Dummy projects data
|
||||
const projects: Project[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Coastal Blue Carbon',
|
||||
type: 'Blue Carbon',
|
||||
description: 'Protecting and restoring coastal wetlands that capture carbon 10x faster than forests.',
|
||||
location: 'Indonesia',
|
||||
pricePerTon: 25,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1559827260-dc66d52bef19?auto=format&fit=crop&q=80&w=800',
|
||||
percentage: 0.35
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Wind Power Initiative',
|
||||
type: 'Renewable Energy',
|
||||
description: 'Supporting offshore wind farms to replace fossil fuel energy generation.',
|
||||
location: 'North Sea',
|
||||
pricePerTon: 15,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1548337138-e87d889cc369?auto=format&fit=crop&q=80&w=800',
|
||||
percentage: 0.30
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Rainforest Conservation',
|
||||
type: 'Forestry',
|
||||
description: 'Protecting critical rainforest habitats from deforestation.',
|
||||
location: 'Amazon Basin',
|
||||
pricePerTon: 12,
|
||||
imageUrl: 'https://images.unsplash.com/photo-1511884642898-4c92249e20b6?auto=format&fit=crop&q=80&w=800',
|
||||
percentage: 0.35
|
||||
useEffect(() => {
|
||||
if (!config.wrenApiKey) {
|
||||
setError('Carbon offset service is currently unavailable. Please use our contact form to request offsetting.');
|
||||
setLoadingPortfolio(false);
|
||||
return;
|
||||
}
|
||||
];
|
||||
fetchPortfolio();
|
||||
}, []);
|
||||
|
||||
// Calculate cost (using dummy pricing)
|
||||
const pricePerTon = 18; // $18 per ton
|
||||
const totalCost = monetaryAmount || (tons * pricePerTon);
|
||||
const targetCurrency = getCurrencyByCode(currency);
|
||||
const fetchPortfolio = async () => {
|
||||
try {
|
||||
const allPortfolios = await getPortfolios();
|
||||
|
||||
const handleInputChange = (field: keyof typeof orderData, value: string) => {
|
||||
setOrderData(prev => ({ ...prev, [field]: value }));
|
||||
if (!allPortfolios || allPortfolios.length === 0) {
|
||||
throw new Error('No portfolios available');
|
||||
}
|
||||
|
||||
const puffinPortfolio = allPortfolios.find(p =>
|
||||
p.name.toLowerCase().includes('puffin') ||
|
||||
p.name.toLowerCase().includes('maritime')
|
||||
);
|
||||
|
||||
if (puffinPortfolio) {
|
||||
console.log('[MobileOffsetOrder] Found Puffin portfolio with ID:', puffinPortfolio.id);
|
||||
setPortfolio(puffinPortfolio);
|
||||
} else {
|
||||
console.log('[MobileOffsetOrder] No Puffin portfolio found, using first available portfolio with ID:', allPortfolios[0].id);
|
||||
setPortfolio(allPortfolios[0]);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to fetch portfolio information. Please try again.');
|
||||
} finally {
|
||||
setLoadingPortfolio(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleProceedToPayment = () => {
|
||||
const handleOffsetOrder = async () => {
|
||||
if (!portfolio) return;
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const newOrder = await createOffsetOrder(portfolio.id, tons);
|
||||
setOrder(newOrder);
|
||||
setSuccess(true);
|
||||
setCurrentStep('confirmation');
|
||||
} catch (err) {
|
||||
setError('Failed to create offset order. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const renderPortfolioPrice = (portfolio: Portfolio) => {
|
||||
try {
|
||||
const pricePerTon = portfolio.pricePerTon || 18;
|
||||
const targetCurrency = getCurrencyByCode(currency);
|
||||
return formatCurrency(pricePerTon, targetCurrency);
|
||||
} catch (err) {
|
||||
console.error('Error formatting portfolio price:', err);
|
||||
return formatCurrency(18, currencies.USD);
|
||||
}
|
||||
};
|
||||
|
||||
const offsetCost = monetaryAmount || (portfolio ? tons * (portfolio.pricePerTon || 18) : 0);
|
||||
const targetCurrency = getCurrencyByCode(currency);
|
||||
|
||||
const handleInputChange = (field: keyof typeof formData, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleProceedToProjects = () => {
|
||||
setCurrentStep('projects');
|
||||
};
|
||||
|
||||
const handleProceedFromProjects = () => {
|
||||
setCurrentStep('payment');
|
||||
};
|
||||
const handleProjectClick = useCallback((project: OffsetProject) => {
|
||||
setSelectedProject(project);
|
||||
}, []);
|
||||
|
||||
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 handleCloseLightbox = () => {
|
||||
setSelectedProject(null);
|
||||
};
|
||||
|
||||
const renderSummaryStep = () => (
|
||||
@ -103,6 +147,105 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
>
|
||||
{error && !config.wrenApiKey ? (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-blue-900 mb-4">
|
||||
Contact Us for Offsetting
|
||||
</h3>
|
||||
<p className="text-blue-700 mb-4 text-sm">
|
||||
Our automated offsetting service is temporarily unavailable. Please fill out the form below and our team will help you offset your emissions.
|
||||
</p>
|
||||
<form onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
try {
|
||||
await sendFormspreeEmail(formData, 'offset');
|
||||
setSuccess(true);
|
||||
setCurrentStep('confirmation');
|
||||
} catch (err) {
|
||||
setError('Failed to send request. Please try again.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.name}
|
||||
onChange={(e) => handleInputChange('name', e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={formData.email}
|
||||
onChange={(e) => handleInputChange('email', e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Phone
|
||||
</label>
|
||||
<input
|
||||
type="tel"
|
||||
value={formData.phone}
|
||||
onChange={(e) => handleInputChange('phone', e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Message
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={formData.message}
|
||||
onChange={(e) => handleInputChange('message', e.target.value)}
|
||||
className="w-full px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full flex items-center justify-center bg-blue-500 text-white py-3 rounded-lg transition-colors ${
|
||||
loading ? 'opacity-50 cursor-not-allowed' : 'hover:bg-blue-600'
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin mr-2" size={20} />
|
||||
Sending Request...
|
||||
</>
|
||||
) : (
|
||||
'Send Offset Request'
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<AlertCircle className="text-red-500" size={20} />
|
||||
<p className="text-red-700 text-sm">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : loadingPortfolio ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<Loader2 className="animate-spin text-blue-500" size={32} />
|
||||
<span className="ml-2 text-gray-600">Loading portfolio information...</span>
|
||||
</div>
|
||||
) : portfolio ? (
|
||||
<>
|
||||
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-4">Offset Summary</h3>
|
||||
|
||||
@ -123,7 +266,7 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<div className="text-sm text-green-600">Verified carbon credits</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold text-green-900">
|
||||
{formatCurrency(pricePerTon, targetCurrency)}
|
||||
{renderPortfolioPrice(portfolio)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -131,70 +274,27 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<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)}
|
||||
{formatCurrency(offsetCost, 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 className="bg-gray-50 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-gray-900 mb-2">Portfolio</h4>
|
||||
<p className="text-sm text-gray-600">{portfolio.name}</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{portfolio.description}</p>
|
||||
</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"
|
||||
onClick={handleProceedToProjects}
|
||||
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"
|
||||
>
|
||||
Proceed to Payment
|
||||
View Projects & Continue
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@ -205,21 +305,26 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
>
|
||||
{portfolio && (
|
||||
<>
|
||||
<div className="bg-white rounded-2xl p-6 shadow-sm">
|
||||
<h3 className="text-lg font-semibold mb-2">Project Portfolio</h3>
|
||||
<h3 className="text-lg font-semibold mb-2">{portfolio.name}</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Your offset will be distributed across these verified carbon reduction projects
|
||||
</p>
|
||||
|
||||
{portfolio.projects && portfolio.projects.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{projects.map((project, index) => (
|
||||
{portfolio.projects.map((project, index) => (
|
||||
<motion.div
|
||||
key={project.id}
|
||||
className="border border-gray-200 rounded-xl overflow-hidden"
|
||||
key={project.id || `project-${index}`}
|
||||
className="border border-gray-200 rounded-xl overflow-hidden cursor-pointer hover:border-blue-400 transition-colors"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
onClick={() => handleProjectClick(project)}
|
||||
>
|
||||
{project.imageUrl && (
|
||||
<div className="relative h-32">
|
||||
<img
|
||||
src={project.imageUrl}
|
||||
@ -229,32 +334,42 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
|
||||
<div className="absolute bottom-2 left-3 right-3">
|
||||
<h4 className="text-white font-semibold text-sm">{project.name}</h4>
|
||||
<p className="text-white/80 text-xs">{project.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-4">
|
||||
{!project.imageUrl && (
|
||||
<h4 className="font-semibold text-gray-900 mb-2">{project.name}</h4>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-medium text-blue-600 bg-blue-50 px-2 py-1 rounded">
|
||||
{project.type}
|
||||
<div className="flex items-center space-x-2">
|
||||
<ProjectTypeIcon project={project} />
|
||||
<span className="text-xs font-medium text-gray-600">
|
||||
{project.type || 'Environmental Project'}
|
||||
</span>
|
||||
</div>
|
||||
{project.percentage && (
|
||||
<span className="text-sm font-semibold text-gray-900">
|
||||
{(project.percentage * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-gray-600 mb-3">
|
||||
{project.description}
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
|
||||
{project.shortDescription || project.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Price per ton</span>
|
||||
<span className="font-medium text-gray-900">${project.pricePerTon}</span>
|
||||
<span className="font-medium text-gray-900">${project.pricePerTon.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 rounded-xl p-4">
|
||||
@ -265,109 +380,17 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-blue-900">Portfolio Price</span>
|
||||
<span className="font-bold text-lg text-blue-900">
|
||||
{formatCurrency(totalCost, targetCurrency)}
|
||||
{formatCurrency(offsetCost, targetCurrency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleProceedFromProjects}
|
||||
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"
|
||||
>
|
||||
Continue 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}
|
||||
onClick={handleOffsetOrder}
|
||||
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"
|
||||
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 ${
|
||||
loading ? 'opacity-50 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
@ -375,10 +398,11 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
Processing...
|
||||
</div>
|
||||
) : (
|
||||
'Place Order'
|
||||
'Confirm Offset Order'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
|
||||
@ -395,42 +419,52 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
</div>
|
||||
|
||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">
|
||||
Order Confirmed!
|
||||
{order ? 'Offset Order Successful!' : 'Request Sent!'}
|
||||
</h3>
|
||||
|
||||
<p className="text-gray-600 mb-6">
|
||||
Congratulations! You've successfully offset {tons.toFixed(2)} tons of CO₂ from your yacht emissions.
|
||||
{order
|
||||
? "Your order has been processed successfully. You'll receive a confirmation email shortly."
|
||||
: "Your offset request has been sent. Our team will contact you shortly to complete the process."
|
||||
}
|
||||
</p>
|
||||
|
||||
{order && (
|
||||
<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>
|
||||
<span className="font-mono font-medium">{order.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">CO₂ Offset:</span>
|
||||
<span className="font-medium">{tons.toFixed(2)} tons</span>
|
||||
<span className="font-medium">{order.tons} tons</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Amount Paid:</span>
|
||||
<span className="text-gray-600">Amount:</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(totalCost, targetCurrency)}
|
||||
{formatCurrency(order.amountCharged / 100, currencies[order.currency])}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-600">Email:</span>
|
||||
<span className="font-medium">{orderData.email}</span>
|
||||
<span className="text-gray-600">Portfolio:</span>
|
||||
<span className="font-medium">{order.portfolio.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.email && (
|
||||
<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.
|
||||
📧 {order
|
||||
? `A confirmation email has been sent to ${formData.email}`
|
||||
: `We'll contact you at ${formData.email}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@ -454,7 +488,7 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<motion.button
|
||||
onClick={currentStep === 'confirmation' ? onBack : () => {
|
||||
if (currentStep === 'payment') {
|
||||
if (currentStep === 'projects') {
|
||||
setCurrentStep('summary');
|
||||
} else {
|
||||
onBack();
|
||||
@ -471,7 +505,6 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<h1 className="text-lg font-bold text-gray-900">
|
||||
{currentStep === 'summary' ? 'Order Summary' :
|
||||
currentStep === 'projects' ? 'Project Portfolio' :
|
||||
currentStep === 'payment' ? 'Payment' :
|
||||
'Order Confirmed'}
|
||||
</h1>
|
||||
</div>
|
||||
@ -484,13 +517,10 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<div className="px-4 pb-3">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`flex-1 h-2 rounded-full ${
|
||||
['summary', 'projects', 'payment'].includes(currentStep) ? 'bg-blue-500' : 'bg-gray-200'
|
||||
['summary', 'projects'].includes(currentStep) ? 'bg-blue-500' : 'bg-gray-200'
|
||||
}`} />
|
||||
<div className={`flex-1 h-2 rounded-full ${
|
||||
['projects', 'payment'].includes(currentStep) ? 'bg-blue-500' : 'bg-gray-200'
|
||||
}`} />
|
||||
<div className={`flex-1 h-2 rounded-full ${
|
||||
currentStep === 'payment' ? 'bg-blue-500' : 'bg-gray-200'
|
||||
currentStep === 'projects' ? 'bg-blue-500' : 'bg-gray-200'
|
||||
}`} />
|
||||
</div>
|
||||
</div>
|
||||
@ -501,10 +531,105 @@ export function MobileOffsetOrder({ tons, monetaryAmount, currency, onBack }: Pr
|
||||
<AnimatePresence mode="wait">
|
||||
{currentStep === 'summary' && renderSummaryStep()}
|
||||
{currentStep === 'projects' && renderProjectsStep()}
|
||||
{currentStep === 'payment' && renderPaymentStep()}
|
||||
{currentStep === 'confirmation' && renderConfirmationStep()}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Lightbox Modal for Project Details */}
|
||||
<AnimatePresence>
|
||||
{selectedProject && (
|
||||
<motion.div
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center p-4"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.8)' }}
|
||||
onClick={handleCloseLightbox}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<motion.div
|
||||
className="relative bg-white rounded-lg shadow-2xl max-w-lg w-full max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
initial={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: 20 }}
|
||||
transition={{
|
||||
duration: 0.4,
|
||||
ease: [0.22, 1, 0.36, 1],
|
||||
scale: { type: "spring", stiffness: 300, damping: 30 }
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={handleCloseLightbox}
|
||||
className="absolute top-4 right-4 p-2 rounded-full bg-gray-100 hover:bg-gray-200 transition-colors z-10"
|
||||
aria-label="Close details"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
|
||||
{selectedProject.imageUrl && (
|
||||
<div className="relative h-48 overflow-hidden rounded-t-lg">
|
||||
<img
|
||||
src={selectedProject.imageUrl}
|
||||
alt={selectedProject.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||
<div className="absolute bottom-4 left-4 right-4">
|
||||
<h3 className="text-xl font-bold text-white mb-1">{selectedProject.name}</h3>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="p-6">
|
||||
{!selectedProject.imageUrl && (
|
||||
<>
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-2">{selectedProject.name}</h3>
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<ProjectTypeIcon project={selectedProject} />
|
||||
<span className="text-gray-600 text-sm">{selectedProject.type || 'Environmental Project'}</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p className="text-gray-700 mb-4 text-sm">
|
||||
{selectedProject.description || selectedProject.shortDescription}
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<p className="text-xs text-gray-600 mb-1">Price per Ton</p>
|
||||
<p className="text-lg font-bold text-gray-900">${selectedProject.pricePerTon.toFixed(2)}</p>
|
||||
</div>
|
||||
{selectedProject.percentage && (
|
||||
<div className="bg-gray-50 p-3 rounded-lg">
|
||||
<p className="text-xs text-gray-600 mb-1">Portfolio Allocation</p>
|
||||
<p className="text-lg font-bold text-gray-900">{(selectedProject.percentage * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(selectedProject.location || selectedProject.verificationStandard) && (
|
||||
<div className="space-y-2 text-sm">
|
||||
{selectedProject.location && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">Location:</span>
|
||||
<span className="font-medium text-gray-900">{selectedProject.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedProject.verificationStandard && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-gray-600">Verification:</span>
|
||||
<span className="font-medium text-gray-900">{selectedProject.verificationStandard}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user