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>
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user