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:
2026-03-04 09:31:18 +03:00
commit cc15338ad9
361 changed files with 45005 additions and 0 deletions

View File

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