@instockng/storefront-ui 1.0.10 → 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.
- package/README.md +26 -0
- package/dist/components/Checkout.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/index111.mjs +1 -1
- package/dist/index20.mjs +2 -2
- package/dist/index21.mjs +1 -1
- package/dist/index28.mjs +11 -11
- package/dist/index29.mjs +1 -1
- package/dist/index3.mjs +83 -73
- package/dist/index30.mjs +1 -1
- package/dist/index31.mjs +1 -1
- package/dist/index32.mjs +1 -1
- 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 +66 -49
- 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/index58.mjs +2 -2
- package/dist/index59.mjs +4 -3
- package/dist/index60.mjs +4 -2
- package/dist/index61.mjs +2 -5
- 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/index80.mjs +69 -43
- package/dist/index81.mjs +1 -1
- package/dist/index82.mjs +1 -1
- package/dist/index83.mjs +5 -5
- package/dist/index85.mjs +2 -2
- package/dist/index87.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 +1 -1
- package/dist/index94.mjs +3 -3
- 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 +262 -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 +305 -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,76 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { DiscountCodeInput } from './DiscountCodeInput';
|
|
3
|
+
import { MockCartProvider } from '@/test-utils/MockCartProvider';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/DiscountCodeInput',
|
|
7
|
+
component: DiscountCodeInput,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'padded',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
} satisfies Meta<typeof DiscountCodeInput>;
|
|
13
|
+
|
|
14
|
+
export default meta;
|
|
15
|
+
type Story = StoryObj<typeof meta>;
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
decorators: [
|
|
19
|
+
(Story) => (
|
|
20
|
+
<MockCartProvider>
|
|
21
|
+
<Story />
|
|
22
|
+
</MockCartProvider>
|
|
23
|
+
),
|
|
24
|
+
],
|
|
25
|
+
args: {
|
|
26
|
+
onSuccess: () => {
|
|
27
|
+
console.log('Discount applied successfully');
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const WithCurrentCode: Story = {
|
|
33
|
+
decorators: [
|
|
34
|
+
(Story) => (
|
|
35
|
+
<MockCartProvider
|
|
36
|
+
cart={{
|
|
37
|
+
pricing: {
|
|
38
|
+
discount: {
|
|
39
|
+
code: 'SAVE20',
|
|
40
|
+
type: 'percentage',
|
|
41
|
+
value: 20,
|
|
42
|
+
amount: 2000,
|
|
43
|
+
description: '20% off your order',
|
|
44
|
+
},
|
|
45
|
+
subtotal: 10000,
|
|
46
|
+
deliveryCharge: 0,
|
|
47
|
+
total: 8000,
|
|
48
|
+
},
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Story />
|
|
52
|
+
</MockCartProvider>
|
|
53
|
+
),
|
|
54
|
+
],
|
|
55
|
+
args: {
|
|
56
|
+
onSuccess: () => {
|
|
57
|
+
console.log('Discount applied successfully');
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const WithCustomPlaceholder: Story = {
|
|
63
|
+
decorators: [
|
|
64
|
+
(Story) => (
|
|
65
|
+
<MockCartProvider>
|
|
66
|
+
<Story />
|
|
67
|
+
</MockCartProvider>
|
|
68
|
+
),
|
|
69
|
+
],
|
|
70
|
+
args: {
|
|
71
|
+
placeholder: 'Enter promo code here',
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
console.log('Discount applied successfully');
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DiscountCodeInput Component
|
|
3
|
+
*
|
|
4
|
+
* Input field for applying/removing discount codes with validation feedback.
|
|
5
|
+
* Uses CartProvider for cart operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { useCart } from '../contexts/CartContext';
|
|
10
|
+
import { Card, CardContent } from './ui/card';
|
|
11
|
+
import { Button } from './ui/button';
|
|
12
|
+
import { Badge } from './ui/badge';
|
|
13
|
+
import { Tag, X, CheckCircle, AlertCircle } from 'lucide-react';
|
|
14
|
+
import { cn } from '../lib/utils';
|
|
15
|
+
|
|
16
|
+
export interface DiscountCodeInputProps {
|
|
17
|
+
/** Callback on successful apply/remove */
|
|
18
|
+
onSuccess?: () => void;
|
|
19
|
+
/** Custom class name */
|
|
20
|
+
className?: string;
|
|
21
|
+
/** Placeholder text */
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
/** Custom class name for the input */
|
|
24
|
+
inputClassName?: string;
|
|
25
|
+
/** Custom class name for the apply button */
|
|
26
|
+
buttonClassName?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function DiscountCodeInput({
|
|
30
|
+
onSuccess,
|
|
31
|
+
className,
|
|
32
|
+
placeholder = 'Enter discount code',
|
|
33
|
+
inputClassName,
|
|
34
|
+
buttonClassName,
|
|
35
|
+
}: DiscountCodeInputProps) {
|
|
36
|
+
const [code, setCode] = useState('');
|
|
37
|
+
const [successMessage, setSuccessMessage] = useState('');
|
|
38
|
+
const [isApplying, setIsApplying] = useState(false);
|
|
39
|
+
const [isRemoving, setIsRemoving] = useState(false);
|
|
40
|
+
const [errorMessage, setErrorMessage] = useState('');
|
|
41
|
+
|
|
42
|
+
const { applyDiscount, removeDiscount, cart } = useCart();
|
|
43
|
+
|
|
44
|
+
const handleApply = async (e: React.FormEvent) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (!code.trim()) return;
|
|
47
|
+
|
|
48
|
+
setIsApplying(true);
|
|
49
|
+
setErrorMessage('');
|
|
50
|
+
try {
|
|
51
|
+
await applyDiscount(code.trim());
|
|
52
|
+
setSuccessMessage('Discount code applied!');
|
|
53
|
+
setCode('');
|
|
54
|
+
setTimeout(() => setSuccessMessage(''), 3000);
|
|
55
|
+
onSuccess?.();
|
|
56
|
+
} catch (err: any) {
|
|
57
|
+
setErrorMessage(err?.message || 'Invalid or expired discount code');
|
|
58
|
+
} finally {
|
|
59
|
+
setIsApplying(false);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const handleRemove = async () => {
|
|
64
|
+
setIsRemoving(true);
|
|
65
|
+
setErrorMessage('');
|
|
66
|
+
try {
|
|
67
|
+
await removeDiscount();
|
|
68
|
+
setSuccessMessage('Discount code removed');
|
|
69
|
+
setTimeout(() => setSuccessMessage(''), 3000);
|
|
70
|
+
onSuccess?.();
|
|
71
|
+
} catch (err: any) {
|
|
72
|
+
setErrorMessage(err?.message || 'Failed to remove discount code');
|
|
73
|
+
} finally {
|
|
74
|
+
setIsRemoving(false);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className={cn('', className)}>
|
|
80
|
+
<div className="pt-6">
|
|
81
|
+
<div className="space-y-3">
|
|
82
|
+
{/* Label */}
|
|
83
|
+
<div className="flex items-center gap-2 text-sm font-medium">
|
|
84
|
+
<Tag className="h-4 w-4" />
|
|
85
|
+
Discount Code
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Current Code Display */}
|
|
89
|
+
{cart && cart.pricing.discount && (
|
|
90
|
+
<div className="flex items-center justify-between rounded-md bg-green-50 p-3">
|
|
91
|
+
<div className="flex items-center gap-2">
|
|
92
|
+
<CheckCircle className="h-4 w-4 text-green-600" />
|
|
93
|
+
<span className="text-sm font-medium text-green-900">
|
|
94
|
+
Code Applied:
|
|
95
|
+
</span>
|
|
96
|
+
<Badge className="bg-green-600 text-white">{cart.pricing.discount.code}</Badge>
|
|
97
|
+
</div>
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
size="sm"
|
|
101
|
+
onClick={handleRemove}
|
|
102
|
+
disabled={isRemoving}
|
|
103
|
+
className="h-8 px-2 text-green-700 hover:text-green-800"
|
|
104
|
+
>
|
|
105
|
+
{isRemoving ? (
|
|
106
|
+
'Removing...'
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<X className="h-4 w-4" />
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</Button>
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{/* Input Form */}
|
|
117
|
+
{!cart?.pricing?.discount && (
|
|
118
|
+
<form onSubmit={handleApply} className="flex gap-2">
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
value={code}
|
|
122
|
+
onChange={(e) => setCode(e.target.value.toUpperCase())}
|
|
123
|
+
placeholder={placeholder}
|
|
124
|
+
disabled={isApplying}
|
|
125
|
+
className={cn(
|
|
126
|
+
'flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm',
|
|
127
|
+
'focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50',
|
|
128
|
+
'disabled:cursor-not-allowed disabled:opacity-50',
|
|
129
|
+
errorMessage && 'border-red-500 focus:border-red-500 focus:ring-red-500',
|
|
130
|
+
inputClassName
|
|
131
|
+
)}
|
|
132
|
+
/>
|
|
133
|
+
<Button
|
|
134
|
+
type="submit"
|
|
135
|
+
disabled={!code.trim() || isApplying}
|
|
136
|
+
className={cn('bg-accent-500 text-white hover:bg-accent-600', buttonClassName)}
|
|
137
|
+
>
|
|
138
|
+
{isApplying ? 'Applying...' : 'Apply'}
|
|
139
|
+
</Button>
|
|
140
|
+
</form>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* Error Message */}
|
|
144
|
+
{errorMessage && (
|
|
145
|
+
<div className="flex items-center gap-2 text-sm text-red-600">
|
|
146
|
+
<AlertCircle className="h-4 w-4" />
|
|
147
|
+
{errorMessage}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Success Message */}
|
|
152
|
+
{successMessage && (
|
|
153
|
+
<div className="flex items-center gap-2 text-sm text-green-600">
|
|
154
|
+
<CheckCircle className="h-4 w-4" />
|
|
155
|
+
{successMessage}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { OrderConfirmation } from './OrderConfirmation';
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
4
|
+
import { fn } from '@storybook/test';
|
|
5
|
+
|
|
6
|
+
// Mock order data
|
|
7
|
+
const mockOrderResponse = {
|
|
8
|
+
order: {
|
|
9
|
+
id: 'order-123',
|
|
10
|
+
orderNumber: 1234,
|
|
11
|
+
status: 'prospect' as const,
|
|
12
|
+
brand: {
|
|
13
|
+
name: 'Fashion Store',
|
|
14
|
+
logoUrl: 'https://via.placeholder.com/150x50/4F46E5/FFFFFF?text=Fashion+Store',
|
|
15
|
+
},
|
|
16
|
+
customer: {
|
|
17
|
+
firstName: 'John',
|
|
18
|
+
lastName: 'Doe',
|
|
19
|
+
phone: '+1234567890',
|
|
20
|
+
address: '123 Main St',
|
|
21
|
+
city: 'New York',
|
|
22
|
+
},
|
|
23
|
+
items: [
|
|
24
|
+
{
|
|
25
|
+
id: 'item-1',
|
|
26
|
+
variant: {
|
|
27
|
+
name: 'Medium - Blue',
|
|
28
|
+
price: '29.99',
|
|
29
|
+
sku: 'TSHIRT-MD-BLUE',
|
|
30
|
+
product: {
|
|
31
|
+
name: 'Premium Cotton T-Shirt',
|
|
32
|
+
imageUrl: 'https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?w=400',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
quantity: 2,
|
|
36
|
+
subtotal: '59.98',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'item-2',
|
|
40
|
+
variant: {
|
|
41
|
+
name: 'Large - Black',
|
|
42
|
+
price: '59.99',
|
|
43
|
+
sku: 'JEANS-LG-BLACK',
|
|
44
|
+
product: {
|
|
45
|
+
name: 'Slim Fit Jeans',
|
|
46
|
+
imageUrl: 'https://images.unsplash.com/photo-1542272604-787c3835535d?w=400',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
quantity: 1,
|
|
50
|
+
subtotal: '59.99',
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
subtotal: '119.97',
|
|
54
|
+
deliveryFee: '5.99',
|
|
55
|
+
total: '125.96',
|
|
56
|
+
deliveryZone: {
|
|
57
|
+
name: 'Manhattan Downtown',
|
|
58
|
+
},
|
|
59
|
+
paymentMethod: 'cod' as const,
|
|
60
|
+
createdAt: new Date().toISOString(),
|
|
61
|
+
},
|
|
62
|
+
canConfirm: true,
|
|
63
|
+
confirmationMessage: 'Please review your order details and click confirm to proceed.',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const queryClient = new QueryClient({
|
|
67
|
+
defaultOptions: {
|
|
68
|
+
queries: {
|
|
69
|
+
retry: false,
|
|
70
|
+
queryFn: async () => mockOrderResponse,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const meta: Meta<typeof OrderConfirmation> = {
|
|
76
|
+
title: 'Components/OrderConfirmation',
|
|
77
|
+
component: OrderConfirmation,
|
|
78
|
+
parameters: {
|
|
79
|
+
layout: 'fullscreen',
|
|
80
|
+
},
|
|
81
|
+
tags: ['autodocs'],
|
|
82
|
+
decorators: [
|
|
83
|
+
(Story) => (
|
|
84
|
+
<QueryClientProvider client={queryClient}>
|
|
85
|
+
<Story />
|
|
86
|
+
</QueryClientProvider>
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
} satisfies Meta<typeof OrderConfirmation>;
|
|
90
|
+
|
|
91
|
+
export default meta;
|
|
92
|
+
type Story = StoryObj<typeof meta>;
|
|
93
|
+
|
|
94
|
+
export const Default: Story = {
|
|
95
|
+
args: {
|
|
96
|
+
orderId: 'order-123',
|
|
97
|
+
token: 'token-abc',
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const WithBrandLogo: Story = {
|
|
102
|
+
args: {
|
|
103
|
+
orderId: 'order-123',
|
|
104
|
+
token: 'token-abc',
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const CustomCurrency: Story = {
|
|
109
|
+
args: {
|
|
110
|
+
orderId: 'order-123',
|
|
111
|
+
token: 'token-abc',
|
|
112
|
+
currency: 'EUR',
|
|
113
|
+
locale: 'de-DE',
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const CustomWhatsApp: Story = {
|
|
118
|
+
args: {
|
|
119
|
+
orderId: 'order-123',
|
|
120
|
+
token: 'token-abc',
|
|
121
|
+
whatsappHelpLink: 'https://wa.me/1234567890',
|
|
122
|
+
whatsappHelpNumber: '+1 (234) 567-890',
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const WithCallbacks: Story = {
|
|
127
|
+
args: {
|
|
128
|
+
orderId: 'order-123',
|
|
129
|
+
token: 'token-abc',
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const CustomStyling: Story = {
|
|
134
|
+
args: {
|
|
135
|
+
orderId: 'order-123',
|
|
136
|
+
token: 'token-abc',
|
|
137
|
+
className: 'bg-blue-50',
|
|
138
|
+
headerClassName: 'text-blue-900',
|
|
139
|
+
buttonClassName: 'bg-blue-600 hover:bg-blue-700',
|
|
140
|
+
cardClassName: 'border-blue-200 shadow-xl',
|
|
141
|
+
},
|
|
142
|
+
};
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OrderConfirmation Component
|
|
3
|
+
*
|
|
4
|
+
* A pre-built order confirmation page for e-commerce sites.
|
|
5
|
+
* Displays order details and allows customers to confirm prospect orders.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState } from 'react';
|
|
9
|
+
import { useGetOrder, useConfirmOrder } from '@oms/api-client';
|
|
10
|
+
import { WHATSAPP_HELP_LINK, WHATSAPP_HELP_NUMBER_FORMATTED } from '@oms/shared';
|
|
11
|
+
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
|
12
|
+
import { Button } from './ui/button';
|
|
13
|
+
import { Badge } from './ui/badge';
|
|
14
|
+
import { CheckCircle, XCircle, Loader2, Package } from 'lucide-react';
|
|
15
|
+
import { formatCurrency, formatDateTime, cn } from '../lib/utils';
|
|
16
|
+
|
|
17
|
+
export interface OrderConfirmationProps {
|
|
18
|
+
/** Order UUID */
|
|
19
|
+
orderId: string;
|
|
20
|
+
/** User action token for authentication */
|
|
21
|
+
token: string;
|
|
22
|
+
|
|
23
|
+
/** Custom class name for root container */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Custom class name for header section */
|
|
26
|
+
headerClassName?: string;
|
|
27
|
+
/** Custom class name for buttons */
|
|
28
|
+
buttonClassName?: string;
|
|
29
|
+
/** Custom class name for cards */
|
|
30
|
+
cardClassName?: string;
|
|
31
|
+
|
|
32
|
+
/** WhatsApp help link (defaults to shared constant) */
|
|
33
|
+
whatsappHelpLink?: string;
|
|
34
|
+
/** WhatsApp help number formatted (defaults to shared constant) */
|
|
35
|
+
whatsappHelpNumber?: string;
|
|
36
|
+
|
|
37
|
+
/** Currency code for formatting */
|
|
38
|
+
currency?: string;
|
|
39
|
+
/** Locale for date/currency formatting */
|
|
40
|
+
locale?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function OrderConfirmation({
|
|
44
|
+
orderId,
|
|
45
|
+
token,
|
|
46
|
+
className,
|
|
47
|
+
headerClassName,
|
|
48
|
+
buttonClassName,
|
|
49
|
+
cardClassName,
|
|
50
|
+
whatsappHelpLink = WHATSAPP_HELP_LINK,
|
|
51
|
+
whatsappHelpNumber = WHATSAPP_HELP_NUMBER_FORMATTED,
|
|
52
|
+
currency = 'NGN',
|
|
53
|
+
locale = 'en-NG',
|
|
54
|
+
}: OrderConfirmationProps) {
|
|
55
|
+
const [confirmed, setConfirmed] = useState(false);
|
|
56
|
+
|
|
57
|
+
const { data, isLoading, error } = useGetOrder(orderId, token, {
|
|
58
|
+
retry: false,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const confirmMutation = useConfirmOrder({
|
|
62
|
+
onSuccess: (response) => {
|
|
63
|
+
setConfirmed(true);
|
|
64
|
+
},
|
|
65
|
+
onError: (err) => {
|
|
66
|
+
// TODO: Sentry
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (isLoading) {
|
|
71
|
+
return (
|
|
72
|
+
<div className={cn("min-h-screen bg-gray-50 flex items-center justify-center", className)}>
|
|
73
|
+
<div className="text-center">
|
|
74
|
+
<Loader2 className="h-12 w-12 animate-spin text-blue-500 mx-auto mb-4" />
|
|
75
|
+
<p className="text-gray-600">Loading order details...</p>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (error || !data || "error" in data) {
|
|
82
|
+
return (
|
|
83
|
+
<div className={cn("min-h-screen bg-gray-50 flex items-center justify-center p-4", className)}>
|
|
84
|
+
<Card className={cn("max-w-md w-full", cardClassName)}>
|
|
85
|
+
<CardHeader>
|
|
86
|
+
<div className="flex items-center space-x-2">
|
|
87
|
+
<XCircle className="h-6 w-6 text-red-500" />
|
|
88
|
+
<CardTitle>Order Not Found</CardTitle>
|
|
89
|
+
</div>
|
|
90
|
+
</CardHeader>
|
|
91
|
+
<CardContent>
|
|
92
|
+
<p className="text-gray-600">
|
|
93
|
+
We couldn't find this order. The link may be invalid or expired. Please contact support if you need assistance.
|
|
94
|
+
</p>
|
|
95
|
+
</CardContent>
|
|
96
|
+
</Card>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// The order is returned directly from the API now
|
|
102
|
+
const order = data;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<div className={cn("min-h-screen bg-gray-50 py-8 px-4", className)}>
|
|
106
|
+
<div className="max-w-3xl mx-auto space-y-6">
|
|
107
|
+
{/* Header */}
|
|
108
|
+
<div className={cn("text-center", headerClassName)}>
|
|
109
|
+
{order.brand.logoUrl ? (
|
|
110
|
+
<div className="inline-flex items-center justify-center w-24 h-16 mb-4">
|
|
111
|
+
<img
|
|
112
|
+
src={order.brand.logoUrl}
|
|
113
|
+
alt={`${order.brand.name} logo`}
|
|
114
|
+
className="max-w-full max-h-full object-contain"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
) : (
|
|
118
|
+
<div className="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
|
|
119
|
+
<Package className="h-8 w-8 text-blue-600" />
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
<h1 className="text-3xl font-bold text-gray-900">Order #{order.orderNumber}</h1>
|
|
123
|
+
<p className="text-gray-600 mt-2">{order.brand.name}</p>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
{/* Confirmation Status */}
|
|
127
|
+
{confirmed && (
|
|
128
|
+
<Card className={cn("border-green-200 bg-green-50", cardClassName)}>
|
|
129
|
+
<CardContent className="pt-6">
|
|
130
|
+
<div className="flex items-start space-x-3">
|
|
131
|
+
<CheckCircle className="h-6 w-6 text-green-600 mt-0.5" />
|
|
132
|
+
<div>
|
|
133
|
+
<h3 className="font-semibold text-green-900">Order Confirmed!</h3>
|
|
134
|
+
<p className="text-green-700 mt-1">
|
|
135
|
+
Thank you for confirming your order. We will contact you soon to arrange delivery.
|
|
136
|
+
</p>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</CardContent>
|
|
140
|
+
</Card>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* Status Message */}
|
|
144
|
+
{!confirmed && order.confirmationMessage && (
|
|
145
|
+
<Card className={cn(order.canConfirm ? 'border-orange-200 bg-orange-50' : '', cardClassName)}>
|
|
146
|
+
<CardContent className="pt-6">
|
|
147
|
+
<p className={order.canConfirm ? 'text-orange-900' : 'text-gray-700'}>
|
|
148
|
+
{order.confirmationMessage}
|
|
149
|
+
</p>
|
|
150
|
+
</CardContent>
|
|
151
|
+
</Card>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
{/* Confirm Button */}
|
|
155
|
+
{order.canConfirm && !confirmed && (
|
|
156
|
+
<Card className={cardClassName}>
|
|
157
|
+
<CardContent className="pt-6">
|
|
158
|
+
<Button
|
|
159
|
+
onClick={() => confirmMutation.mutate({ orderId, token })}
|
|
160
|
+
disabled={confirmMutation.isPending}
|
|
161
|
+
className={cn("w-full", buttonClassName)}
|
|
162
|
+
size="lg"
|
|
163
|
+
>
|
|
164
|
+
{confirmMutation.isPending ? (
|
|
165
|
+
<>
|
|
166
|
+
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
|
|
167
|
+
Confirming...
|
|
168
|
+
</>
|
|
169
|
+
) : (
|
|
170
|
+
'Confirm Order'
|
|
171
|
+
)}
|
|
172
|
+
</Button>
|
|
173
|
+
{confirmMutation.isError && (
|
|
174
|
+
<p className="text-red-600 text-sm mt-2 text-center">
|
|
175
|
+
Failed to confirm order. Please try again or contact support.
|
|
176
|
+
</p>
|
|
177
|
+
)}
|
|
178
|
+
</CardContent>
|
|
179
|
+
</Card>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Order Details */}
|
|
183
|
+
<Card className={cardClassName}>
|
|
184
|
+
<CardHeader>
|
|
185
|
+
<CardTitle>Order Details</CardTitle>
|
|
186
|
+
</CardHeader>
|
|
187
|
+
<CardContent className="space-y-4">
|
|
188
|
+
<div className="grid grid-cols-2 gap-4">
|
|
189
|
+
<div>
|
|
190
|
+
<p className="text-sm text-gray-600">Status</p>
|
|
191
|
+
<Badge className="mt-1">{order.status}</Badge>
|
|
192
|
+
</div>
|
|
193
|
+
<div>
|
|
194
|
+
<p className="text-sm text-gray-600">Order Date</p>
|
|
195
|
+
<p className="font-medium">{order.createdAt ? formatDateTime(order.createdAt) : 'N/A'}</p>
|
|
196
|
+
</div>
|
|
197
|
+
<div>
|
|
198
|
+
<p className="text-sm text-gray-600">Payment Method</p>
|
|
199
|
+
<p className="font-medium">{order.paymentMethod === 'cod' ? 'Cash on Delivery' : 'Online Payment'}</p>
|
|
200
|
+
</div>
|
|
201
|
+
<div>
|
|
202
|
+
<p className="text-sm text-gray-600">Estimated Delivery</p>
|
|
203
|
+
<p className="font-medium">{order.estimatedDays ? `${order.estimatedDays} days` : 'TBD'}</p>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</CardContent>
|
|
207
|
+
</Card>
|
|
208
|
+
|
|
209
|
+
{/* Delivery Information */}
|
|
210
|
+
<Card className={cardClassName}>
|
|
211
|
+
<CardHeader>
|
|
212
|
+
<CardTitle>Delivery Information</CardTitle>
|
|
213
|
+
</CardHeader>
|
|
214
|
+
<CardContent className="space-y-3">
|
|
215
|
+
<div>
|
|
216
|
+
<p className="text-sm text-gray-600">Customer</p>
|
|
217
|
+
<p className="font-medium">{order.firstName} {order.lastName}</p>
|
|
218
|
+
<p className="text-sm text-gray-600">{order.phone}</p>
|
|
219
|
+
</div>
|
|
220
|
+
<div>
|
|
221
|
+
<p className="text-sm text-gray-600">Address</p>
|
|
222
|
+
<p className="font-medium">{order.address}</p>
|
|
223
|
+
<p className="text-sm text-gray-600">{order.city}, {order.deliveryZone.state.name}</p>
|
|
224
|
+
<p className="text-sm text-gray-600">{order.deliveryZone.name}</p>
|
|
225
|
+
</div>
|
|
226
|
+
</CardContent>
|
|
227
|
+
</Card>
|
|
228
|
+
|
|
229
|
+
{/* Order Items */}
|
|
230
|
+
<Card className={cardClassName}>
|
|
231
|
+
<CardHeader>
|
|
232
|
+
<CardTitle>Items</CardTitle>
|
|
233
|
+
</CardHeader>
|
|
234
|
+
<CardContent>
|
|
235
|
+
<div className="space-y-4">
|
|
236
|
+
{order.items?.map((item) => (
|
|
237
|
+
<div key={item.id} className="flex items-start justify-between border-b pb-4 last:border-0">
|
|
238
|
+
<div className="flex items-start space-x-4 flex-1">
|
|
239
|
+
{(item.variant.thumbnailUrl || item.variant.product.thumbnailUrl) && (
|
|
240
|
+
<img
|
|
241
|
+
src={item.variant.thumbnailUrl || item.variant.product.thumbnailUrl || undefined}
|
|
242
|
+
alt={item.variant.product.name}
|
|
243
|
+
className="w-16 h-16 object-cover rounded"
|
|
244
|
+
/>
|
|
245
|
+
)}
|
|
246
|
+
<div className="flex-1">
|
|
247
|
+
<p className="font-medium">{item.variant.product.name}</p>
|
|
248
|
+
{item.variant.name && (
|
|
249
|
+
<Badge variant="outline" className="mt-1">{item.variant.name}</Badge>
|
|
250
|
+
)}
|
|
251
|
+
<p className="text-sm text-gray-600 mt-1">Quantity: {item.quantity}</p>
|
|
252
|
+
<p className="text-sm text-gray-600">SKU: {item.variant.sku}</p>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div className="text-right">
|
|
256
|
+
<p className="font-medium">{formatCurrency(item.priceAtPurchase * item.quantity)}</p>
|
|
257
|
+
<p className="text-sm text-gray-600">{formatCurrency(item.priceAtPurchase)} each</p>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
))}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
{/* Order Summary */}
|
|
264
|
+
<div className="mt-6 border-t pt-4 space-y-2">
|
|
265
|
+
<div className="flex justify-between text-sm">
|
|
266
|
+
<span className="text-gray-600">Subtotal</span>
|
|
267
|
+
<span className="font-medium">{formatCurrency(order.subtotal)}</span>
|
|
268
|
+
</div>
|
|
269
|
+
{order.discountAmount && order.discountAmount > 0 && (
|
|
270
|
+
<div className="flex justify-between text-sm text-green-600">
|
|
271
|
+
<span>Discount</span>
|
|
272
|
+
<span>-{formatCurrency(order.discountAmount)}</span>
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
<div className="flex justify-between text-sm">
|
|
276
|
+
<span className="text-gray-600">Delivery Charge</span>
|
|
277
|
+
<span className="font-medium">{formatCurrency(order.deliveryCharge)}</span>
|
|
278
|
+
</div>
|
|
279
|
+
<div className="flex justify-between text-lg font-bold border-t pt-2">
|
|
280
|
+
<span>Total</span>
|
|
281
|
+
<span>{formatCurrency(order.totalPrice)}</span>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
</CardContent>
|
|
285
|
+
</Card>
|
|
286
|
+
|
|
287
|
+
{/* Support */}
|
|
288
|
+
<Card className={cardClassName}>
|
|
289
|
+
<CardContent className="pt-6">
|
|
290
|
+
<p className="text-sm text-gray-600 text-center">
|
|
291
|
+
Need help with your order? Call us on {whatsappHelpNumber} or contact us on{' '}
|
|
292
|
+
<a href={whatsappHelpLink} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
|
|
293
|
+
WhatsApp
|
|
294
|
+
</a>
|
|
295
|
+
</p>
|
|
296
|
+
</CardContent>
|
|
297
|
+
</Card>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
);
|
|
301
|
+
}
|