@instockng/storefront-ui 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +26 -0
  2. package/dist/components/Checkout.d.ts.map +1 -1
  3. package/dist/contexts/CartContext.d.ts.map +1 -1
  4. package/dist/index10.mjs +144 -141
  5. package/dist/index101.mjs +1 -1
  6. package/dist/index102.mjs +3 -3
  7. package/dist/index103.mjs +3 -3
  8. package/dist/index105.mjs +1 -1
  9. package/dist/index111.mjs +1 -1
  10. package/dist/index20.mjs +2 -2
  11. package/dist/index21.mjs +1 -1
  12. package/dist/index28.mjs +11 -11
  13. package/dist/index29.mjs +1 -1
  14. package/dist/index3.mjs +88 -74
  15. package/dist/index30.mjs +1 -1
  16. package/dist/index31.mjs +1 -1
  17. package/dist/index32.mjs +1 -1
  18. package/dist/index37.mjs +1 -1
  19. package/dist/index41.mjs +36 -23
  20. package/dist/index42.mjs +44 -36
  21. package/dist/index43.mjs +99 -44
  22. package/dist/index44.mjs +112 -99
  23. package/dist/index45.mjs +44 -80
  24. package/dist/index46.mjs +64 -53
  25. package/dist/index47.mjs +66 -49
  26. package/dist/index48.mjs +54 -73
  27. package/dist/index49.mjs +52 -63
  28. package/dist/index50.mjs +14 -70
  29. package/dist/index51.mjs +13 -14
  30. package/dist/index52.mjs +58 -13
  31. package/dist/index53.mjs +101 -34
  32. package/dist/index54.mjs +99 -95
  33. package/dist/index55.mjs +22 -132
  34. package/dist/index58.mjs +2 -2
  35. package/dist/index59.mjs +4 -3
  36. package/dist/index60.mjs +4 -2
  37. package/dist/index61.mjs +2 -5
  38. package/dist/index62.mjs +30 -231
  39. package/dist/index63.mjs +42 -5
  40. package/dist/index64.mjs +228 -127
  41. package/dist/index65.mjs +4 -66
  42. package/dist/index66.mjs +124 -77
  43. package/dist/index67.mjs +65 -26
  44. package/dist/index68.mjs +84 -6
  45. package/dist/index69.mjs +26 -72
  46. package/dist/index70.mjs +8 -3
  47. package/dist/index71.mjs +75 -2
  48. package/dist/index72.mjs +3 -82
  49. package/dist/index73.mjs +2 -54
  50. package/dist/index74.mjs +82 -5
  51. package/dist/index75.mjs +53 -4
  52. package/dist/index76.mjs +5 -178
  53. package/dist/index77.mjs +5 -53
  54. package/dist/index78.mjs +178 -68
  55. package/dist/index79.mjs +50 -31
  56. package/dist/index80.mjs +69 -43
  57. package/dist/index81.mjs +2 -2
  58. package/dist/index82.mjs +1 -1
  59. package/dist/index83.mjs +6 -6
  60. package/dist/index84.mjs +2 -2
  61. package/dist/index85.mjs +2 -2
  62. package/dist/index87.mjs +2 -2
  63. package/dist/index88.mjs +2 -2
  64. package/dist/index89.mjs +1 -1
  65. package/dist/index91.mjs +4 -4
  66. package/dist/index92.mjs +3 -3
  67. package/dist/index93.mjs +30 -12
  68. package/dist/index94.mjs +11 -7
  69. package/dist/index95.mjs +3 -30
  70. package/dist/index96.mjs +3 -10
  71. package/dist/index97.mjs +13 -4
  72. package/dist/index98.mjs +7 -4
  73. package/dist/index99.mjs +1 -1
  74. package/dist/styles.css +1 -0
  75. package/package.json +14 -13
  76. package/src/components/CartItem.stories.tsx +94 -0
  77. package/src/components/CartItem.tsx +141 -0
  78. package/src/components/Checkout.stories.tsx +380 -0
  79. package/src/components/Checkout.tsx +954 -0
  80. package/src/components/DiscountCodeInput.stories.tsx +76 -0
  81. package/src/components/DiscountCodeInput.tsx +162 -0
  82. package/src/components/OrderConfirmation.stories.tsx +142 -0
  83. package/src/components/OrderConfirmation.tsx +301 -0
  84. package/src/components/ProductCard.stories.tsx +112 -0
  85. package/src/components/ProductCard.tsx +195 -0
  86. package/src/components/ProductGrid.stories.tsx +137 -0
  87. package/src/components/ProductGrid.tsx +141 -0
  88. package/src/components/ShoppingCart.stories.tsx +459 -0
  89. package/src/components/ShoppingCart.tsx +262 -0
  90. package/src/components/ui/badge.tsx +37 -0
  91. package/src/components/ui/button.tsx +71 -0
  92. package/src/components/ui/card.tsx +79 -0
  93. package/src/components/ui/form-input.tsx +78 -0
  94. package/src/components/ui/form-select.tsx +73 -0
  95. package/src/components/ui/modal.tsx +181 -0
  96. package/src/contexts/CartContext.tsx +305 -0
  97. package/src/hooks/usePaystackPayment.ts +137 -0
  98. package/src/index.ts +51 -0
  99. package/src/lib/utils.ts +45 -0
  100. package/src/paystack.svg +67 -0
  101. package/src/providers/StorefrontProvider.tsx +70 -0
  102. package/src/styles.css +1 -0
  103. package/src/test-utils/MockCartProvider.tsx +424 -0
  104. package/src/vite-env.d.ts +12 -0
@@ -0,0 +1,112 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+ import { ProductCard, type ProductCardProduct } from './ProductCard';
3
+
4
+ const meta = {
5
+ title: 'Components/ProductCard',
6
+ component: ProductCard,
7
+ parameters: {
8
+ layout: 'centered',
9
+ },
10
+ tags: ['autodocs'],
11
+ } satisfies Meta<typeof ProductCard>;
12
+
13
+ export default meta;
14
+ type Story = StoryObj<typeof meta>;
15
+
16
+ // Mock product data
17
+ const mockProduct: ProductCardProduct = {
18
+ id: '1',
19
+ name: 'Premium Cotton T-Shirt',
20
+ description: 'Comfortable and stylish cotton t-shirt perfect for everyday wear',
21
+ imageUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
22
+ brand: {
23
+ name: 'Fashion Brand',
24
+ logoUrl: 'https://via.placeholder.com/100x50',
25
+ },
26
+ variants: [
27
+ {
28
+ id: 'v1',
29
+ name: 'Small - Blue',
30
+ price: 29.99,
31
+ sku: 'TSHIRT-SM-BLUE',
32
+ stock: 15,
33
+ },
34
+ {
35
+ id: 'v2',
36
+ name: 'Medium - Blue',
37
+ price: 29.99,
38
+ sku: 'TSHIRT-MD-BLUE',
39
+ stock: 20,
40
+ },
41
+ {
42
+ id: 'v3',
43
+ name: 'Large - Blue',
44
+ price: 29.99,
45
+ sku: 'TSHIRT-LG-BLUE',
46
+ stock: 8,
47
+ },
48
+ ],
49
+ };
50
+
51
+ export const Default: Story = {
52
+ args: {
53
+ product: mockProduct,
54
+ onAddToCart: (variantId, quantity) => {
55
+ console.log('Add to cart:', variantId, quantity);
56
+ },
57
+ showStock: true,
58
+ },
59
+ };
60
+
61
+ export const OutOfStock: Story = {
62
+ args: {
63
+ product: {
64
+ ...mockProduct,
65
+ variants: mockProduct.variants.map((v) => ({ ...v, stock: 0 })),
66
+ },
67
+ showStock: true,
68
+ },
69
+ };
70
+
71
+ export const LowStock: Story = {
72
+ args: {
73
+ product: {
74
+ ...mockProduct,
75
+ variants: [
76
+ {
77
+ id: 'v1',
78
+ name: 'Last One!',
79
+ price: 29.99,
80
+ sku: 'TSHIRT-SM-RED',
81
+ stock: 2,
82
+ },
83
+ ],
84
+ },
85
+ showStock: true,
86
+ },
87
+ };
88
+
89
+ export const SingleVariant: Story = {
90
+ args: {
91
+ product: {
92
+ ...mockProduct,
93
+ variants: [mockProduct.variants[0]],
94
+ },
95
+ },
96
+ };
97
+
98
+ export const NoImage: Story = {
99
+ args: {
100
+ product: {
101
+ ...mockProduct,
102
+ imageUrl: undefined,
103
+ },
104
+ },
105
+ };
106
+
107
+ export const Loading: Story = {
108
+ args: {
109
+ product: mockProduct,
110
+ isAddingToCart: true,
111
+ },
112
+ };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * ProductCard Component
3
+ *
4
+ * Displays a product with image, name, price, and add to cart button.
5
+ * Handles variant selection if product has multiple variants.
6
+ */
7
+
8
+ import { useState } from 'react';
9
+ import { Card, CardContent, CardFooter } from './ui/card';
10
+ import { Button } from './ui/button';
11
+ import { Badge } from './ui/badge';
12
+ import { ShoppingCart, Package } from 'lucide-react';
13
+ import { formatCurrency, cn } from '../lib/utils';
14
+
15
+ // Simplified product type based on common OMS structure
16
+ export interface ProductCardProduct {
17
+ id: string;
18
+ name: string;
19
+ description?: string;
20
+ imageUrl?: string;
21
+ brand?: {
22
+ name: string;
23
+ logoUrl?: string;
24
+ };
25
+ variants: Array<{
26
+ id: string;
27
+ name: string;
28
+ price: number;
29
+ sku: string;
30
+ stock?: number;
31
+ }>;
32
+ }
33
+
34
+ export interface ProductCardProps {
35
+ product: ProductCardProduct;
36
+ /** Callback when add to cart is clicked */
37
+ onAddToCart?: (variantId: string, quantity: number) => void;
38
+ /** Show stock information */
39
+ showStock?: boolean;
40
+ /** Custom class names */
41
+ className?: string;
42
+ /** Image class name */
43
+ imageClassName?: string;
44
+ /** Disable add to cart button */
45
+ disableAddToCart?: boolean;
46
+ /** Loading state for add to cart */
47
+ isAddingToCart?: boolean;
48
+ /** Click handler for card */
49
+ onClick?: () => void;
50
+ }
51
+
52
+ export function ProductCard({
53
+ product,
54
+ onAddToCart,
55
+ showStock = true,
56
+ className,
57
+ imageClassName,
58
+ disableAddToCart = false,
59
+ isAddingToCart = false,
60
+ onClick,
61
+ }: ProductCardProps) {
62
+ const [selectedVariantId, setSelectedVariantId] = useState<string>(
63
+ product.variants[0]?.id || ''
64
+ );
65
+ const [quantity, setQuantity] = useState(1);
66
+
67
+ const selectedVariant = product.variants.find((v) => v.id === selectedVariantId);
68
+ const hasMultipleVariants = product.variants.length > 1;
69
+
70
+ // Check if product is in stock
71
+ const isInStock = selectedVariant
72
+ ? selectedVariant.stock === undefined || selectedVariant.stock > 0
73
+ : false;
74
+
75
+ const handleAddToCart = (e: React.MouseEvent) => {
76
+ e.stopPropagation();
77
+ if (selectedVariant && onAddToCart) {
78
+ onAddToCart(selectedVariant.id, quantity);
79
+ }
80
+ };
81
+
82
+ return (
83
+ <Card
84
+ className={cn(
85
+ 'group overflow-hidden transition-all hover:shadow-lg',
86
+ onClick && 'cursor-pointer',
87
+ className
88
+ )}
89
+ onClick={onClick}
90
+ >
91
+ {/* Product Image */}
92
+ <div className="relative aspect-square overflow-hidden bg-gray-100">
93
+ {product.imageUrl ? (
94
+ <img
95
+ src={product.imageUrl}
96
+ alt={product.name}
97
+ className={cn(
98
+ 'h-full w-full object-cover transition-transform group-hover:scale-105',
99
+ imageClassName
100
+ )}
101
+ />
102
+ ) : (
103
+ <div className="flex h-full w-full items-center justify-center">
104
+ <Package className="h-16 w-16 text-gray-400" />
105
+ </div>
106
+ )}
107
+
108
+ {/* Out of stock badge */}
109
+ {!isInStock && (
110
+ <Badge className="absolute right-2 top-2 bg-red-600">Out of Stock</Badge>
111
+ )}
112
+
113
+ {/* Stock badge */}
114
+ {showStock && isInStock && selectedVariant?.stock !== undefined && selectedVariant.stock < 10 && (
115
+ <Badge className="absolute right-2 top-2 bg-orange-600">
116
+ Only {selectedVariant.stock} left
117
+ </Badge>
118
+ )}
119
+ </div>
120
+
121
+ <CardContent className="p-4">
122
+ {/* Product Name */}
123
+ <h3 className="mb-1 font-semibold line-clamp-2">{product.name}</h3>
124
+
125
+ {/* Description */}
126
+ {product.description && (
127
+ <p className="mb-2 text-sm text-gray-600 line-clamp-2">{product.description}</p>
128
+ )}
129
+
130
+ {/* Variant Selector */}
131
+ {hasMultipleVariants && (
132
+ <div className="mb-3">
133
+ <label className="mb-1 block text-xs font-medium text-gray-700">
134
+ Select Option
135
+ </label>
136
+ <select
137
+ value={selectedVariantId}
138
+ onChange={(e) => {
139
+ e.stopPropagation();
140
+ setSelectedVariantId(e.target.value);
141
+ }}
142
+ className="w-full rounded-md border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
143
+ onClick={(e) => e.stopPropagation()}
144
+ >
145
+ {product.variants.map((variant) => (
146
+ <option key={variant.id} value={variant.id}>
147
+ {variant.name} - {formatCurrency(variant.price)}
148
+ </option>
149
+ ))}
150
+ </select>
151
+ </div>
152
+ )}
153
+
154
+ {/* Price */}
155
+ <div className="mb-3 text-2xl font-bold text-blue-600">
156
+ {selectedVariant && formatCurrency(selectedVariant.price)}
157
+ </div>
158
+
159
+ {/* Quantity Selector */}
160
+ <div className="flex items-center gap-2">
161
+ <label className="text-xs font-medium text-gray-700">Qty:</label>
162
+ <input
163
+ type="number"
164
+ min="1"
165
+ max={selectedVariant?.stock || 999}
166
+ value={quantity}
167
+ onChange={(e) => {
168
+ e.stopPropagation();
169
+ setQuantity(Math.max(1, parseInt(e.target.value) || 1));
170
+ }}
171
+ onClick={(e) => e.stopPropagation()}
172
+ className="w-20 rounded-md border border-gray-300 px-2 py-1 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
173
+ />
174
+ </div>
175
+ </CardContent>
176
+
177
+ <CardFooter className="p-4 pt-0">
178
+ <Button
179
+ onClick={handleAddToCart}
180
+ disabled={disableAddToCart || !isInStock || isAddingToCart}
181
+ className="w-full"
182
+ >
183
+ {isAddingToCart ? (
184
+ <>Adding...</>
185
+ ) : (
186
+ <>
187
+ <ShoppingCart className="mr-2 h-4 w-4" />
188
+ Add to Cart
189
+ </>
190
+ )}
191
+ </Button>
192
+ </CardFooter>
193
+ </Card>
194
+ );
195
+ }
@@ -0,0 +1,137 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { ProductGrid } from './ProductGrid';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { fn } from '@storybook/test';
5
+
6
+ // Mock products data
7
+ const mockProductsResponse = {
8
+ data: [
9
+ {
10
+ id: '1',
11
+ name: 'Premium Cotton T-Shirt',
12
+ description: 'Comfortable and stylish cotton t-shirt',
13
+ thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
14
+ variants: [
15
+ { id: 'v1', name: 'Small - Blue', price: 29.99, sku: 'TSHIRT-SM-BLUE', stock: 15 },
16
+ { id: 'v2', name: 'Medium - Blue', price: 29.99, sku: 'TSHIRT-MD-BLUE', stock: 20 },
17
+ { id: 'v3', name: 'Large - Blue', price: 29.99, sku: 'TSHIRT-LG-BLUE', stock: 8 },
18
+ ],
19
+ },
20
+ {
21
+ id: '2',
22
+ name: 'Slim Fit Jeans',
23
+ description: 'Classic denim jeans with modern fit',
24
+ thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
25
+ variants: [
26
+ { id: 'v4', name: 'Medium - Black', price: 59.99, sku: 'JEANS-MD-BLACK', stock: 12 },
27
+ { id: 'v5', name: 'Large - Black', price: 59.99, sku: 'JEANS-LG-BLACK', stock: 5 },
28
+ ],
29
+ },
30
+ {
31
+ id: '3',
32
+ name: 'Casual Sneakers',
33
+ description: 'Comfortable everyday sneakers',
34
+ thumbnailUrl: 'https://images.unsplash.com/photo-1549298916-b41d501d3772?w=400',
35
+ variants: [
36
+ { id: 'v6', name: 'Size 9 - White', price: 79.99, sku: 'SNEAKER-9-WHITE', stock: 0 },
37
+ { id: 'v7', name: 'Size 10 - White', price: 79.99, sku: 'SNEAKER-10-WHITE', stock: 3 },
38
+ ],
39
+ },
40
+ {
41
+ id: '4',
42
+ name: 'Leather Wallet',
43
+ description: 'Premium leather wallet with multiple compartments',
44
+ thumbnailUrl: 'https://images.unsplash.com/photo-1627123424574-724758594e93?w=400',
45
+ variants: [
46
+ { id: 'v8', name: 'Brown', price: 39.99, sku: 'WALLET-BROWN', stock: 25 },
47
+ ],
48
+ },
49
+ ],
50
+ };
51
+
52
+ const queryClient = new QueryClient({
53
+ defaultOptions: {
54
+ queries: {
55
+ retry: false,
56
+ queryFn: async () => mockProductsResponse,
57
+ },
58
+ },
59
+ });
60
+
61
+ const meta: Meta<typeof ProductGrid> = {
62
+ title: 'Components/ProductGrid',
63
+ component: ProductGrid,
64
+ parameters: {
65
+ layout: 'padded',
66
+ },
67
+ tags: ['autodocs'],
68
+ decorators: [
69
+ (Story) => (
70
+ <QueryClientProvider client={queryClient}>
71
+ <Story />
72
+ </QueryClientProvider>
73
+ ),
74
+ ],
75
+ } satisfies Meta<typeof ProductGrid>;
76
+
77
+ export default meta;
78
+ type Story = StoryObj<typeof meta>;
79
+
80
+ export const ThreeColumns: Story = {
81
+ args: {
82
+ brandId: 'brand-1',
83
+ columns: 3,
84
+ onAddToCart: fn(),
85
+ },
86
+ };
87
+
88
+ export const TwoColumns: Story = {
89
+ args: {
90
+ brandId: 'brand-1',
91
+ columns: 2,
92
+ onAddToCart: fn(),
93
+ },
94
+ };
95
+
96
+ export const FourColumns: Story = {
97
+ args: {
98
+ brandId: 'brand-1',
99
+ columns: 4,
100
+ onAddToCart: fn(),
101
+ },
102
+ };
103
+
104
+ export const WithStockInfo: Story = {
105
+ args: {
106
+ brandId: 'brand-1',
107
+ columns: 3,
108
+ showStock: true,
109
+ onAddToCart: fn(),
110
+ },
111
+ };
112
+
113
+ export const WithoutStockInfo: Story = {
114
+ args: {
115
+ brandId: 'brand-1',
116
+ columns: 3,
117
+ showStock: false,
118
+ onAddToCart: fn(),
119
+ },
120
+ };
121
+
122
+ export const WithClickHandler: Story = {
123
+ args: {
124
+ brandId: 'brand-1',
125
+ columns: 3,
126
+ onAddToCart: fn(),
127
+ onProductClick: fn(),
128
+ },
129
+ };
130
+
131
+ export const CustomCurrency: Story = {
132
+ args: {
133
+ brandId: 'brand-1',
134
+ columns: 3,
135
+ onAddToCart: fn(),
136
+ },
137
+ };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * ProductGrid Component
3
+ *
4
+ * Displays a grid of products using ProductCard.
5
+ * Handles loading, empty states, and integrates with useGetProducts hook.
6
+ */
7
+
8
+ import { useGetProducts } from '@oms/api-client';
9
+ import { ProductCard, type ProductCardProduct } from './ProductCard';
10
+ import { Loader2, Package } from 'lucide-react';
11
+ import { cn } from '../lib/utils';
12
+
13
+ export interface ProductGridProps {
14
+ /** Brand UUID to fetch products for */
15
+ brandId: string;
16
+ /** Callback when add to cart is clicked */
17
+ onAddToCart?: (variantId: string, quantity: number) => void;
18
+ /** Callback when product card is clicked */
19
+ onProductClick?: (product: ProductCardProduct) => void;
20
+ /** Number of columns in grid */
21
+ columns?: 2 | 3 | 4;
22
+ /** Show stock information */
23
+ showStock?: boolean;
24
+ /** Custom class name for grid container */
25
+ className?: string;
26
+ /** Custom class name for product cards */
27
+ cardClassName?: string;
28
+ /** Show loading state */
29
+ showLoading?: boolean;
30
+ /** Custom empty state message */
31
+ emptyMessage?: string;
32
+ /** Custom error message */
33
+ errorMessage?: string;
34
+ /** Loading variant ID (to show loading state on specific card) */
35
+ loadingVariantId?: string;
36
+ }
37
+
38
+ export function ProductGrid({
39
+ brandId,
40
+ onAddToCart,
41
+ onProductClick,
42
+ columns = 3,
43
+ showStock = true,
44
+ className,
45
+ cardClassName,
46
+ showLoading = true,
47
+ emptyMessage = 'No products available at the moment.',
48
+ errorMessage = 'Failed to load products. Please try again later.',
49
+ loadingVariantId,
50
+ }: ProductGridProps) {
51
+ const { data, isLoading, error } = useGetProducts(brandId);
52
+
53
+ // Loading state
54
+ if (isLoading && showLoading) {
55
+ return (
56
+ <div className="flex min-h-[400px] items-center justify-center">
57
+ <div className="text-center">
58
+ <Loader2 className="mx-auto mb-4 h-12 w-12 animate-spin text-blue-600" />
59
+ <p className="text-gray-600">Loading products...</p>
60
+ </div>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ // Error state
66
+ if (error) {
67
+ return (
68
+ <div className="flex min-h-[400px] items-center justify-center">
69
+ <div className="text-center">
70
+ <Package className="mx-auto mb-4 h-12 w-12 text-red-600" />
71
+ <p className="text-red-600">{errorMessage}</p>
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ // Get products array from response
78
+ const products = data || [];
79
+
80
+ // Empty state
81
+ if (products.length === 0) {
82
+ return (
83
+ <div className="flex min-h-[400px] items-center justify-center">
84
+ <div className="text-center">
85
+ <Package className="mx-auto mb-4 h-12 w-12 text-gray-400" />
86
+ <p className="text-gray-600">{emptyMessage}</p>
87
+ </div>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // Column class mapping
93
+ const gridColsClass = {
94
+ 2: 'grid-cols-1 sm:grid-cols-2',
95
+ 3: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
96
+ 4: 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
97
+ };
98
+
99
+ return (
100
+ <div
101
+ className={cn(
102
+ 'grid gap-6',
103
+ gridColsClass[columns],
104
+ className
105
+ )}
106
+ >
107
+ {products.map((product: any) => {
108
+ // Convert product to ProductCardProduct format
109
+ const productCardData: ProductCardProduct = {
110
+ id: product.id,
111
+ name: product.name,
112
+ description: product.description || undefined,
113
+ imageUrl: product.thumbnailUrl || undefined,
114
+ brand: undefined, // Brand info not included in product response
115
+ variants: product.variants.map((v: any) => ({
116
+ id: v.id,
117
+ name: v.name || v.sku,
118
+ price: typeof v.price === 'string' ? parseFloat(v.price) : v.price,
119
+ sku: v.sku,
120
+ stock: v.stock || undefined,
121
+ })),
122
+ };
123
+
124
+ // Check if any variant of this product is currently being added to cart
125
+ const isAddingToCart = product.variants.some((v: any) => v.id === loadingVariantId);
126
+
127
+ return (
128
+ <ProductCard
129
+ key={product.id}
130
+ product={productCardData}
131
+ onAddToCart={onAddToCart}
132
+ onClick={onProductClick ? () => onProductClick(productCardData) : undefined}
133
+ showStock={showStock}
134
+ className={cardClassName}
135
+ isAddingToCart={isAddingToCart}
136
+ />
137
+ );
138
+ })}
139
+ </div>
140
+ );
141
+ }