Completes the first milestone of The Pet Loft ecommerce platform: - apps/storefront: full customer-facing Next.js app with HeroUI (cart, checkout, orders, wishlist, product detail, shop, search, auth) - convex/: serverless backend with schema, queries, mutations, actions, HTTP routes, Stripe/Shippo integrations, and co-located tests - packages/types, packages/utils, packages/convex: shared workspace packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
3.1 KiB
TypeScript
100 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { Alert, Button, Card } from "@heroui/react";
|
|
import type { CheckoutValidationResult } from "@/lib/checkout/types";
|
|
import { CheckoutLineItems } from "../content/CheckoutLineItems";
|
|
import { CheckoutOrderSummary } from "../content/CheckoutOrderSummary";
|
|
|
|
type CartValidationStepProps = {
|
|
result: CheckoutValidationResult;
|
|
onProceed: () => void;
|
|
onBackToCart: () => void;
|
|
};
|
|
|
|
export function CartValidationStep({
|
|
result,
|
|
onProceed,
|
|
onBackToCart,
|
|
}: CartValidationStepProps) {
|
|
const hasBlockingIssues = !result.valid;
|
|
const hasPriceWarnings =
|
|
result.valid &&
|
|
result.issues.some((i) => i.type === "price_changed");
|
|
|
|
const itemCount = result.items.reduce((sum, i) => sum + i.quantity, 0);
|
|
|
|
const issueBannerId = "checkout-issue-banner";
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
{/* Issue banners */}
|
|
{hasBlockingIssues && (
|
|
<Alert id={issueBannerId} status="danger" role="alert">
|
|
<Alert.Indicator />
|
|
<Alert.Content>
|
|
<Alert.Title>Some items need attention</Alert.Title>
|
|
<Alert.Description>
|
|
Please return to your cart to resolve the issues below, then
|
|
try again.
|
|
</Alert.Description>
|
|
</Alert.Content>
|
|
</Alert>
|
|
)}
|
|
|
|
{hasPriceWarnings && (
|
|
<Alert id={issueBannerId} status="accent">
|
|
<Alert.Indicator />
|
|
<Alert.Content>
|
|
<Alert.Title>Prices updated</Alert.Title>
|
|
<Alert.Description>
|
|
Some prices have changed since you added items. The current
|
|
prices are shown below.
|
|
</Alert.Description>
|
|
</Alert.Content>
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Two-column on lg: items + sticky summary/actions */}
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr,340px] lg:items-start">
|
|
{/* Line items */}
|
|
<div>
|
|
<CheckoutLineItems items={result.items} issues={result.issues} />
|
|
</div>
|
|
|
|
{/* Summary + actions sidebar */}
|
|
<div className="lg:sticky lg:top-8 lg:self-start">
|
|
<Card className="rounded-lg p-4">
|
|
<Card.Content className="flex flex-col gap-4 p-0">
|
|
<CheckoutOrderSummary
|
|
subtotal={result.subtotal}
|
|
itemCount={itemCount}
|
|
/>
|
|
<div className="flex flex-col gap-2">
|
|
<Button
|
|
className="w-full bg-[#236f6b] font-medium text-white"
|
|
size="lg"
|
|
onPress={onProceed}
|
|
isDisabled={hasBlockingIssues}
|
|
aria-disabled={hasBlockingIssues || undefined}
|
|
aria-describedby={
|
|
hasBlockingIssues ? issueBannerId : undefined
|
|
}
|
|
>
|
|
Continue to shipping
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full md:w-auto"
|
|
onPress={onBackToCart}
|
|
>
|
|
Back to cart
|
|
</Button>
|
|
</div>
|
|
</Card.Content>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|