Add comprehensive OpenPanel analytics tracking: - Install @openpanel/nextjs SDK - Add OpenPanelComponent to root layout for automatic page views - Create useAnalytics hook for tracking custom events - Track checkout funnel: started, shipping step, order completed - Track product views and add-to-cart events - Identify users on order completion - Add NEXT_PUBLIC_OPENPANEL_CLIENT_ID to environment
701 lines
28 KiB
TypeScript
701 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,
|
|
CHECKOUT_BILLING_ADDRESS_UPDATE,
|
|
CHECKOUT_COMPLETE,
|
|
CHECKOUT_EMAIL_UPDATE,
|
|
CHECKOUT_METADATA_UPDATE,
|
|
CHECKOUT_SHIPPING_METHOD_UPDATE,
|
|
} from "@/lib/saleor/mutations/Checkout";
|
|
import { GET_CHECKOUT_BY_ID } from "@/lib/saleor/queries/Checkout";
|
|
import type { Checkout } from "@/types/saleor";
|
|
|
|
interface ShippingAddressUpdateResponse {
|
|
checkoutShippingAddressUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface BillingAddressUpdateResponse {
|
|
checkoutBillingAddressUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface CheckoutCompleteResponse {
|
|
checkoutComplete?: {
|
|
order?: { number: string };
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface EmailUpdateResponse {
|
|
checkoutEmailUpdate?: {
|
|
checkout?: Checkout;
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface MetadataUpdateResponse {
|
|
updateMetadata?: {
|
|
item?: {
|
|
id: string;
|
|
metadata?: Array<{ key: string; value: string }>;
|
|
};
|
|
errors?: Array<{ message: string }>;
|
|
};
|
|
}
|
|
|
|
interface ShippingMethodUpdateResponse {
|
|
checkoutShippingMethodUpdate?: {
|
|
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, 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 [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 [showShippingMethods, setShowShippingMethods] = useState(false);
|
|
|
|
const lines = getLines();
|
|
const total = getTotal();
|
|
|
|
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,
|
|
})),
|
|
});
|
|
}
|
|
}, [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 handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
if (!checkout) {
|
|
setError(t("errorNoCheckout"));
|
|
return;
|
|
}
|
|
|
|
if (!shippingAddress.email || !shippingAddress.email.includes("@")) {
|
|
setError(t("errorEmailRequired"));
|
|
return;
|
|
}
|
|
|
|
if (!shippingAddress.firstName || !shippingAddress.lastName || !shippingAddress.streetAddress1 || !shippingAddress.city || !shippingAddress.postalCode || !shippingAddress.phone) {
|
|
setError(t("errorFieldsRequired"));
|
|
return;
|
|
}
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// If we're showing shipping methods and one is selected, complete the order
|
|
if (showShippingMethods && selectedShippingMethod) {
|
|
console.log("Phase 2: Completing order with shipping method...");
|
|
|
|
console.log("Step 1: Updating billing address...");
|
|
const billingResult = await saleorClient.mutate<BillingAddressUpdateResponse>({
|
|
mutation: CHECKOUT_BILLING_ADDRESS_UPDATE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
billingAddress: {
|
|
firstName: billingAddress.firstName,
|
|
lastName: billingAddress.lastName,
|
|
streetAddress1: billingAddress.streetAddress1,
|
|
streetAddress2: billingAddress.streetAddress2,
|
|
city: billingAddress.city,
|
|
postalCode: billingAddress.postalCode,
|
|
country: billingAddress.country,
|
|
phone: billingAddress.phone,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (billingResult.data?.checkoutBillingAddressUpdate?.errors && billingResult.data.checkoutBillingAddressUpdate.errors.length > 0) {
|
|
throw new Error(`Billing address update failed: ${billingResult.data.checkoutBillingAddressUpdate.errors[0].message}`);
|
|
}
|
|
console.log("Step 1: Billing address updated successfully");
|
|
|
|
console.log("Step 2: Setting shipping method...");
|
|
const shippingMethodResult = await saleorClient.mutate<ShippingMethodUpdateResponse>({
|
|
mutation: CHECKOUT_SHIPPING_METHOD_UPDATE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
shippingMethodId: selectedShippingMethod,
|
|
},
|
|
});
|
|
|
|
if (shippingMethodResult.data?.checkoutShippingMethodUpdate?.errors && shippingMethodResult.data.checkoutShippingMethodUpdate.errors.length > 0) {
|
|
throw new Error(`Shipping method update failed: ${shippingMethodResult.data.checkoutShippingMethodUpdate.errors[0].message}`);
|
|
}
|
|
console.log("Step 2: Shipping method set successfully");
|
|
|
|
console.log("Step 3: Saving phone number...");
|
|
const metadataResult = await saleorClient.mutate<MetadataUpdateResponse>({
|
|
mutation: CHECKOUT_METADATA_UPDATE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
metadata: [
|
|
{ key: "phone", value: shippingAddress.phone },
|
|
{ key: "shippingPhone", value: shippingAddress.phone },
|
|
],
|
|
},
|
|
});
|
|
|
|
if (metadataResult.data?.updateMetadata?.errors && metadataResult.data.updateMetadata.errors.length > 0) {
|
|
console.warn("Failed to save phone metadata:", metadataResult.data.updateMetadata.errors);
|
|
} else {
|
|
console.log("Step 3: Phone number saved successfully");
|
|
}
|
|
|
|
console.log("Step 4: Completing checkout...");
|
|
const completeResult = await saleorClient.mutate<CheckoutCompleteResponse>({
|
|
mutation: CHECKOUT_COMPLETE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
},
|
|
});
|
|
|
|
if (completeResult.data?.checkoutComplete?.errors && 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);
|
|
|
|
// Track order completion
|
|
const lines = getLines();
|
|
const total = getTotal();
|
|
trackOrderCompleted({
|
|
order_id: checkout.id,
|
|
order_number: 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,
|
|
});
|
|
|
|
// Identify the user
|
|
identifyUser({
|
|
profileId: shippingAddress.email,
|
|
email: shippingAddress.email,
|
|
firstName: shippingAddress.firstName,
|
|
lastName: shippingAddress.lastName,
|
|
});
|
|
} else {
|
|
throw new Error(t("errorCreatingOrder"));
|
|
}
|
|
} else {
|
|
// Phase 1: Update email and address, then fetch shipping methods
|
|
console.log("Phase 1: Updating email and address...");
|
|
|
|
console.log("Step 1: Updating email...");
|
|
const emailResult = await saleorClient.mutate<EmailUpdateResponse>({
|
|
mutation: CHECKOUT_EMAIL_UPDATE,
|
|
variables: {
|
|
checkoutId: checkout.id,
|
|
email: shippingAddress.email,
|
|
},
|
|
});
|
|
|
|
if (emailResult.data?.checkoutEmailUpdate?.errors && emailResult.data.checkoutEmailUpdate.errors.length > 0) {
|
|
throw new Error(`Email update failed: ${emailResult.data.checkoutEmailUpdate.errors[0].message}`);
|
|
}
|
|
console.log("Step 1: Email updated successfully");
|
|
|
|
console.log("Step 2: Updating shipping address...");
|
|
console.log("Shipping address data:", {
|
|
firstName: shippingAddress.firstName,
|
|
lastName: shippingAddress.lastName,
|
|
streetAddress1: shippingAddress.streetAddress1,
|
|
city: shippingAddress.city,
|
|
postalCode: shippingAddress.postalCode,
|
|
country: shippingAddress.country,
|
|
phone: shippingAddress.phone,
|
|
});
|
|
const shippingResult = 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,
|
|
},
|
|
},
|
|
});
|
|
|
|
if (shippingResult.data?.checkoutShippingAddressUpdate?.errors && shippingResult.data.checkoutShippingAddressUpdate.errors.length > 0) {
|
|
throw new Error(`Shipping address update failed: ${shippingResult.data.checkoutShippingAddressUpdate.errors[0].message}`);
|
|
}
|
|
console.log("Step 2: Shipping address updated successfully");
|
|
|
|
// Query for checkout to get available shipping methods
|
|
console.log("Step 3: Fetching 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);
|
|
|
|
if (availableMethods.length === 0) {
|
|
throw new Error(t("errorNoShippingMethods"));
|
|
}
|
|
|
|
setShippingMethods(availableMethods);
|
|
setShowShippingMethods(true);
|
|
|
|
// Track shipping step
|
|
trackCheckoutStep("shipping_method_selection", {
|
|
available_methods_count: availableMethods.length,
|
|
});
|
|
|
|
// Don't complete yet - show shipping method selection
|
|
console.log("Phase 1 complete. Waiting for shipping method selection...");
|
|
}
|
|
} 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 */}
|
|
{showShippingMethods && shippingMethods.length > 0 && (
|
|
<div className="border-b border-border pb-6">
|
|
<h2 className="text-xl font-serif mb-4">{t("shippingMethod")}</h2>
|
|
<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) => setSelectedShippingMethod(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>
|
|
{!selectedShippingMethod && (
|
|
<p className="text-red-500 text-sm mt-2">{t("errorSelectShipping")}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={isLoading || lines.length === 0 || (showShippingMethods && !selectedShippingMethod)}
|
|
className="w-full py-4 bg-foreground text-white font-medium hover:bg-accent-dark transition-colors disabled:opacity-50"
|
|
>
|
|
{isLoading ? t("processing") : showShippingMethods ? t("completeOrder", { total: formatPrice(total) }) : t("continueToShipping")}
|
|
</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>
|
|
<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>
|
|
</>
|
|
);
|
|
} |