ecom / client /src /pages /checkout.tsx
shashwatIDR's picture
Upload 147 files
b89a86e verified
import { useState } from "react";
import { useLocation } from "wouter";
import { useAuth } from "@/hooks/use-auth";
import { useCart } from "@/hooks/use-cart";
import { ordersApi } from "@/lib/api";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import Header from "@/components/layout/header";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/hooks/use-toast";
import { Loader2, CreditCard, Shield, IndianRupee, Smartphone } from "lucide-react";
import { INDIAN_STATES, PAYMENT_METHODS, DEFAULT_COUNTRY } from "@/lib/constants";
const checkoutSchema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
email: z.string().email("Valid email is required"),
phone: z.string().min(10, "Phone number is required"),
street: z.string().min(1, "Street address is required"),
city: z.string().min(1, "City is required"),
state: z.string().min(1, "State is required"),
pinCode: z.string().min(6, "PIN code is required").max(6, "PIN code must be 6 digits"),
country: z.string().min(1, "Country is required"),
paymentMethod: z.enum(["cod", "upi"], { required_error: "Please select a payment method" }),
upiId: z.string().optional(),
}).refine((data) => {
if (data.paymentMethod === "upi" && (!data.upiId || data.upiId.length === 0)) {
return false;
}
return true;
}, {
message: "UPI ID is required for UPI payment",
path: ["upiId"]
});
type CheckoutFormData = z.infer<typeof checkoutSchema>;
export default function Checkout() {
const [, setLocation] = useLocation();
const { user } = useAuth();
const { items, subtotal, tax, total, clearCart } = useCart();
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const form = useForm<CheckoutFormData>({
resolver: zodResolver(checkoutSchema),
defaultValues: {
firstName: user?.firstName || "",
lastName: user?.lastName || "",
email: user?.email || "",
phone: user?.phone || "",
street: user?.street || "",
city: user?.city || "",
state: user?.state || "",
pinCode: user?.zipCode || "",
country: user?.country || DEFAULT_COUNTRY,
paymentMethod: "cod" as const,
upiId: "",
},
});
// Redirect if not authenticated or cart is empty
if (!user) {
setLocation('/auth');
return null;
}
if (items.length === 0) {
setLocation('/');
return null;
}
const onSubmit = async (data: CheckoutFormData) => {
setIsLoading(true);
try {
const shippingAddress = `${data.street}, ${data.city}, ${data.state} ${data.pinCode}, ${data.country}`;
const orderData = {
subtotal: subtotal.toString(),
tax: tax.toString(),
shipping: "0.00",
total: total.toString(),
shippingAddress,
paymentMethod: data.paymentMethod,
items: items.map(item => ({
productId: item.productId,
quantity: item.quantity,
price: item.product?.price || "0",
})),
};
await ordersApi.create(orderData);
clearCart();
toast({
title: "Order placed successfully!",
description: "Thank you for your purchase. You will receive a confirmation email shortly.",
});
setLocation('/profile');
} catch (error) {
toast({
variant: "destructive",
title: "Order failed",
description: "There was an error processing your order. Please try again.",
});
} finally {
setIsLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50">
<Header searchQuery={searchQuery} setSearchQuery={setSearchQuery} />
<div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6 py-4" data-testid="checkout-page">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900" data-testid="checkout-title">
Checkout
</h1>
<p className="text-gray-600 mt-2">Complete your purchase</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Checkout Form */}
<div className="lg:col-span-2">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* User Details Confirmation */}
<Card>
<CardHeader>
<CardTitle>Confirm Your Details</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="bg-blue-50 p-4 rounded-lg">
<p className="text-sm text-gray-700 mb-2">
Please confirm that your details below are correct:
</p>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="font-medium">Name:</span> {user?.firstName} {user?.lastName}
</div>
<div>
<span className="font-medium">Email:</span> {user?.email}
</div>
<div>
<span className="font-medium">Phone:</span> {user?.phone}
</div>
<div>
<span className="font-medium">Address:</span> {user?.street}
</div>
<div>
<span className="font-medium">City:</span> {user?.city}
</div>
<div>
<span className="font-medium">State:</span> {user?.state}
</div>
</div>
</div>
</CardContent>
</Card>
{/* Shipping Information */}
<Card>
<CardHeader>
<CardTitle>Shipping Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input {...field} data-testid="first-name-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input {...field} data-testid="last-name-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input {...field} type="email" data-testid="email-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>Phone Number</FormLabel>
<FormControl>
<Input {...field} type="tel" data-testid="phone-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="street"
render={({ field }) => (
<FormItem>
<FormLabel>Street Address</FormLabel>
<FormControl>
<Input {...field} data-testid="street-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>City</FormLabel>
<FormControl>
<Input {...field} data-testid="city-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="state"
render={({ field }) => (
<FormItem>
<FormLabel>State</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="state-select">
<SelectValue placeholder="Select state" />
</SelectTrigger>
</FormControl>
<SelectContent className="max-h-60 overflow-y-auto">
{INDIAN_STATES.map((state) => (
<SelectItem key={state} value={state}>
{state}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="pinCode"
render={({ field }) => (
<FormItem>
<FormLabel>PIN Code</FormLabel>
<FormControl>
<Input {...field} placeholder="400001" data-testid="pin-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
</Card>
{/* Payment Information */}
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<IndianRupee className="mr-2 h-5 w-5" />
Payment Method
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="paymentMethod"
render={({ field }) => (
<FormItem>
<FormLabel>Select Payment Method</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger data-testid="payment-method-select">
<SelectValue placeholder="Choose payment method" />
</SelectTrigger>
</FormControl>
<SelectContent>
{PAYMENT_METHODS.map((method) => (
<SelectItem key={method.value} value={method.value}>
{method.label}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
{form.watch("paymentMethod") === "upi" && (
<FormField
control={form.control}
name="upiId"
render={({ field }) => (
<FormItem>
<FormLabel>UPI ID</FormLabel>
<FormControl>
<Input {...field} placeholder="yourname@paytm" data-testid="upi-input" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<div className="flex items-center space-x-2 text-sm text-gray-600 mt-4">
<Smartphone className="h-4 w-4" />
<p>
{form.watch("paymentMethod") === "cod"
? "Pay with cash when your order is delivered"
: "Secure UPI payment gateway"
}
</p>
</div>
</CardContent>
</Card>
<Button
type="submit"
className="w-full bg-blue-600 hover:bg-blue-700 text-lg py-3"
disabled={isLoading}
data-testid="place-order-button"
>
{isLoading && <Loader2 className="mr-2 h-5 w-5 animate-spin" />}
<Shield className="mr-2 h-5 w-5" />
Place Order - ₹{total.toFixed(2)}
</Button>
</form>
</Form>
</div>
{/* Order Summary */}
<div className="lg:col-span-1">
<Card className="sticky top-4">
<CardHeader>
<CardTitle>Order Summary</CardTitle>
</CardHeader>
<CardContent>
{/* Order Items */}
<div className="space-y-3 mb-4" data-testid="order-items">
{items.map((item) => (
<div key={item.id} className="flex items-center space-x-3">
<div className="w-12 h-12 bg-gray-100 rounded overflow-hidden flex-shrink-0">
{item.product?.images && item.product.images.length > 0 ? (
<img
src={item.product.images[0]}
alt={item.product.title}
className="w-full h-full object-cover"
data-testid={`order-item-image-${item.id}`}
/>
) : (
<div className="w-full h-full bg-gray-200 flex items-center justify-center">
<span className="text-xs text-gray-400">No Image</span>
</div>
)}
</div>
<div className="flex-1">
<h4 className="text-sm font-medium text-gray-900 line-clamp-1" data-testid={`order-item-title-${item.id}`}>
{item.product?.title || 'Unknown Product'}
</h4>
<p className="text-xs text-gray-500" data-testid={`order-item-seller-${item.id}`}>
{item.product?.store?.name || 'Unknown Seller'}
</p>
<div className="flex justify-between items-center mt-1">
<span className="text-xs text-gray-500" data-testid={`order-item-quantity-${item.id}`}>
Qty: {item.quantity}
</span>
<span className="text-sm font-semibold text-gray-900" data-testid={`order-item-price-${item.id}`}>
₹{item.product ? (parseFloat(item.product.price) * item.quantity).toFixed(2) : '0.00'}
</span>
</div>
</div>
</div>
))}
</div>
<Separator className="my-4" />
{/* Order Totals */}
<div className="space-y-2 mb-4" data-testid="order-totals">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Subtotal:</span>
<span className="text-gray-900" data-testid="order-subtotal">
₹{subtotal.toFixed(2)}
</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Shipping:</span>
<span className="text-green-600" data-testid="order-shipping">Free</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600">Tax:</span>
<span className="text-gray-900" data-testid="order-tax">
₹{tax.toFixed(2)}
</span>
</div>
<Separator />
<div className="flex justify-between font-semibold text-lg">
<span className="text-gray-900">Total:</span>
<span className="text-gray-900" data-testid="order-total">
₹{total.toFixed(2)}
</span>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
</div>
);
}