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,125 @@
|
||||
"use client";
|
||||
|
||||
import { Button, Chip, RadioGroup, Radio, Label, Skeleton } from "@heroui/react";
|
||||
import type { CheckoutAddress } from "@/lib/checkout/types";
|
||||
|
||||
type AddressSelectorProps = {
|
||||
addresses: CheckoutAddress[];
|
||||
selectedId: string | null;
|
||||
onSelect: (id: string) => void;
|
||||
onAddNew: () => void;
|
||||
isLoading: boolean;
|
||||
};
|
||||
|
||||
export function AddressSelector({
|
||||
addresses,
|
||||
selectedId,
|
||||
onSelect,
|
||||
onAddNew,
|
||||
isLoading,
|
||||
}: AddressSelectorProps) {
|
||||
if (isLoading) {
|
||||
return <AddressSelectorSkeleton />;
|
||||
}
|
||||
|
||||
if (addresses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<RadioGroup
|
||||
aria-label="Select shipping address"
|
||||
value={selectedId ?? undefined}
|
||||
onChange={(value) => onSelect(value)}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{addresses.map((address) => (
|
||||
<Radio key={address.id} value={address.id}>
|
||||
{({ isSelected }) => (
|
||||
<AddressCard address={address} isSelected={isSelected} />
|
||||
)}
|
||||
</Radio>
|
||||
))}
|
||||
</div>
|
||||
</RadioGroup>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="w-full md:w-auto"
|
||||
onPress={onAddNew}
|
||||
>
|
||||
+ Add new address
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddressCard({
|
||||
address,
|
||||
isSelected,
|
||||
}: {
|
||||
address: CheckoutAddress;
|
||||
isSelected: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex cursor-pointer flex-col gap-2 rounded-lg border-2 p-4 transition-colors
|
||||
${isSelected ? "border-[#236f6b] bg-[#e8f7f6]/40" : "border-default-200 bg-background hover:border-default-400"}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="text-sm font-semibold text-foreground">{address.fullName}</p>
|
||||
<div className="flex items-center gap-1.5">
|
||||
{address.isDefault && (
|
||||
<Chip size="sm" variant="soft">
|
||||
Default
|
||||
</Chip>
|
||||
)}
|
||||
{address.isValidated ? (
|
||||
<Chip size="sm" variant="soft" color="success">
|
||||
Verified
|
||||
</Chip>
|
||||
) : (
|
||||
<Chip size="sm" variant="soft" color="warning">
|
||||
Not verified
|
||||
</Chip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-default-600">
|
||||
<p>{address.addressLine1}</p>
|
||||
{address.additionalInformation && <p>{address.additionalInformation}</p>}
|
||||
<p>{address.city}</p>
|
||||
<p>{address.postalCode}</p>
|
||||
</div>
|
||||
|
||||
{address.phone && (
|
||||
<p className="text-xs text-default-400">{address.phone}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddressSelectorSkeleton() {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{Array.from({ length: 2 }).map((_, i) => (
|
||||
<div key={i} className="flex flex-col gap-3 rounded-lg border-2 border-default-200 p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Skeleton className="h-4 w-28 rounded-md" />
|
||||
<Skeleton className="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Skeleton className="h-3 w-40 rounded-md" />
|
||||
<Skeleton className="h-3 w-32 rounded-md" />
|
||||
<Skeleton className="h-3 w-20 rounded-md" />
|
||||
</div>
|
||||
<Skeleton className="h-3 w-24 rounded-md" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user