Compare commits
1 Commits
feature/00
...
feature/00
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d43481716d |
464
src/app/checkout/page.tsx
Normal file
464
src/app/checkout/page.tsx
Normal file
@@ -0,0 +1,464 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import Header from "@/components/layout/Header";
|
||||
import Footer from "@/components/layout/Footer";
|
||||
import { useSaleorCheckoutStore } from "@/stores/saleorCheckoutStore";
|
||||
import { formatPrice } from "@/lib/saleor";
|
||||
import { saleorClient } from "@/lib/saleor/client";
|
||||
import {
|
||||
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
CHECKOUT_COMPLETE,
|
||||
} from "@/lib/saleor/mutations/Checkout";
|
||||
|
||||
interface AddressForm {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
streetAddress1: string;
|
||||
streetAddress2: string;
|
||||
city: string;
|
||||
postalCode: string;
|
||||
phone: string;
|
||||
}
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
const { checkout, refreshCheckout, getLines, getTotal } = useSaleorCheckoutStore();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [orderComplete, setOrderComplete] = useState(false);
|
||||
const [orderNumber, setOrderNumber] = useState<string | null>(null);
|
||||
|
||||
const [sameAsShipping, setSameAsShipping] = useState(true);
|
||||
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
const [billingAddress, setBillingAddress] = useState<AddressForm>({
|
||||
firstName: "",
|
||||
lastName: "",
|
||||
streetAddress1: "",
|
||||
streetAddress2: "",
|
||||
city: "",
|
||||
postalCode: "",
|
||||
phone: "",
|
||||
});
|
||||
|
||||
const lines = getLines();
|
||||
const total = getTotal();
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkout) {
|
||||
refreshCheckout();
|
||||
}
|
||||
}, [checkout, refreshCheckout]);
|
||||
|
||||
// Redirect if cart is empty
|
||||
useEffect(() => {
|
||||
if (lines.length === 0 && !orderComplete) {
|
||||
// Optionally redirect to cart or products
|
||||
// router.push("/products");
|
||||
}
|
||||
}, [lines, orderComplete, router]);
|
||||
|
||||
const handleShippingChange = (field: keyof AddressForm, value: string) => {
|
||||
setShippingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
if (sameAsShipping) {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleBillingChange = (field: keyof AddressForm, value: string) => {
|
||||
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!checkout) {
|
||||
setError("No active checkout. Please try again.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Update shipping address
|
||||
const shippingResult = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
shippingAddress: {
|
||||
...shippingAddress,
|
||||
country: "RS", // Serbia
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors?.length > 0) {
|
||||
throw new Error(shippingResult.data.checkoutShippingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
// Update billing address
|
||||
const billingResult = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
billingAddress: {
|
||||
...billingAddress,
|
||||
country: "RS",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (billingResult.data?.checkoutBillingAddressUpdate?.errors?.length > 0) {
|
||||
throw new Error(billingResult.data.checkoutBillingAddressUpdate.errors[0].message);
|
||||
}
|
||||
|
||||
// Complete checkout (creates order)
|
||||
const completeResult = await saleorClient.mutate({
|
||||
mutation: CHECKOUT_COMPLETE,
|
||||
variables: {
|
||||
checkoutId: checkout.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (completeResult.data?.checkoutComplete?.errors?.length > 0) {
|
||||
throw new Error(completeResult.data.checkoutComplete.errors[0].message);
|
||||
}
|
||||
|
||||
const order = completeResult.data?.checkoutComplete?.order;
|
||||
if (order) {
|
||||
setOrderNumber(order.number);
|
||||
setOrderComplete(true);
|
||||
} else {
|
||||
throw new Error("Failed to create order");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || "An error occurred during checkout");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Order Success Page
|
||||
if (orderComplete) {
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Header />
|
||||
<section className="pt-24 pb-20 px-4">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="mb-6">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||
<svg className="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-serif mb-2">Order Confirmed!</h1>
|
||||
<p className="text-foreground-muted">Thank you for your purchase.</p>
|
||||
</div>
|
||||
|
||||
{orderNumber && (
|
||||
<div className="bg-background-ice p-6 rounded-lg mb-6">
|
||||
<p className="text-sm text-foreground-muted mb-1">Order Number</p>
|
||||
<p className="text-2xl font-serif">#{orderNumber}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-foreground-muted mb-8">
|
||||
You will receive a confirmation email shortly. We will contact you to arrange Cash on Delivery.
|
||||
</p>
|
||||
|
||||
<Link
|
||||
href="/products"
|
||||
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
||||
>
|
||||
Continue Shopping
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen">
|
||||
<Header />
|
||||
|
||||
<section className="pt-24 pb-20 px-4">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h1 className="text-3xl font-serif mb-8">Checkout</h1>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-600 p-4 mb-6 rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
{/* Checkout Form */}
|
||||
<div>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{/* Shipping Address */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Shipping Address</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.firstName}
|
||||
onChange={(e) => handleShippingChange("firstName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.lastName}
|
||||
onChange={(e) => handleShippingChange("lastName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Street Address</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.streetAddress1}
|
||||
onChange={(e) => handleShippingChange("streetAddress1", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<input
|
||||
type="text"
|
||||
value={shippingAddress.streetAddress2}
|
||||
onChange={(e) => handleShippingChange("streetAddress2", e.target.value)}
|
||||
placeholder="Apartment, suite, etc. (optional)"
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">City</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.city}
|
||||
onChange={(e) => handleShippingChange("city", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Postal Code</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={shippingAddress.postalCode}
|
||||
onChange={(e) => handleShippingChange("postalCode", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={shippingAddress.phone}
|
||||
onChange={(e) => handleShippingChange("phone", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Billing Address Toggle */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sameAsShipping}
|
||||
onChange={(e) => setSameAsShipping(e.target.checked)}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Billing address same as shipping</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Billing Address (if different) */}
|
||||
{!sameAsShipping && (
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Billing Address</h2>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">First Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.firstName}
|
||||
onChange={(e) => handleBillingChange("firstName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Last Name</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.lastName}
|
||||
onChange={(e) => handleBillingChange("lastName", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Street Address</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.streetAddress1}
|
||||
onChange={(e) => handleBillingChange("streetAddress1", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">City</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.city}
|
||||
onChange={(e) => handleBillingChange("city", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Postal Code</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={billingAddress.postalCode}
|
||||
onChange={(e) => handleBillingChange("postalCode", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm font-medium mb-1">Phone</label>
|
||||
<input
|
||||
type="tel"
|
||||
required
|
||||
value={billingAddress.phone}
|
||||
onChange={(e) => handleBillingChange("phone", e.target.value)}
|
||||
className="w-full border border-border px-4 py-2 rounded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Payment Method */}
|
||||
<div className="border-b border-border pb-6">
|
||||
<h2 className="text-xl font-serif mb-4">Payment Method</h2>
|
||||
<div className="bg-background-ice p-4 rounded">
|
||||
<div className="flex items-center gap-3">
|
||||
<input
|
||||
type="radio"
|
||||
checked
|
||||
readOnly
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span>Cash on Delivery (COD)</span>
|
||||
</div>
|
||||
<p className="text-sm text-foreground-muted mt-2 ml-7">
|
||||
Pay when your order is delivered to your door.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading || lines.length === 0}
|
||||
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? "Processing..." : `Complete Order - ${formatPrice(total)}`}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Order Summary */}
|
||||
<div className="bg-background-ice p-6 rounded-lg h-fit">
|
||||
<h2 className="text-xl font-serif mb-6">Order Summary</h2>
|
||||
|
||||
{lines.length === 0 ? (
|
||||
<p className="text-foreground-muted">Your cart is empty</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-4 mb-6">
|
||||
{lines.map((line) => (
|
||||
<div key={line.id} className="flex gap-4">
|
||||
<div className="w-16 h-16 bg-white relative flex-shrink-0">
|
||||
{line.variant.product.media[0]?.url && (
|
||||
<Image
|
||||
src={line.variant.product.media[0].url}
|
||||
alt={line.variant.product.name}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-sm">{line.variant.product.name}</h3>
|
||||
<p className="text-foreground-muted text-sm">
|
||||
Qty: {line.quantity}
|
||||
</p>
|
||||
<p className="text-sm">
|
||||
{formatPrice(line.totalPrice.gross.amount)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4 space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-muted">Subtotal</span>
|
||||
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-foreground-muted">Shipping</span>
|
||||
<span>
|
||||
{checkout?.shippingPrice?.gross?.amount
|
||||
? formatPrice(checkout.shippingPrice.gross.amount)
|
||||
: "Calculated"
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
|
||||
<span>Total</span>
|
||||
<span>{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Footer />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user