ecommerce 2/3
This commit is contained in:
parent
1b34cb5922
commit
159e6e7147
@ -7,6 +7,14 @@ export interface ShippingAddress {
|
||||
zip: string;
|
||||
country: string;
|
||||
}
|
||||
/** A saved address with extra metadata (matches pm-pics SavedShippingAddress). */
|
||||
export interface SavedAddress extends ShippingAddress {
|
||||
id: string;
|
||||
label?: string;
|
||||
isDefault?: boolean;
|
||||
phone?: string;
|
||||
note?: string;
|
||||
}
|
||||
export type PaymentMethod = "shopify" | "crypto";
|
||||
export interface CheckoutPageProps {
|
||||
/** Called when user submits the checkout form. */
|
||||
@ -18,6 +26,8 @@ export interface CheckoutPageProps {
|
||||
onBackToCart?: () => void;
|
||||
/** Pre-fill shipping form fields (e.g. from user profile). */
|
||||
initialShipping?: Partial<ShippingAddress>;
|
||||
/** Saved shipping addresses — enables address selector. */
|
||||
savedAddresses?: SavedAddress[];
|
||||
/** Pre-filled tax amount, if known. */
|
||||
tax?: number;
|
||||
/** Pre-filled shipping cost. */
|
||||
@ -29,4 +39,4 @@ export interface CheckoutPageProps {
|
||||
* Checkout page — two-column layout with shipping form + payment selector on
|
||||
* the left and an OrderSummary on the right.
|
||||
*/
|
||||
export declare function CheckoutPage({ onPlaceOrder, onBackToCart, initialShipping, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;
|
||||
export declare function CheckoutPage({ onPlaceOrder, onBackToCart, initialShipping, savedAddresses, tax, shipping, className, }: CheckoutPageProps): import("react/jsx-runtime").JSX.Element;
|
||||
|
||||
2
packages/ecommerce/dist-lib/lib-export.d.ts
vendored
2
packages/ecommerce/dist-lib/lib-export.d.ts
vendored
@ -7,7 +7,7 @@ export type { CartItem, CartActions, CartState } from './cart/types';
|
||||
export { OrderSummary } from './checkout/OrderSummary';
|
||||
export type { OrderSummaryProps } from './checkout/OrderSummary';
|
||||
export { CheckoutPage } from './checkout/CheckoutPage';
|
||||
export type { CheckoutPageProps, ShippingAddress, PaymentMethod, } from './checkout/CheckoutPage';
|
||||
export type { CheckoutPageProps, ShippingAddress, SavedAddress, PaymentMethod, } from './checkout/CheckoutPage';
|
||||
export { PolicyPage } from './policies/PolicyPage';
|
||||
export type { PolicyPageProps } from './policies/PolicyPage';
|
||||
export { ShippingPage } from './policies/ShippingPage';
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -44,7 +44,7 @@ export function CartPage({ onCheckout, className }: CartPageProps) {
|
||||
|
||||
/* ---------- Cart with items ---------- */
|
||||
return (
|
||||
<div className={cn("mx-auto max-w-3xl space-y-6", className)}>
|
||||
<div className={cn("mx-auto max-w-3xl space-y-6 px-4 py-8", className)}>
|
||||
<Card>
|
||||
<CardHeader className="flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="flex items-center gap-2 text-xl">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import { CreditCard, Bitcoin, ShoppingBag } from "lucide-react";
|
||||
import { CreditCard, Bitcoin, ShoppingBag, MapPin, Save } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
@ -20,6 +20,15 @@ export interface ShippingAddress {
|
||||
country: string;
|
||||
}
|
||||
|
||||
/** A saved address with extra metadata (matches pm-pics SavedShippingAddress). */
|
||||
export interface SavedAddress extends ShippingAddress {
|
||||
id: string;
|
||||
label?: string;
|
||||
isDefault?: boolean;
|
||||
phone?: string;
|
||||
note?: string;
|
||||
}
|
||||
|
||||
export type PaymentMethod = "shopify" | "crypto";
|
||||
|
||||
export interface CheckoutPageProps {
|
||||
@ -28,10 +37,14 @@ export interface CheckoutPageProps {
|
||||
shipping: ShippingAddress;
|
||||
paymentMethod: PaymentMethod;
|
||||
}) => void;
|
||||
/** Called when user wants to save the entered address to their profile. */
|
||||
onSaveAddress?: (address: ShippingAddress) => void;
|
||||
/** Called when user clicks "Back to Cart". */
|
||||
onBackToCart?: () => void;
|
||||
/** Pre-fill shipping form fields (e.g. from user profile). */
|
||||
initialShipping?: Partial<ShippingAddress>;
|
||||
/** Saved shipping addresses — enables address selector. */
|
||||
savedAddresses?: SavedAddress[];
|
||||
/** Pre-filled tax amount, if known. */
|
||||
tax?: number;
|
||||
/** Pre-filled shipping cost. */
|
||||
@ -46,8 +59,10 @@ export interface CheckoutPageProps {
|
||||
*/
|
||||
export function CheckoutPage({
|
||||
onPlaceOrder,
|
||||
onSaveAddress,
|
||||
onBackToCart,
|
||||
initialShipping,
|
||||
savedAddresses,
|
||||
tax,
|
||||
shipping,
|
||||
className,
|
||||
@ -55,20 +70,51 @@ export function CheckoutPage({
|
||||
const itemCount = useCartStore((s) => s.itemCount);
|
||||
|
||||
const [paymentMethod, setPaymentMethod] = useState<PaymentMethod>("shopify");
|
||||
const [form, setForm] = useState<ShippingAddress>({
|
||||
fullName: initialShipping?.fullName ?? "",
|
||||
email: initialShipping?.email ?? "",
|
||||
address: initialShipping?.address ?? "",
|
||||
city: initialShipping?.city ?? "",
|
||||
zip: initialShipping?.zip ?? "",
|
||||
country: initialShipping?.country ?? "",
|
||||
const [saveAddress, setSaveAddress] = useState(false);
|
||||
|
||||
// Find default address or first address from savedAddresses
|
||||
const defaultAddress = savedAddresses?.find((a) => a.isDefault) ?? savedAddresses?.[0];
|
||||
|
||||
// Determine initial form values: saved default > initialShipping > empty
|
||||
const resolveInitial = (): ShippingAddress => ({
|
||||
fullName: defaultAddress?.fullName ?? initialShipping?.fullName ?? "",
|
||||
email: defaultAddress?.email ?? initialShipping?.email ?? "",
|
||||
address: defaultAddress?.address ?? initialShipping?.address ?? "",
|
||||
city: defaultAddress?.city ?? initialShipping?.city ?? "",
|
||||
zip: defaultAddress?.zip ?? initialShipping?.zip ?? "",
|
||||
country: defaultAddress?.country ?? initialShipping?.country ?? "",
|
||||
});
|
||||
|
||||
const [form, setForm] = useState<ShippingAddress>(resolveInitial);
|
||||
const [selectedAddressId, setSelectedAddressId] = useState<string>(
|
||||
defaultAddress?.id ?? "custom"
|
||||
);
|
||||
|
||||
// Sync form when a saved address is selected
|
||||
const selectAddress = (id: string) => {
|
||||
setSelectedAddressId(id);
|
||||
if (id === "custom") return;
|
||||
const addr = savedAddresses?.find((a) => a.id === id);
|
||||
if (addr) {
|
||||
setForm({
|
||||
fullName: addr.fullName,
|
||||
email: addr.email,
|
||||
address: addr.address,
|
||||
city: addr.city,
|
||||
zip: addr.zip,
|
||||
country: addr.country,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const field = (key: keyof ShippingAddress, value: string) =>
|
||||
setForm((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (saveAddress && selectedAddressId === "custom" && onSaveAddress) {
|
||||
onSaveAddress(form);
|
||||
}
|
||||
onPlaceOrder?.({ shipping: form, paymentMethod });
|
||||
};
|
||||
|
||||
@ -90,7 +136,7 @@ export function CheckoutPage({
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className={cn("mx-auto grid max-w-5xl gap-8 lg:grid-cols-[1fr_380px]", className)}
|
||||
className={cn("mx-auto grid max-w-5xl gap-8 px-4 py-8 lg:grid-cols-[1fr_380px]", className)}
|
||||
>
|
||||
{/* ---- Left: Shipping + Payment ---- */}
|
||||
<div className="space-y-6">
|
||||
@ -99,72 +145,142 @@ export function CheckoutPage({
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Shipping Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-name">Full Name</Label>
|
||||
<Input
|
||||
id="ck-name"
|
||||
required
|
||||
placeholder="Jane Doe"
|
||||
value={form.fullName}
|
||||
onChange={(e) => field("fullName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Saved Address Selector */}
|
||||
{savedAddresses && savedAddresses.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium flex items-center gap-1.5">
|
||||
<MapPin className="h-4 w-4" />
|
||||
Saved Addresses
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
{savedAddresses.map((addr) => (
|
||||
<button
|
||||
key={addr.id}
|
||||
type="button"
|
||||
onClick={() => selectAddress(addr.id)}
|
||||
className={cn(
|
||||
"flex w-full items-start gap-3 rounded-lg border p-3 text-left transition-colors text-sm",
|
||||
selectedAddressId === addr.id
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:bg-accent/5",
|
||||
)}
|
||||
>
|
||||
<MapPin className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className="font-medium">
|
||||
{addr.label || addr.fullName}
|
||||
{addr.isDefault && (
|
||||
<span className="ml-2 text-xs text-muted-foreground">(Default)</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-muted-foreground truncate">
|
||||
{addr.address}, {addr.city}, {addr.country}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => selectAddress("custom")}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-lg border p-3 text-left transition-colors text-sm",
|
||||
selectedAddressId === "custom"
|
||||
? "border-primary bg-primary/5"
|
||||
: "border-border hover:bg-accent/5",
|
||||
)}
|
||||
>
|
||||
<span className="text-muted-foreground">✏️</span>
|
||||
<span className="font-medium">Enter a new address</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-email">Email</Label>
|
||||
<Input
|
||||
id="ck-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="jane@example.com"
|
||||
value={form.email}
|
||||
onChange={(e) => field("email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
{/* Form Fields */}
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-name">Full Name</Label>
|
||||
<Input
|
||||
id="ck-name"
|
||||
required
|
||||
placeholder="Jane Doe"
|
||||
value={form.fullName}
|
||||
onChange={(e) => field("fullName", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-address">Address</Label>
|
||||
<Input
|
||||
id="ck-address"
|
||||
required
|
||||
placeholder="123 Main St"
|
||||
value={form.address}
|
||||
onChange={(e) => field("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-email">Email</Label>
|
||||
<Input
|
||||
id="ck-email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="jane@example.com"
|
||||
value={form.email}
|
||||
onChange={(e) => field("email", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ck-city">City</Label>
|
||||
<Input
|
||||
id="ck-city"
|
||||
required
|
||||
placeholder="New York"
|
||||
value={form.city}
|
||||
onChange={(e) => field("city", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-address">Address</Label>
|
||||
<Input
|
||||
id="ck-address"
|
||||
required
|
||||
placeholder="123 Main St"
|
||||
value={form.address}
|
||||
onChange={(e) => field("address", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ck-zip">ZIP / Postal Code</Label>
|
||||
<Input
|
||||
id="ck-zip"
|
||||
required
|
||||
placeholder="10001"
|
||||
value={form.zip}
|
||||
onChange={(e) => field("zip", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ck-city">City</Label>
|
||||
<Input
|
||||
id="ck-city"
|
||||
required
|
||||
placeholder="New York"
|
||||
value={form.city}
|
||||
onChange={(e) => field("city", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-country">Country</Label>
|
||||
<Input
|
||||
id="ck-country"
|
||||
required
|
||||
placeholder="United States"
|
||||
value={form.country}
|
||||
onChange={(e) => field("country", e.target.value)}
|
||||
/>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="ck-zip">ZIP / Postal Code</Label>
|
||||
<Input
|
||||
id="ck-zip"
|
||||
required
|
||||
placeholder="10001"
|
||||
value={form.zip}
|
||||
onChange={(e) => field("zip", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="sm:col-span-2 space-y-1.5">
|
||||
<Label htmlFor="ck-country">Country</Label>
|
||||
<Input
|
||||
id="ck-country"
|
||||
required
|
||||
placeholder="United States"
|
||||
value={form.country}
|
||||
onChange={(e) => field("country", e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedAddressId === "custom" && onSaveAddress && (
|
||||
<div className="sm:col-span-2 flex items-center gap-2 pt-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="ck-save-address"
|
||||
checked={saveAddress}
|
||||
onChange={(e) => setSaveAddress(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-gray-300 accent-primary"
|
||||
/>
|
||||
<Label htmlFor="ck-save-address" className="text-sm font-normal cursor-pointer flex items-center gap-1.5">
|
||||
<Save className="h-3.5 w-3.5" />
|
||||
Save this address for future orders
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -17,6 +17,7 @@ export { CheckoutPage } from "./checkout/CheckoutPage";
|
||||
export type {
|
||||
CheckoutPageProps,
|
||||
ShippingAddress,
|
||||
SavedAddress,
|
||||
PaymentMethod,
|
||||
} from "./checkout/CheckoutPage";
|
||||
|
||||
|
||||
@ -23,10 +23,10 @@ export interface PolicyLinksProps {
|
||||
*/
|
||||
export function PolicyLinks({ links = defaultLinks, className }: PolicyLinksProps) {
|
||||
return (
|
||||
<nav className={cn("flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted-foreground", className)}>
|
||||
<nav className={cn("flex flex-wrap items-center justify-center gap-y-2 text-xs text-muted-foreground", className)}>
|
||||
{links.map((l, i) => (
|
||||
<React.Fragment key={l.href}>
|
||||
{i > 0 && <span className="hidden sm:inline" aria-hidden>·</span>}
|
||||
{i > 0 && <span className="mx-2 select-none" aria-hidden>·</span>}
|
||||
<a href={l.href} className="hover:text-foreground hover:underline transition-colors">
|
||||
{l.label}
|
||||
</a>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user