@instockng/storefront-ui 1.0.10 → 1.0.12
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.
- package/README.md +26 -0
- package/dist/components/Checkout.d.ts.map +1 -1
- package/dist/components/ShoppingCart.d.ts.map +1 -1
- package/dist/contexts/CartContext.d.ts.map +1 -1
- package/dist/index10.mjs +144 -141
- package/dist/index101.mjs +1 -1
- package/dist/index102.mjs +3 -3
- package/dist/index103.mjs +3 -3
- package/dist/index105.mjs +1 -1
- package/dist/index111.mjs +1 -1
- package/dist/index20.mjs +2 -2
- package/dist/index21.mjs +1 -1
- package/dist/index28.mjs +11 -11
- package/dist/index3.mjs +88 -78
- package/dist/index37.mjs +1 -1
- package/dist/index41.mjs +36 -23
- package/dist/index42.mjs +44 -36
- package/dist/index43.mjs +99 -44
- package/dist/index44.mjs +112 -99
- package/dist/index45.mjs +44 -80
- package/dist/index46.mjs +64 -53
- package/dist/index47.mjs +65 -48
- package/dist/index48.mjs +54 -73
- package/dist/index49.mjs +52 -63
- package/dist/index50.mjs +14 -70
- package/dist/index51.mjs +13 -14
- package/dist/index52.mjs +58 -13
- package/dist/index53.mjs +101 -34
- package/dist/index54.mjs +99 -95
- package/dist/index55.mjs +22 -132
- package/dist/index62.mjs +30 -231
- package/dist/index63.mjs +42 -5
- package/dist/index64.mjs +228 -127
- package/dist/index65.mjs +4 -66
- package/dist/index66.mjs +124 -77
- package/dist/index67.mjs +65 -26
- package/dist/index68.mjs +84 -6
- package/dist/index69.mjs +26 -72
- package/dist/index70.mjs +8 -3
- package/dist/index71.mjs +75 -2
- package/dist/index72.mjs +3 -82
- package/dist/index73.mjs +2 -54
- package/dist/index74.mjs +82 -5
- package/dist/index75.mjs +53 -4
- package/dist/index76.mjs +5 -178
- package/dist/index77.mjs +5 -53
- package/dist/index78.mjs +178 -68
- package/dist/index79.mjs +50 -31
- package/dist/index8.mjs +8 -7
- package/dist/index80.mjs +69 -43
- package/dist/index81.mjs +2 -2
- package/dist/index82.mjs +1 -1
- package/dist/index83.mjs +6 -6
- package/dist/index84.mjs +2 -2
- package/dist/index85.mjs +2 -2
- package/dist/index87.mjs +2 -2
- package/dist/index88.mjs +2 -2
- package/dist/index89.mjs +1 -1
- package/dist/index91.mjs +4 -4
- package/dist/index92.mjs +3 -3
- package/dist/index93.mjs +12 -30
- package/dist/index94.mjs +7 -11
- package/dist/index95.mjs +30 -3
- package/dist/index96.mjs +10 -3
- package/dist/index97.mjs +4 -13
- package/dist/index98.mjs +4 -7
- package/dist/index99.mjs +1 -1
- package/dist/styles.css +1 -0
- package/package.json +14 -13
- package/src/components/CartItem.stories.tsx +94 -0
- package/src/components/CartItem.tsx +141 -0
- package/src/components/Checkout.stories.tsx +380 -0
- package/src/components/Checkout.tsx +954 -0
- package/src/components/DiscountCodeInput.stories.tsx +76 -0
- package/src/components/DiscountCodeInput.tsx +162 -0
- package/src/components/OrderConfirmation.stories.tsx +142 -0
- package/src/components/OrderConfirmation.tsx +301 -0
- package/src/components/ProductCard.stories.tsx +112 -0
- package/src/components/ProductCard.tsx +195 -0
- package/src/components/ProductGrid.stories.tsx +137 -0
- package/src/components/ProductGrid.tsx +141 -0
- package/src/components/ShoppingCart.stories.tsx +459 -0
- package/src/components/ShoppingCart.tsx +263 -0
- package/src/components/ui/badge.tsx +37 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +79 -0
- package/src/components/ui/form-input.tsx +78 -0
- package/src/components/ui/form-select.tsx +73 -0
- package/src/components/ui/modal.tsx +181 -0
- package/src/contexts/CartContext.tsx +316 -0
- package/src/hooks/usePaystackPayment.ts +137 -0
- package/src/index.ts +51 -0
- package/src/lib/utils.ts +45 -0
- package/src/paystack.svg +67 -0
- package/src/providers/StorefrontProvider.tsx +70 -0
- package/src/styles.css +1 -0
- package/src/test-utils/MockCartProvider.tsx +424 -0
- package/src/vite-env.d.ts +12 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { CartItem } from './CartItem';
|
|
3
|
+
import type {CartItem as CartItemType} from '@oms/api-client';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/CartItem',
|
|
7
|
+
component: CartItem,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
} satisfies Meta<typeof CartItem>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
const mockCartItem: CartItemType = {
|
|
18
|
+
id: 'item-1',
|
|
19
|
+
quantity: 2,
|
|
20
|
+
variant: {
|
|
21
|
+
createdAt: new Date().toISOString(),
|
|
22
|
+
updatedAt: new Date().toISOString(),
|
|
23
|
+
deletedAt: null,
|
|
24
|
+
isActive: true,
|
|
25
|
+
id: 'variant-1',
|
|
26
|
+
name: 'Medium - Blue',
|
|
27
|
+
price: 29.99,
|
|
28
|
+
sku: 'TSHIRT-MD-BLUE',
|
|
29
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
|
|
30
|
+
trackInventory: true,
|
|
31
|
+
lowStockThreshold: 10,
|
|
32
|
+
productId: 'product-1',
|
|
33
|
+
product: {
|
|
34
|
+
name: 'Premium Cotton T-Shirt',
|
|
35
|
+
slug: 'cotton-shirt',
|
|
36
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
|
|
37
|
+
id: 'product-1',
|
|
38
|
+
brandId: 'brand-1',
|
|
39
|
+
createdAt: new Date().toISOString(),
|
|
40
|
+
updatedAt: new Date().toISOString(),
|
|
41
|
+
deletedAt: null,
|
|
42
|
+
isActive: true,
|
|
43
|
+
description: 'Premium cotton t-shirt',
|
|
44
|
+
quantityDiscounts: null,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
basePrice: 29.99,
|
|
48
|
+
discountPercent: 0,
|
|
49
|
+
finalPrice: 29.99,
|
|
50
|
+
subtotal: 59.98,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Default: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
item: mockCartItem,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const NoImage: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
item: {
|
|
62
|
+
...mockCartItem,
|
|
63
|
+
variant: {
|
|
64
|
+
...mockCartItem.variant,
|
|
65
|
+
product: {
|
|
66
|
+
name: 'Premium Cotton T-Shirt',
|
|
67
|
+
id: 'product-1',
|
|
68
|
+
brandId: 'brand-1',
|
|
69
|
+
slug: 'cotton-shirt',
|
|
70
|
+
createdAt: new Date().toISOString(),
|
|
71
|
+
updatedAt: new Date().toISOString(),
|
|
72
|
+
deletedAt: null,
|
|
73
|
+
isActive: true,
|
|
74
|
+
description: 'Premium cotton t-shirt',
|
|
75
|
+
quantityDiscounts: null,
|
|
76
|
+
thumbnailUrl: null,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Disabled: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
item: mockCartItem,
|
|
86
|
+
disabled: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const Removing: Story = {
|
|
91
|
+
args: {
|
|
92
|
+
item: mockCartItem,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CartItem Component
|
|
3
|
+
*
|
|
4
|
+
* Displays a single item in the shopping cart with quantity controls and remove button.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Trash2, Package } from 'lucide-react';
|
|
8
|
+
import { formatCurrency, cn } from '../lib/utils';
|
|
9
|
+
import type { CartItem as CartItemType } from '@oms/api-client';
|
|
10
|
+
import { useCart } from '@/contexts/CartContext';
|
|
11
|
+
import { useCallback, useState } from 'react';
|
|
12
|
+
|
|
13
|
+
export interface CartItemProps {
|
|
14
|
+
item: CartItemType;
|
|
15
|
+
/** Custom class name */
|
|
16
|
+
className?: string;
|
|
17
|
+
/** Disable controls */
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function CartItem({
|
|
22
|
+
item,
|
|
23
|
+
className,
|
|
24
|
+
disabled = false,
|
|
25
|
+
}: CartItemProps) {
|
|
26
|
+
const [isRemoving, setIsRemoving] = useState(false);
|
|
27
|
+
const [isUpdating, setIsUpdating] = useState(false);
|
|
28
|
+
const { removeItem, updateItem } = useCart();
|
|
29
|
+
|
|
30
|
+
const handleQuantityChange = useCallback(async (newQuantity: number) => {
|
|
31
|
+
if (newQuantity <= 0) return;
|
|
32
|
+
setIsUpdating(true);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
await updateItem(item.id, newQuantity);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('Failed to update item quantity:', error);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
setIsUpdating(false);
|
|
41
|
+
}, [item.id, updateItem]);
|
|
42
|
+
|
|
43
|
+
const handleRemove = useCallback(async () => {
|
|
44
|
+
setIsRemoving(true);
|
|
45
|
+
try {
|
|
46
|
+
await removeItem(item.id);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('Failed to remove item:', error);
|
|
49
|
+
}
|
|
50
|
+
setIsRemoving(false);
|
|
51
|
+
}, [item.id, removeItem]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div
|
|
55
|
+
className={cn(
|
|
56
|
+
'flex gap-3 transition-opacity',
|
|
57
|
+
(disabled || isRemoving || isUpdating) && 'opacity-50',
|
|
58
|
+
className
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{/* Product Image */}
|
|
62
|
+
<div className="h-20 w-20 flex-shrink-0 overflow-hidden rounded-lg bg-gray-100">
|
|
63
|
+
{item.variant.product.thumbnailUrl ? (
|
|
64
|
+
<img
|
|
65
|
+
src={item.variant.product.thumbnailUrl}
|
|
66
|
+
alt={item.variant.product.name}
|
|
67
|
+
className="h-full w-full object-cover"
|
|
68
|
+
/>
|
|
69
|
+
) : (
|
|
70
|
+
<div className="flex h-full w-full items-center justify-center">
|
|
71
|
+
<Package className="h-6 w-6 text-gray-400" />
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Item Details */}
|
|
77
|
+
<div className="flex flex-1 flex-col justify-between py-1">
|
|
78
|
+
<div>
|
|
79
|
+
<div className="flex items-start justify-between gap-2">
|
|
80
|
+
<div className="flex-1">
|
|
81
|
+
<h3 className="text-base font-semibold leading-tight">
|
|
82
|
+
{item.variant.product.name}
|
|
83
|
+
</h3>
|
|
84
|
+
{item.variant.name && <p className="mt-0.5 text-sm text-gray-600">
|
|
85
|
+
{item.variant.name}
|
|
86
|
+
</p>}
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
{/* Remove Button */}
|
|
90
|
+
<button
|
|
91
|
+
onClick={handleRemove}
|
|
92
|
+
disabled={disabled || isRemoving}
|
|
93
|
+
className="text-gray-400 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-50"
|
|
94
|
+
aria-label="Remove item"
|
|
95
|
+
>
|
|
96
|
+
<Trash2 className="h-5 w-5" />
|
|
97
|
+
</button>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Quantity Controls and Price */}
|
|
102
|
+
<div className="flex items-center justify-between mt-2">
|
|
103
|
+
{/* Quantity Controls */}
|
|
104
|
+
<div className="inline-flex items-center overflow-hidden rounded-full bg-gray-200 text-gray-700">
|
|
105
|
+
<button
|
|
106
|
+
aria-label="Decrease quantity"
|
|
107
|
+
onClick={() => handleQuantityChange(item.quantity - 1)}
|
|
108
|
+
disabled={disabled || item.quantity <= 1 || isUpdating}
|
|
109
|
+
className="flex h-8 w-8 items-center justify-center transition-colors hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50"
|
|
110
|
+
>
|
|
111
|
+
−
|
|
112
|
+
</button>
|
|
113
|
+
<span className="px-3 text-base font-medium select-none">
|
|
114
|
+
{item.quantity}
|
|
115
|
+
</span>
|
|
116
|
+
<button
|
|
117
|
+
aria-label="Increase quantity"
|
|
118
|
+
onClick={() => handleQuantityChange(item.quantity + 1)}
|
|
119
|
+
disabled={disabled || isUpdating}
|
|
120
|
+
className="flex h-8 w-8 items-center justify-center transition-colors hover:bg-gray-300 disabled:cursor-not-allowed disabled:opacity-50"
|
|
121
|
+
>
|
|
122
|
+
+
|
|
123
|
+
</button>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Price */}
|
|
127
|
+
<div className="text-right">
|
|
128
|
+
<p className="text-base font-medium">
|
|
129
|
+
{formatCurrency(item.subtotal)}
|
|
130
|
+
</p>
|
|
131
|
+
{item.basePrice !== item.finalPrice && (
|
|
132
|
+
<p className="text-xs text-[#DC143C] line-through">
|
|
133
|
+
{formatCurrency(item.basePrice * item.quantity)}
|
|
134
|
+
</p>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkout Storybook Stories
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates the checkout flow with mocked cart and delivery zone data.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { fn } from '@storybook/test';
|
|
10
|
+
import { Checkout, type CheckoutProps } from './Checkout';
|
|
11
|
+
import { Button } from './ui/button';
|
|
12
|
+
import { MockCartProvider } from '../test-utils/MockCartProvider';
|
|
13
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
14
|
+
import type { Cart } from '@oms/api-client';
|
|
15
|
+
|
|
16
|
+
// Mock delivery zones data
|
|
17
|
+
const mockDeliveryZones = [
|
|
18
|
+
{
|
|
19
|
+
id: 'state-1',
|
|
20
|
+
name: 'Lagos',
|
|
21
|
+
zones: [
|
|
22
|
+
{
|
|
23
|
+
id: 'zone-1',
|
|
24
|
+
name: 'Ikeja',
|
|
25
|
+
deliveryCost: 1500,
|
|
26
|
+
freeShippingThreshold: 10000,
|
|
27
|
+
allowCOD: true,
|
|
28
|
+
allowOnline: true,
|
|
29
|
+
waybillOnly: false,
|
|
30
|
+
estimatedDays: 2,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'zone-2',
|
|
34
|
+
name: 'Victoria Island',
|
|
35
|
+
deliveryCost: 2000,
|
|
36
|
+
freeShippingThreshold: 15000,
|
|
37
|
+
allowCOD: true,
|
|
38
|
+
allowOnline: true,
|
|
39
|
+
waybillOnly: false,
|
|
40
|
+
estimatedDays: 1,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'zone-3',
|
|
44
|
+
name: 'Lekki',
|
|
45
|
+
deliveryCost: 2500,
|
|
46
|
+
freeShippingThreshold: 15000,
|
|
47
|
+
allowCOD: true,
|
|
48
|
+
allowOnline: true,
|
|
49
|
+
waybillOnly: false,
|
|
50
|
+
estimatedDays: 2,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'state-2',
|
|
56
|
+
name: 'Abuja',
|
|
57
|
+
zones: [
|
|
58
|
+
{
|
|
59
|
+
id: 'zone-4',
|
|
60
|
+
name: 'Wuse',
|
|
61
|
+
deliveryCost: 3000,
|
|
62
|
+
freeShippingThreshold: 20000,
|
|
63
|
+
allowCOD: true,
|
|
64
|
+
allowOnline: true,
|
|
65
|
+
waybillOnly: false,
|
|
66
|
+
estimatedDays: 3,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'zone-5',
|
|
70
|
+
name: 'Garki',
|
|
71
|
+
deliveryCost: 3000,
|
|
72
|
+
freeShippingThreshold: 20000,
|
|
73
|
+
allowCOD: true,
|
|
74
|
+
allowOnline: true,
|
|
75
|
+
waybillOnly: false,
|
|
76
|
+
estimatedDays: 3,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
// Create a query client with mocked delivery zones data
|
|
83
|
+
const createMockQueryClient = () => {
|
|
84
|
+
const queryClient = new QueryClient({
|
|
85
|
+
defaultOptions: {
|
|
86
|
+
queries: {
|
|
87
|
+
retry: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Pre-populate the cache with mock delivery zones
|
|
93
|
+
queryClient.setQueryData(['deliveryZones', 'mock-brand-456'], mockDeliveryZones);
|
|
94
|
+
|
|
95
|
+
return queryClient;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Mock cart data
|
|
99
|
+
const mockCart: Cart = {
|
|
100
|
+
id: 'mock-cart-123',
|
|
101
|
+
expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24).toISOString(),
|
|
102
|
+
brand: {
|
|
103
|
+
id: 'mock-brand-456',
|
|
104
|
+
name: 'Demo Store',
|
|
105
|
+
slug: 'demo-store',
|
|
106
|
+
logoUrl: 'https://images.unsplash.com/photo-1599305445671-ac291c95aaa9?w=200&h=80&fit=crop',
|
|
107
|
+
siteUrl: 'https://demo-store.com',
|
|
108
|
+
domain: 'demo-store.com',
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
updatedAt: new Date().toISOString(),
|
|
111
|
+
deletedAt: null,
|
|
112
|
+
},
|
|
113
|
+
customerPhone: null,
|
|
114
|
+
customerEmail: null,
|
|
115
|
+
customerFirstName: null,
|
|
116
|
+
customerLastName: null,
|
|
117
|
+
availablePaymentMethods: ['cod'],
|
|
118
|
+
deliveryZone: null,
|
|
119
|
+
recoveryAttempts: 0,
|
|
120
|
+
lastRecoveryAttemptAt: null,
|
|
121
|
+
recoveryDiscountCode: null,
|
|
122
|
+
items: [
|
|
123
|
+
{
|
|
124
|
+
id: 'item-1',
|
|
125
|
+
variant: {
|
|
126
|
+
id: 'variant-1',
|
|
127
|
+
productId: 'product-1',
|
|
128
|
+
name: 'Medium - Blue',
|
|
129
|
+
price: 2999,
|
|
130
|
+
sku: 'TSHIRT-MD-BLUE',
|
|
131
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
|
|
132
|
+
trackInventory: true,
|
|
133
|
+
lowStockThreshold: 10,
|
|
134
|
+
isActive: true,
|
|
135
|
+
createdAt: new Date().toISOString(),
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
deletedAt: null,
|
|
138
|
+
product: {
|
|
139
|
+
id: 'product-1',
|
|
140
|
+
name: 'Premium Cotton T-Shirt',
|
|
141
|
+
slug: 'cotton-shirt',
|
|
142
|
+
brandId: 'mock-brand-456',
|
|
143
|
+
isActive: true,
|
|
144
|
+
description: 'Comfortable premium cotton t-shirt',
|
|
145
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
|
|
146
|
+
createdAt: new Date().toISOString(),
|
|
147
|
+
updatedAt: new Date().toISOString(),
|
|
148
|
+
deletedAt: null,
|
|
149
|
+
quantityDiscounts: null,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
quantity: 2,
|
|
153
|
+
basePrice: 2999,
|
|
154
|
+
discountPercent: 0,
|
|
155
|
+
finalPrice: 2599,
|
|
156
|
+
subtotal: 5198,
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'item-2',
|
|
160
|
+
variant: {
|
|
161
|
+
id: 'variant-2',
|
|
162
|
+
productId: 'product-2',
|
|
163
|
+
name: 'Large - Black',
|
|
164
|
+
price: 3499,
|
|
165
|
+
sku: 'JEANS-LG-BLACK',
|
|
166
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
|
|
167
|
+
trackInventory: true,
|
|
168
|
+
lowStockThreshold: 10,
|
|
169
|
+
isActive: true,
|
|
170
|
+
createdAt: new Date().toISOString(),
|
|
171
|
+
updatedAt: new Date().toISOString(),
|
|
172
|
+
deletedAt: null,
|
|
173
|
+
product: {
|
|
174
|
+
id: 'product-2',
|
|
175
|
+
name: 'Slim Fit Jeans',
|
|
176
|
+
brandId: 'mock-brand-456',
|
|
177
|
+
slug: 'cotton-shirt',
|
|
178
|
+
isActive: true,
|
|
179
|
+
description: 'Modern slim fit jeans',
|
|
180
|
+
thumbnailUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
|
|
181
|
+
createdAt: new Date().toISOString(),
|
|
182
|
+
updatedAt: new Date().toISOString(),
|
|
183
|
+
deletedAt: null,
|
|
184
|
+
quantityDiscounts: null,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
quantity: 1,
|
|
188
|
+
basePrice: 3499,
|
|
189
|
+
discountPercent: 0,
|
|
190
|
+
finalPrice: 3499,
|
|
191
|
+
subtotal: 3499,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
pricing: {
|
|
195
|
+
subtotal: 8697,
|
|
196
|
+
deliveryCharge: 0,
|
|
197
|
+
discount: null,
|
|
198
|
+
total: 8697,
|
|
199
|
+
},
|
|
200
|
+
createdAt: new Date().toISOString(),
|
|
201
|
+
updatedAt: new Date().toISOString(),
|
|
202
|
+
convertedToOrderId: null,
|
|
203
|
+
wasRecovered: false,
|
|
204
|
+
recoveryUrl: 'https://demo-store.com?cartId=mock-cart-123',
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const meta = {
|
|
208
|
+
title: 'Components/Checkout',
|
|
209
|
+
component: Checkout,
|
|
210
|
+
parameters: {
|
|
211
|
+
layout: 'centered',
|
|
212
|
+
},
|
|
213
|
+
tags: ['autodocs'],
|
|
214
|
+
} satisfies Meta<typeof Checkout>;
|
|
215
|
+
|
|
216
|
+
export default meta;
|
|
217
|
+
|
|
218
|
+
// Wrapper component to handle modal state and provide mock data
|
|
219
|
+
const CheckoutWrapper = (args: any) => {
|
|
220
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
221
|
+
const queryClient = createMockQueryClient();
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<QueryClientProvider client={queryClient}>
|
|
225
|
+
<MockCartProvider cart={mockCart} renderCart={false}>
|
|
226
|
+
<div className="flex flex-col items-center gap-4 p-8">
|
|
227
|
+
<div className="text-center">
|
|
228
|
+
<h2 className="text-2xl font-bold mb-2">Checkout Flow Demo</h2>
|
|
229
|
+
<p className="text-gray-600 mb-4">
|
|
230
|
+
Click the button below to start the checkout process with mock data
|
|
231
|
+
</p>
|
|
232
|
+
</div>
|
|
233
|
+
<Button onClick={() => setIsOpen(true)} size="lg">
|
|
234
|
+
Start Checkout
|
|
235
|
+
</Button>
|
|
236
|
+
</div>
|
|
237
|
+
<Checkout
|
|
238
|
+
{...args}
|
|
239
|
+
isOpen={isOpen}
|
|
240
|
+
onClose={() => setIsOpen(false)}
|
|
241
|
+
onSuccess={(orderId) => {
|
|
242
|
+
args.onSuccess?.(orderId);
|
|
243
|
+
}}
|
|
244
|
+
onError={(error) => {
|
|
245
|
+
console.error('Checkout error:', error);
|
|
246
|
+
alert(`Checkout failed: ${error.message}`);
|
|
247
|
+
args.onError?.(error);
|
|
248
|
+
}}
|
|
249
|
+
/>
|
|
250
|
+
</MockCartProvider>
|
|
251
|
+
</QueryClientProvider>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const mockRefundPolicy = {
|
|
256
|
+
title: '🤔 Curious about Refunds?',
|
|
257
|
+
policies: [
|
|
258
|
+
'2 day return window from delivery date',
|
|
259
|
+
'Items must be unused and in original packaging',
|
|
260
|
+
'Refunds processed as soon as item is received',
|
|
261
|
+
'Refunds issued to original payment method',
|
|
262
|
+
'Contact support@example.com to initiate refund',
|
|
263
|
+
],
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Type for story args that excludes modal-controlled props
|
|
267
|
+
type CheckoutStoryArgs = Omit<CheckoutProps, 'isOpen' | 'onClose'>;
|
|
268
|
+
|
|
269
|
+
// Default checkout with mock data
|
|
270
|
+
export const Default: StoryObj<CheckoutStoryArgs> = {
|
|
271
|
+
render: (args) => <CheckoutWrapper {...args} />,
|
|
272
|
+
args: {
|
|
273
|
+
onSuccess: fn(),
|
|
274
|
+
onError: fn(),
|
|
275
|
+
refundPolicy: mockRefundPolicy,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Checkout with pre-filled customer data
|
|
280
|
+
export const WithPrefilledData: StoryObj<CheckoutStoryArgs> = {
|
|
281
|
+
render: (args) => <CheckoutWrapper {...args} />,
|
|
282
|
+
args: {
|
|
283
|
+
...Default.args,
|
|
284
|
+
initialData: {
|
|
285
|
+
firstName: 'John',
|
|
286
|
+
lastName: 'Doe',
|
|
287
|
+
email: 'john.doe@example.com',
|
|
288
|
+
phone: '08012345678',
|
|
289
|
+
address: '123 Main Street',
|
|
290
|
+
city: 'Ikeja',
|
|
291
|
+
deliveryZoneId: 'zone-1',
|
|
292
|
+
paymentMethod: 'online',
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// Without refund policy accordion
|
|
298
|
+
export const NoRefundPolicy: StoryObj<CheckoutStoryArgs> = {
|
|
299
|
+
render: (args) => <CheckoutWrapper {...args} />,
|
|
300
|
+
args: {
|
|
301
|
+
...Default.args,
|
|
302
|
+
refundPolicy: undefined,
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Cash on delivery pre-selected
|
|
307
|
+
export const CashOnDeliverySelected: StoryObj<CheckoutStoryArgs> = {
|
|
308
|
+
render: (args) => <CheckoutWrapper {...args} />,
|
|
309
|
+
args: {
|
|
310
|
+
...Default.args,
|
|
311
|
+
initialData: {
|
|
312
|
+
paymentMethod: 'cod',
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
// Cart with discount applied
|
|
318
|
+
export const WithDiscount: StoryObj<CheckoutStoryArgs> = {
|
|
319
|
+
render: (args) => {
|
|
320
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
321
|
+
const queryClient = createMockQueryClient();
|
|
322
|
+
|
|
323
|
+
const cartWithDiscount: Cart = {
|
|
324
|
+
...mockCart,
|
|
325
|
+
pricing: {
|
|
326
|
+
subtotal: 8697,
|
|
327
|
+
discount: {
|
|
328
|
+
code: 'SAVE10',
|
|
329
|
+
type: 'percentage',
|
|
330
|
+
value: 10,
|
|
331
|
+
amount: 870,
|
|
332
|
+
description: '10% off your order',
|
|
333
|
+
},
|
|
334
|
+
deliveryCharge: 0,
|
|
335
|
+
total: 7827,
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<QueryClientProvider client={queryClient}>
|
|
341
|
+
<MockCartProvider cart={cartWithDiscount} renderCart={false}>
|
|
342
|
+
<div className="flex flex-col items-center gap-4 p-8">
|
|
343
|
+
<div className="text-center">
|
|
344
|
+
<h2 className="text-2xl font-bold mb-2">Checkout with Discount</h2>
|
|
345
|
+
<p className="text-gray-600 mb-4">10% discount applied to cart</p>
|
|
346
|
+
</div>
|
|
347
|
+
<Button onClick={() => setIsOpen(true)} size="lg">
|
|
348
|
+
Start Checkout
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
<Checkout
|
|
352
|
+
{...args}
|
|
353
|
+
isOpen={isOpen}
|
|
354
|
+
onClose={() => setIsOpen(false)}
|
|
355
|
+
onSuccess={(orderId) => {
|
|
356
|
+
args.onSuccess?.(orderId);
|
|
357
|
+
}}
|
|
358
|
+
onError={(error) => {
|
|
359
|
+
console.error('Checkout error:', error);
|
|
360
|
+
alert(`Checkout failed: ${error.message}`);
|
|
361
|
+
args.onError?.(error);
|
|
362
|
+
}}
|
|
363
|
+
/>
|
|
364
|
+
</MockCartProvider>
|
|
365
|
+
</QueryClientProvider>
|
|
366
|
+
);
|
|
367
|
+
},
|
|
368
|
+
args: {
|
|
369
|
+
...Default.args,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Custom submit button text
|
|
374
|
+
export const CustomButtonText: StoryObj<CheckoutStoryArgs> = {
|
|
375
|
+
render: (args) => <CheckoutWrapper {...args} />,
|
|
376
|
+
args: {
|
|
377
|
+
...Default.args,
|
|
378
|
+
submitButtonText: 'Complete Purchase',
|
|
379
|
+
},
|
|
380
|
+
};
|