- Fix shipping cost not included in checkout total - Add useShippingMethodSelector hook for proper abstraction - Remove blocking initCheckout from Header for better performance - Checkout now initializes lazily when cart opens or item added
702 lines
28 KiB
TypeScript
702 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { useTranslations, useLocale } from "next-intl";
|
|
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 { useAnalytics } from "@/lib/analytics";
|
|
import {
|
|
CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
|
} from "@/lib/saleor/mutations/Checkout";
|
|
import { PaymentSection } from "./components/PaymentSection";
|
|
import { DEFAULT_PAYMENT_METHOD } from "@/lib/config/paymentMethods";
|
|
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
|
|
import type { Checkout } from "@/types/saleor";
|
|
import { createCheckoutService, type Address } from "@/lib/services/checkoutService";
|
|
import { useShippingMethodSelector } from "@/lib/hooks/useShippingMethodSelector";
|
|
|
|
interface ShippingAddressUpdateResponse {
|
|
checkoutShippingAddressUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutQueryResponse {
|
|
checkout?: Checkout;
|
|
}
|
|
|
|
|
|
|
|
interface ShippingMethod {
|
|
id: string;
|
|
name: string;
|
|
price: {
|
|
amount: number;
|
|
currency: string;
|
|
};
|
|
}
|
|
|
|
interface AddressForm {
|
|
firstName: string;
|
|
lastName: string;
|
|
streetAddress1: string;
|
|
streetAddress2: string;
|
|
city: string;
|
|
postalCode: string;
|
|
country: string;
|
|
phone: string;
|
|
email: string;
|
|
}
|
|
|
|
export default function CheckoutPage() {
|
|
const t = useTranslations("Checkout");
|
|
const locale = useLocale();
|
|
const router = useRouter();
|
|
const { checkout, refreshCheckout, clearCheckout, getLines, getTotal } = useSaleorCheckoutStore();
|
|
const { trackCheckoutStarted, trackCheckoutStep, trackOrderCompleted, identifyUser } = useAnalytics();
|
|
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 [selectedPaymentMethod, setSelectedPaymentMethod] = useState<string>(DEFAULT_PAYMENT_METHOD);
|
|
const [shippingAddress, setShippingAddress] = useState<AddressForm>({
|
|
firstName: "",
|
|
lastName: "",
|
|
streetAddress1: "",
|
|
streetAddress2: "",
|
|
city: "",
|
|
postalCode: "",
|
|
country: "RS",
|
|
phone: "",
|
|
email: "",
|
|
});
|
|
const [billingAddress, setBillingAddress] = useState<AddressForm>({
|
|
firstName: "",
|
|
lastName: "",
|
|
streetAddress1: "",
|
|
streetAddress2: "",
|
|
city: "",
|
|
postalCode: "",
|
|
country: "RS",
|
|
phone: "",
|
|
email: "",
|
|
});
|
|
|
|
const [shippingMethods, setShippingMethods] = useState<ShippingMethod[]>([]);
|
|
const [selectedShippingMethod, setSelectedShippingMethod] = useState<string>("");
|
|
const [isLoadingShipping, setIsLoadingShipping] = useState(false);
|
|
|
|
// Hook to manage shipping method selection (both manual and auto)
|
|
const { selectShippingMethodWithApi } = useShippingMethodSelector({
|
|
checkoutId: checkout?.id ?? null,
|
|
onSelect: setSelectedShippingMethod,
|
|
onRefresh: refreshCheckout,
|
|
});
|
|
|
|
const lines = getLines();
|
|
// Use checkout.totalPrice directly for reactive updates when shipping method changes
|
|
const total = checkout?.totalPrice?.gross?.amount || getTotal();
|
|
|
|
// Debounced shipping method fetching
|
|
useEffect(() => {
|
|
if (!checkout) return;
|
|
|
|
// Check if address is complete enough to fetch shipping methods
|
|
const isAddressComplete =
|
|
shippingAddress.firstName &&
|
|
shippingAddress.lastName &&
|
|
shippingAddress.streetAddress1 &&
|
|
shippingAddress.city &&
|
|
shippingAddress.postalCode &&
|
|
shippingAddress.country;
|
|
|
|
if (!isAddressComplete) {
|
|
setShippingMethods([]);
|
|
return;
|
|
}
|
|
|
|
const timer = setTimeout(async () => {
|
|
setIsLoadingShipping(true);
|
|
try {
|
|
console.log("Fetching shipping methods...");
|
|
|
|
// First update the shipping address
|
|
await saleorClient.mutate<ShippingAddressUpdateResponse>({
|
|
mutation: CHECKOUT_SHIPPING_ADDRESS_UPDATE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
shippingAddress: {
|
|
firstName: shippingAddress.firstName,
|
|
lastName: shippingAddress.lastName,
|
|
streetAddress1: shippingAddress.streetAddress1,
|
|
streetAddress2: shippingAddress.streetAddress2,
|
|
city: shippingAddress.city,
|
|
postalCode: shippingAddress.postalCode,
|
|
country: shippingAddress.country,
|
|
phone: shippingAddress.phone,
|
|
},
|
|
},
|
|
});
|
|
|
|
// Then query for shipping methods
|
|
const checkoutQueryResult = await saleorClient.query<CheckoutQueryResponse>({
|
|
query: GET_CHECKOUT_BY_ID,
|
|
variables: { id: checkout.id },
|
|
fetchPolicy: "network-only",
|
|
});
|
|
|
|
const availableMethods = checkoutQueryResult.data?.checkout?.shippingMethods || [];
|
|
console.log("Available shipping methods:", availableMethods);
|
|
|
|
setShippingMethods(availableMethods);
|
|
|
|
// Auto-select first method if none selected
|
|
if (availableMethods.length > 0 && !selectedShippingMethod) {
|
|
const firstMethodId = availableMethods[0].id;
|
|
// Use the hook to both update UI and call API
|
|
await selectShippingMethodWithApi(firstMethodId);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error fetching shipping methods:", err);
|
|
} finally {
|
|
setIsLoadingShipping(false);
|
|
}
|
|
}, 500); // 500ms debounce
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [checkout, shippingAddress]);
|
|
|
|
useEffect(() => {
|
|
if (!checkout) {
|
|
refreshCheckout();
|
|
}
|
|
}, [checkout, refreshCheckout]);
|
|
|
|
// Track checkout started when page loads
|
|
useEffect(() => {
|
|
if (checkout) {
|
|
const lines = getLines();
|
|
const total = getTotal();
|
|
trackCheckoutStarted({
|
|
total,
|
|
currency: "RSD",
|
|
item_count: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
items: lines.map(line => ({
|
|
id: line.variant.id,
|
|
name: line.variant.product.name,
|
|
quantity: line.quantity,
|
|
price: line.variant.pricing?.price?.gross?.amount || 0,
|
|
currency: line.variant.pricing?.price?.gross?.currency || "RSD",
|
|
})),
|
|
});
|
|
}
|
|
}, [checkout]);
|
|
|
|
// Scroll to top when order is complete
|
|
useEffect(() => {
|
|
if (orderComplete) {
|
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
}, [orderComplete]);
|
|
|
|
const handleShippingChange = (field: keyof AddressForm, value: string) => {
|
|
setShippingAddress((prev) => ({ ...prev, [field]: value }));
|
|
if (sameAsShipping && field !== "email") {
|
|
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
|
}
|
|
};
|
|
|
|
const handleBillingChange = (field: keyof AddressForm, value: string) => {
|
|
setBillingAddress((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleEmailChange = (value: string) => {
|
|
setShippingAddress((prev) => ({ ...prev, email: value }));
|
|
};
|
|
|
|
const handleShippingMethodSelect = async (methodId: string) => {
|
|
await selectShippingMethodWithApi(methodId);
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!checkout) {
|
|
setError(t("errorNoCheckout"));
|
|
return;
|
|
}
|
|
|
|
// Validate all required fields
|
|
if (!shippingAddress.email || !shippingAddress.email.includes("@")) {
|
|
setError(t("errorEmailRequired"));
|
|
return;
|
|
}
|
|
|
|
if (!shippingAddress.phone || shippingAddress.phone.length < 8) {
|
|
setError(t("errorPhoneRequired"));
|
|
return;
|
|
}
|
|
|
|
if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode) {
|
|
setError(t("errorFieldsRequired"));
|
|
return;
|
|
}
|
|
|
|
if (!selectedShippingMethod) {
|
|
setError(t("errorSelectShipping"));
|
|
return;
|
|
}
|
|
|
|
if (!selectedPaymentMethod) {
|
|
setError(t("errorSelectPayment"));
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
console.log("Completing order via CheckoutService...");
|
|
|
|
// Create checkout service instance
|
|
const checkoutService = createCheckoutService(checkout.id);
|
|
|
|
// Transform form data to service types
|
|
const serviceShippingAddress: Address = {
|
|
firstName: shippingAddress.firstName,
|
|
lastName: shippingAddress.lastName,
|
|
streetAddress1: shippingAddress.streetAddress1,
|
|
streetAddress2: shippingAddress.streetAddress2,
|
|
city: shippingAddress.city,
|
|
postalCode: shippingAddress.postalCode,
|
|
country: shippingAddress.country,
|
|
phone: shippingAddress.phone,
|
|
};
|
|
|
|
const serviceBillingAddress: Address = {
|
|
firstName: billingAddress.firstName,
|
|
lastName: billingAddress.lastName,
|
|
streetAddress1: billingAddress.streetAddress1,
|
|
streetAddress2: billingAddress.streetAddress2,
|
|
city: billingAddress.city,
|
|
postalCode: billingAddress.postalCode,
|
|
country: billingAddress.country,
|
|
phone: billingAddress.phone,
|
|
};
|
|
|
|
// Execute checkout pipeline
|
|
const result = await checkoutService.execute({
|
|
email: shippingAddress.email,
|
|
shippingAddress: serviceShippingAddress,
|
|
billingAddress: serviceBillingAddress,
|
|
shippingMethodId: selectedShippingMethod,
|
|
languageCode: locale.toUpperCase(),
|
|
metadata: {
|
|
phone: shippingAddress.phone,
|
|
shippingPhone: shippingAddress.phone,
|
|
userLanguage: locale,
|
|
userLocale: locale,
|
|
},
|
|
});
|
|
|
|
if (!result.success || !result.order) {
|
|
// Handle specific error types
|
|
if (result.error === "CHECKOUT_EXPIRED") {
|
|
console.error("Checkout not found, clearing cart...");
|
|
localStorage.removeItem('cart');
|
|
localStorage.removeItem('checkoutId');
|
|
window.location.href = `/${locale}/products`;
|
|
return;
|
|
}
|
|
throw new Error(result.error || t("errorCreatingOrder"));
|
|
}
|
|
|
|
// Success!
|
|
setOrderNumber(result.order.number);
|
|
setOrderComplete(true);
|
|
|
|
// Track order completion BEFORE clearing checkout
|
|
const lines = getLines();
|
|
const total = getTotal();
|
|
console.log("[Checkout] Order total before tracking:", total, "RSD");
|
|
trackOrderCompleted({
|
|
order_id: checkout.id,
|
|
order_number: result.order.number,
|
|
total,
|
|
currency: "RSD",
|
|
item_count: lines.reduce((sum, line) => sum + line.quantity, 0),
|
|
shipping_cost: shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount,
|
|
customer_email: shippingAddress.email,
|
|
});
|
|
|
|
// Clear the checkout/cart from the store
|
|
clearCheckout();
|
|
|
|
// Identify the user
|
|
identifyUser({
|
|
profileId: shippingAddress.email,
|
|
email: shippingAddress.email,
|
|
firstName: shippingAddress.firstName,
|
|
lastName: shippingAddress.lastName,
|
|
});
|
|
|
|
console.log("Order completed successfully:", result.order.number);
|
|
|
|
} catch (err: unknown) {
|
|
console.error("Checkout error:", err);
|
|
|
|
if (err instanceof Error) {
|
|
if (err.name === "AbortError") {
|
|
setError("Request timed out. Please check your connection and try again.");
|
|
} else {
|
|
setError(err.message || t("errorOccurred"));
|
|
}
|
|
} else {
|
|
setError(t("errorOccurred"));
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (orderComplete) {
|
|
return (
|
|
<>
|
|
<Header locale={locale} />
|
|
<main className="min-h-screen">
|
|
<section className="pt-[120px] 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">{t("orderConfirmed")}</h1>
|
|
<p className="text-foreground-muted">{t("thankYou")}</p>
|
|
</div>
|
|
|
|
{orderNumber && (
|
|
<div className="bg-background-ice p-6 rounded-lg mb-6">
|
|
<p className="text-sm text-foreground-muted mb-1">{t("orderNumber")}</p>
|
|
<p className="text-2xl font-serif">#{orderNumber}</p>
|
|
</div>
|
|
)}
|
|
|
|
<p className="text-foreground-muted mb-8">
|
|
{t("confirmationEmail")}
|
|
</p>
|
|
|
|
<Link
|
|
href={`/${locale}/products`}
|
|
className="inline-block px-8 py-3 bg-foreground text-white hover:bg-accent-dark transition-colors"
|
|
>
|
|
{t("continueShoppingBtn")}
|
|
</Link>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
<div className="pt-16">
|
|
<Footer locale={locale} />
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<Header locale={locale} />
|
|
<main className="min-h-screen">
|
|
<section className="pt-[120px] pb-20 px-4">
|
|
<div className="max-w-7xl mx-auto">
|
|
<h1 className="text-3xl font-serif mb-8">{t("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">
|
|
<div>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
<div className="border-b border-border pb-6">
|
|
<h2 className="text-xl font-serif mb-4">{t("contactInfo")}</h2>
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">{t("email")}</label>
|
|
<input
|
|
type="email"
|
|
required
|
|
value={shippingAddress.email}
|
|
onChange={(e) => handleEmailChange(e.target.value)}
|
|
className="w-full border border-border px-4 py-2 rounded"
|
|
placeholder="email@example.com"
|
|
/>
|
|
<p className="text-xs text-foreground-muted mt-1">{t("emailRequired")}</p>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">{t("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"
|
|
placeholder="+381..."
|
|
/>
|
|
<p className="text-xs text-foreground-muted mt-1">{t("phoneRequired")}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-b border-border pb-6">
|
|
<h2 className="text-xl font-serif mb-4">{t("shippingAddress")}</h2>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">{t("firstName")}</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">{t("lastName")}</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">{t("country")}</label>
|
|
<select
|
|
required
|
|
value={shippingAddress.country}
|
|
onChange={(e) => handleShippingChange("country", e.target.value)}
|
|
className="w-full border border-border px-4 py-2 rounded"
|
|
>
|
|
<option value="RS">Serbia (Srbija)</option>
|
|
<option value="BA">Bosnia and Herzegovina</option>
|
|
<option value="ME">Montenegro</option>
|
|
<option value="HR">Croatia</option>
|
|
<option value="SI">Slovenia</option>
|
|
<option value="MK">North Macedonia</option>
|
|
<option value="AL">Albania</option>
|
|
<option value="XK">Kosovo</option>
|
|
<option value="BG">Bulgaria</option>
|
|
<option value="RO">Romania</option>
|
|
<option value="HU">Hungary</option>
|
|
<option value="DE">Germany</option>
|
|
<option value="AT">Austria</option>
|
|
<option value="CH">Switzerland</option>
|
|
<option value="FR">France</option>
|
|
<option value="GB">United Kingdom</option>
|
|
<option value="US">United States</option>
|
|
<option value="CA">Canada</option>
|
|
<option value="AU">Australia</option>
|
|
</select>
|
|
</div>
|
|
<div className="col-span-2">
|
|
<label className="block text-sm font-medium mb-1">{t("streetAddress")}</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={t("streetAddressOptional")}
|
|
className="w-full border border-border px-4 py-2 rounded"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium mb-1">{t("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">{t("postalCode")}</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>
|
|
</div>
|
|
|
|
<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>{t("billingAddressSame")}</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Shipping Method Selection */}
|
|
<div className="border-b border-border pb-6">
|
|
<h2 className="text-xl font-serif mb-4">{t("shippingMethod")}</h2>
|
|
{isLoadingShipping ? (
|
|
<div className="flex items-center gap-2 text-foreground-muted">
|
|
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<span>{t("loadingShippingMethods")}</span>
|
|
</div>
|
|
) : shippingMethods.length > 0 ? (
|
|
<div className="space-y-3">
|
|
{shippingMethods.map((method) => (
|
|
<label
|
|
key={method.id}
|
|
className={`flex items-center justify-between p-4 border rounded cursor-pointer transition-colors ${
|
|
selectedShippingMethod === method.id
|
|
? "border-foreground bg-background-ice"
|
|
: "border-border hover:border-foreground/50"
|
|
}`}
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<input
|
|
type="radio"
|
|
name="shippingMethod"
|
|
value={method.id}
|
|
checked={selectedShippingMethod === method.id}
|
|
onChange={(e) => handleShippingMethodSelect(e.target.value)}
|
|
className="w-4 h-4"
|
|
/>
|
|
<span className="font-medium">{method.name}</span>
|
|
</div>
|
|
<span className="text-foreground-muted">
|
|
{formatPrice(method.price.amount)}
|
|
</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-foreground-muted">{t("enterAddressForShipping")}</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Payment Method Section */}
|
|
<PaymentSection
|
|
selectedMethodId={selectedPaymentMethod}
|
|
onSelectMethod={setSelectedPaymentMethod}
|
|
locale={locale}
|
|
channel="default-channel"
|
|
disabled={isLoading}
|
|
/>
|
|
|
|
{/* Money Back Guarantee Trust Badge */}
|
|
<div className="flex items-center justify-center gap-2 py-3 px-4 bg-green-50 rounded-lg border border-green-100">
|
|
<svg className="w-5 h-5 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
</svg>
|
|
<span className="text-sm font-medium text-green-800">{t("moneyBackGuarantee")}</span>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || lines.length === 0 || !selectedShippingMethod}
|
|
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
|
>
|
|
{isLoading ? t("processing") : t("completeOrder", { total: formatPrice(total) })}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div className="bg-background-ice p-6 rounded-lg h-fit">
|
|
<h2 className="text-xl font-serif mb-6">{t("orderSummary")}</h2>
|
|
|
|
{lines.length === 0 ? (
|
|
<p className="text-foreground-muted">{t("yourCartEmpty")}</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
|
|
sizes="64px"
|
|
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">
|
|
{t("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">{t("subtotal")}</span>
|
|
<span>{formatPrice(checkout?.subtotalPrice?.gross?.amount || 0)}</span>
|
|
</div>
|
|
{selectedShippingMethod && (
|
|
<div className="flex justify-between">
|
|
<span className="text-foreground-muted">{t("shipping")}</span>
|
|
<span>{formatPrice(shippingMethods.find(m => m.id === selectedShippingMethod)?.price.amount || 0)}</span>
|
|
</div>
|
|
)}
|
|
<div className="flex justify-between font-medium text-lg pt-2 border-t border-border">
|
|
<span>{t("total")}</span>
|
|
<span>{formatPrice(total)}</span>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
<div className="pt-16">
|
|
<Footer locale={locale} />
|
|
</div>
|
|
</>
|
|
);
|
|
} |