282 lines
10 KiB
TypeScript
282 lines
10 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Card } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Input } from '../ui/input';
|
|
import { useCart } from '../../context/CartContext';
|
|
import { CreditCard, Lock, ArrowLeft, Loader2 } from 'lucide-react';
|
|
import { useToast } from '../../context/ToastContext';
|
|
import { marketplaceService } from '../../services/marketplaceService';
|
|
import { logger } from '@/utils/logger';
|
|
|
|
interface CheckoutViewProps {
|
|
onBack: () => void;
|
|
onComplete: () => void;
|
|
}
|
|
|
|
export const CheckoutView: React.FC<CheckoutViewProps> = ({
|
|
onBack,
|
|
onComplete,
|
|
}) => {
|
|
const { cart, cartTotal, clearCart } = useCart();
|
|
const { addToast } = useToast();
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const tax = cartTotal * 0.08;
|
|
// const total = cartTotal + tax; // Calculated but not strictly needed for the API call in this mock
|
|
|
|
const [form, setForm] = useState({
|
|
fullName: '',
|
|
email: '',
|
|
address: '',
|
|
city: '',
|
|
zip: '',
|
|
country: 'United States',
|
|
saveInfo: true,
|
|
acceptTerms: false,
|
|
});
|
|
|
|
const handlePurchase = async () => {
|
|
if (!form.fullName || !form.email || !form.address) {
|
|
addToast('Please fill in all billing fields', 'error');
|
|
return;
|
|
}
|
|
if (!form.acceptTerms) {
|
|
addToast('Please accept the terms and conditions', 'error');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
// Map cart items to API format
|
|
const orderItems = cart.map((item) => ({ product_id: item.id }));
|
|
await marketplaceService.createOrder(orderItems);
|
|
|
|
addToast('Purchase successful!', 'success');
|
|
clearCart();
|
|
onComplete();
|
|
} catch (error) {
|
|
addToast('Payment failed. Please try again.', 'error');
|
|
logger.error('Payment failed', {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="animate-fadeIn max-w-5xl mx-auto pb-20">
|
|
<Button
|
|
variant="ghost"
|
|
onClick={onBack}
|
|
className="mb-6 pl-0 text-gray-400 hover:text-white"
|
|
>
|
|
<ArrowLeft className="w-4 h-4 mr-2" /> Back to Cart
|
|
</Button>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
{/* Left: Billing & Payment */}
|
|
<div className="lg:col-span-2 space-y-8">
|
|
{/* Billing Info */}
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white mb-6 border-b border-gray-700 pb-2">
|
|
Billing Information
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<Input
|
|
label="Full Name"
|
|
placeholder="John Doe"
|
|
value={form.fullName}
|
|
onChange={(e) => setForm({ ...form, fullName: e.target.value })}
|
|
/>
|
|
<Input
|
|
label="Email Address"
|
|
placeholder="john@example.com"
|
|
type="email"
|
|
value={form.email}
|
|
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
|
/>
|
|
</div>
|
|
<Input
|
|
label="Address"
|
|
placeholder="123 Studio Blvd"
|
|
value={form.address}
|
|
onChange={(e) => setForm({ ...form, address: e.target.value })}
|
|
className="mb-4"
|
|
/>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
<Input
|
|
label="City"
|
|
placeholder="New York"
|
|
value={form.city}
|
|
onChange={(e) => setForm({ ...form, city: e.target.value })}
|
|
/>
|
|
<Input
|
|
label="Zip / Postal"
|
|
placeholder="10001"
|
|
value={form.zip}
|
|
onChange={(e) => setForm({ ...form, zip: e.target.value })}
|
|
/>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-400 mb-2">
|
|
Country
|
|
</label>
|
|
<select
|
|
className="w-full bg-kodo-graphite border border-kodo-steel rounded-lg py-3 px-4 text-white focus:border-kodo-cyan outline-none appearance-none"
|
|
value={form.country}
|
|
onChange={(e) =>
|
|
setForm({ ...form, country: e.target.value })
|
|
}
|
|
>
|
|
<option>United States</option>
|
|
<option>Canada</option>
|
|
<option>United Kingdom</option>
|
|
<option>Japan</option>
|
|
<option>Germany</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
|
|
{/* Payment Method (Visual Mock) */}
|
|
<Card variant="default">
|
|
<h3 className="font-bold text-white mb-6 border-b border-gray-700 pb-2 flex items-center gap-2">
|
|
<Lock className="w-4 h-4 text-kodo-lime" /> Payment Method
|
|
</h3>
|
|
|
|
<div className="bg-kodo-ink p-4 rounded-xl border border-kodo-steel mb-4">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<CreditCard className="w-5 h-5 text-kodo-cyan" />
|
|
<span className="font-bold text-white">Credit Card</span>
|
|
<div className="flex gap-1 ml-auto">
|
|
<div className="w-8 h-5 bg-gray-700 rounded"></div>
|
|
<div className="w-8 h-5 bg-gray-700 rounded"></div>
|
|
<div className="w-8 h-5 bg-gray-700 rounded"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stripe Element Simulation */}
|
|
<div className="space-y-3">
|
|
<div className="bg-kodo-graphite border border-kodo-steel rounded px-3 py-3 text-sm text-gray-400 flex items-center justify-between">
|
|
<span>4242 4242 4242 4242</span>
|
|
<CreditCard className="w-4 h-4" />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div className="bg-kodo-graphite border border-kodo-steel rounded px-3 py-3 text-sm text-gray-400">
|
|
MM / YY
|
|
</div>
|
|
<div className="bg-kodo-graphite border border-kodo-steel rounded px-3 py-3 text-sm text-gray-400">
|
|
CVC
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="rounded border-gray-600 bg-transparent text-kodo-cyan focus:ring-0"
|
|
checked={form.saveInfo}
|
|
onChange={(e) =>
|
|
setForm({ ...form, saveInfo: e.target.checked })
|
|
}
|
|
/>
|
|
<span className="text-sm text-gray-400">
|
|
Save payment information for future purchases
|
|
</span>
|
|
</label>
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
className="rounded border-gray-600 bg-transparent text-kodo-cyan focus:ring-0"
|
|
checked={form.acceptTerms}
|
|
onChange={(e) =>
|
|
setForm({ ...form, acceptTerms: e.target.checked })
|
|
}
|
|
/>
|
|
<span className="text-sm text-gray-400">
|
|
I accept the{' '}
|
|
<span className="text-white hover:underline">
|
|
Terms of Service
|
|
</span>{' '}
|
|
and{' '}
|
|
<span className="text-white hover:underline">
|
|
Licensing Agreement
|
|
</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Right: Order Summary */}
|
|
<div className="space-y-6">
|
|
<div className="bg-kodo-ink border border-kodo-steel rounded-xl p-6 sticky top-24 shadow-2xl">
|
|
<h3 className="font-bold text-white mb-4 uppercase tracking-wider text-sm">
|
|
Order Summary
|
|
</h3>
|
|
|
|
<div className="space-y-3 max-h-60 overflow-y-auto custom-scrollbar mb-6 pr-2">
|
|
{cart.map((item) => (
|
|
<div
|
|
key={item.cartId}
|
|
className="flex justify-between items-start text-sm"
|
|
>
|
|
<div className="flex-1 pr-4">
|
|
<div className="text-gray-300 font-medium truncate">
|
|
{item.title}
|
|
</div>
|
|
<div className="text-xs text-gray-500">
|
|
{item.selectedLicense?.name || 'Standard'} License
|
|
</div>
|
|
</div>
|
|
<div className="text-white font-mono">
|
|
${(item.selectedLicense?.price || item.price).toFixed(2)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border-t border-gray-700 pt-4 space-y-2 mb-6">
|
|
<div className="flex justify-between text-sm text-gray-400">
|
|
<span>Subtotal</span>
|
|
<span>${cartTotal.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm text-gray-400">
|
|
<span>Taxes</span>
|
|
<span>${tax.toFixed(2)}</span>
|
|
</div>
|
|
<div className="flex justify-between items-end pt-2">
|
|
<span className="font-bold text-white">Total</span>
|
|
<span className="text-2xl font-mono font-bold text-kodo-cyan">
|
|
${(cartTotal + tax).toFixed(2)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="primary"
|
|
className="w-full h-12 text-base shadow-neon-cyan/20"
|
|
onClick={handlePurchase}
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<span className="flex items-center gap-2">
|
|
<Loader2 className="w-4 h-4 animate-spin" /> Processing...
|
|
</span>
|
|
) : (
|
|
'COMPLETE PURCHASE'
|
|
)}
|
|
</Button>
|
|
|
|
<div className="text-center mt-4 flex items-center justify-center gap-2 text-xs text-gray-500">
|
|
<Lock className="w-3 h-3" /> 256-bit SSL Encrypted
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|