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,91 @@
|
||||
import Image from "next/image";
|
||||
import { Card, ScrollShadow } from "@heroui/react";
|
||||
import { formatPrice } from "@repo/utils";
|
||||
import type { OrderLineItem } from "@/lib/orders";
|
||||
|
||||
interface Props {
|
||||
items: OrderLineItem[];
|
||||
currency: string;
|
||||
}
|
||||
|
||||
function LineItem({
|
||||
item,
|
||||
currency,
|
||||
}: {
|
||||
item: OrderLineItem;
|
||||
currency: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-3 py-3 first:pt-0 last:pb-0">
|
||||
{/* Product image */}
|
||||
<div className="relative h-16 w-16 shrink-0 overflow-hidden rounded-md border border-gray-100 bg-gray-50 md:h-20 md:w-20">
|
||||
{item.imageUrl ? (
|
||||
<Image
|
||||
src={item.imageUrl}
|
||||
alt={item.productName}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 768px) 64px, 80px"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center text-2xl text-gray-300">
|
||||
🐾
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Item details */}
|
||||
<div className="flex flex-1 flex-col justify-between gap-1 md:flex-row md:items-start">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium text-[#1a2e2d]">
|
||||
{item.productName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">
|
||||
{item.variantName}
|
||||
</p>
|
||||
<p className="text-xs text-gray-400">SKU: {item.sku}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 text-sm md:flex-col md:items-end">
|
||||
<span className="text-gray-500">
|
||||
{item.quantity} × {formatPrice(item.unitPrice, currency)}
|
||||
</span>
|
||||
<span className="font-semibold text-[#236f6b]">
|
||||
{formatPrice(item.totalPrice, currency)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function OrderLineItems({ items, currency }: Props) {
|
||||
const scrollable = items.length > 4;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
<Card.Title className="text-base">
|
||||
Items ({items.length})
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content className="p-0">
|
||||
{scrollable ? (
|
||||
<ScrollShadow className="max-h-[400px] px-4 pb-4" hideScrollBar>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{items.map((item) => (
|
||||
<LineItem key={item._id} item={item} currency={currency} />
|
||||
))}
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-100 px-4 pb-4">
|
||||
{items.map((item) => (
|
||||
<LineItem key={item._id} item={item} currency={currency} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user