Files
the-pet-loft/apps/storefront/src/components/checkout/steps/CartValidationStep.tsx
ianshaloom cc15338ad9 feat: initial commit — storefront, convex backend, and shared packages
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>
2026-03-04 09:31:18 +03:00

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>
);
}