hey-pharmacist-ecommerce 1.1.11 → 1.1.13
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/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +632 -511
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +633 -512
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/CartItem.tsx +63 -42
- package/src/components/FilterChips.tsx +54 -80
- package/src/components/OrderCard.tsx +89 -56
- package/src/components/ProductCard.tsx +131 -55
- package/src/hooks/useOrders.ts +1 -0
- package/src/lib/types/index.ts +1 -0
- package/src/providers/CartProvider.tsx +47 -3
- package/src/screens/CartScreen.tsx +146 -231
- package/src/screens/CheckoutScreen.tsx +30 -61
- package/src/screens/LoginScreen.tsx +1 -1
- package/src/screens/OrdersScreen.tsx +91 -148
- package/src/screens/ProductDetailScreen.tsx +355 -362
- package/src/screens/RegisterScreen.tsx +1 -1
- package/src/screens/ShopScreen.tsx +439 -268
- package/src/screens/WishlistScreen.tsx +80 -76
|
@@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
|
|
|
5
5
|
import {
|
|
6
6
|
ArrowRight,
|
|
7
7
|
BadgePercent,
|
|
8
|
+
CheckCircle2,
|
|
8
9
|
HeartPulse,
|
|
9
10
|
ShieldCheck,
|
|
10
11
|
ShoppingBag,
|
|
@@ -24,126 +25,63 @@ export function CartScreen() {
|
|
|
24
25
|
const { buildPath } = useBasePath();
|
|
25
26
|
|
|
26
27
|
if (!cart || cart.cartBody.items.length === 0) {
|
|
27
|
-
const highlights = [
|
|
28
|
-
{
|
|
29
|
-
icon: ShieldCheck,
|
|
30
|
-
title: 'Pharmacist approved',
|
|
31
|
-
description:
|
|
32
|
-
'Every product passes pharmacist review and cold-chain handling standards.',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
icon: BadgePercent,
|
|
36
|
-
title: 'Bundle savings',
|
|
37
|
-
description: 'Unlock tiered discounts when you combine vitamins, OTC, and wellness kits.',
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
icon: HeartPulse,
|
|
41
|
-
title: 'Personalized guidance',
|
|
42
|
-
description: 'Follow curated collections tailored to your health goals and routines.',
|
|
43
|
-
},
|
|
44
|
-
];
|
|
45
|
-
|
|
46
28
|
return (
|
|
47
|
-
<div className="
|
|
48
|
-
<div className="
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
29
|
+
<div className="bg-white">
|
|
30
|
+
<div className="min-h-screen bg-white max-w-6xl mx-auto">
|
|
31
|
+
<div className="container mx-auto px-4 py-8">
|
|
32
|
+
<div className="mb-6">
|
|
33
|
+
<h1 className="text-2xl font-bold text-slate-900">Shopping Cart</h1>
|
|
34
|
+
<p className="text-sm text-gray-500 mt-1">0 items in your cart</p>
|
|
35
|
+
</div>
|
|
53
36
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<ShoppingBag className="h-4 w-4" />
|
|
64
|
-
Cart status
|
|
65
|
-
</span>
|
|
66
|
-
<div className="space-y-4">
|
|
67
|
-
<h1 className="text-4xl font-bold md:text-5xl">
|
|
68
|
-
Your wellness cart is waiting for a refill
|
|
69
|
-
</h1>
|
|
70
|
-
<p className="max-w-xl text-lg text-white/75">
|
|
71
|
-
Build a pharmacist-curated bundle with fast shipping, personalized recommendations,
|
|
72
|
-
and exclusive member perks designed to keep you feeling your best.
|
|
73
|
-
</p>
|
|
74
|
-
</div>
|
|
75
|
-
<div className="flex flex-wrap gap-4">
|
|
76
|
-
<Button
|
|
77
|
-
size="lg"
|
|
78
|
-
className="bg-white text-primary-700 shadow-xl shadow-white/30 hover:bg-white/90 hover:text-primary-700"
|
|
79
|
-
onClick={() => router.push(buildPath('/shop'))}
|
|
80
|
-
>
|
|
81
|
-
Discover products
|
|
82
|
-
<ArrowRight className="h-5 w-5" />
|
|
83
|
-
</Button>
|
|
84
|
-
<button
|
|
85
|
-
type="button"
|
|
86
|
-
onClick={() => router.push(buildPath('/categories'))}
|
|
87
|
-
className="inline-flex items-center gap-2 rounded-full border border-white/40 px-5 py-3 text-sm font-semibold text-white/80 transition hover:border-white hover:bg-white/10"
|
|
88
|
-
>
|
|
89
|
-
Browse categories
|
|
90
|
-
<ArrowRight className="h-4 w-4" />
|
|
91
|
-
</button>
|
|
92
|
-
</div>
|
|
93
|
-
<div className="grid gap-4 sm:grid-cols-2">
|
|
94
|
-
{highlights.map(({ icon: Icon, title, description }) => (
|
|
95
|
-
<div
|
|
96
|
-
key={title}
|
|
97
|
-
className="rounded-2xl border border-white/15 bg-white/10 p-5 backdrop-blur transition hover:border-white/25 hover:bg-white/15"
|
|
98
|
-
>
|
|
99
|
-
<Icon className="h-6 w-6 text-white/90" />
|
|
100
|
-
<p className="mt-3 text-sm font-semibold text-white">{title}</p>
|
|
101
|
-
<p className="mt-1 text-xs text-white/70">{description}</p>
|
|
37
|
+
<div className="flex flex-col items-center justify-center py-20">
|
|
38
|
+
<motion.div
|
|
39
|
+
initial={{ opacity: 0, y: 24 }}
|
|
40
|
+
animate={{ opacity: 1, y: 0 }}
|
|
41
|
+
className="text-center space-y-6 max-w-md"
|
|
42
|
+
>
|
|
43
|
+
<div className="flex justify-center">
|
|
44
|
+
<div className="rounded-full bg-gray-100 p-6">
|
|
45
|
+
<ShoppingBag className="h-12 w-12 text-gray-400" />
|
|
102
46
|
</div>
|
|
103
|
-
|
|
104
|
-
</div>
|
|
105
|
-
</motion.div>
|
|
47
|
+
</div>
|
|
106
48
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
Start building my cart
|
|
144
|
-
<ArrowRight className="h-5 w-5" />
|
|
145
|
-
</Button>
|
|
146
|
-
</motion.div>
|
|
49
|
+
<div className="space-y-2">
|
|
50
|
+
<h2 className="text-2xl font-bold text-slate-900">
|
|
51
|
+
Your cart is empty
|
|
52
|
+
</h2>
|
|
53
|
+
<p className="text-gray-500">
|
|
54
|
+
Start adding products to your cart to see them here.
|
|
55
|
+
</p>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div className="flex flex-wrap gap-3 justify-center">
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
onClick={() => router.push(buildPath('/shop'))}
|
|
62
|
+
className="rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-6 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
|
63
|
+
>
|
|
64
|
+
Discover products
|
|
65
|
+
<ArrowRight className="h-5 w-5" />
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="mt-8 space-y-3 pt-6 border-t border-gray-200">
|
|
70
|
+
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
71
|
+
<CheckCircle2 className="h-5 w-5 text-primary-600 flex-shrink-0 mt-0.5" />
|
|
72
|
+
<span>Free shipping on all orders</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
75
|
+
<CheckCircle2 className="h-5 w-5 text-primary-600 flex-shrink-0 mt-0.5" />
|
|
76
|
+
<span>Easy returns within 30 days</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
79
|
+
<CheckCircle2 className="h-5 w-5 text-primary-600 flex-shrink-0 mt-0.5" />
|
|
80
|
+
<span>Secure checkout process</span>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</motion.div>
|
|
84
|
+
</div>
|
|
147
85
|
</div>
|
|
148
86
|
</div>
|
|
149
87
|
</div>
|
|
@@ -154,134 +92,111 @@ export function CartScreen() {
|
|
|
154
92
|
const shipping = 0;
|
|
155
93
|
const tax = 0;
|
|
156
94
|
const total = subtotal + shipping + tax;
|
|
157
|
-
|
|
95
|
+
const itemCount = cart.cartBody.items.length;
|
|
158
96
|
|
|
159
97
|
return (
|
|
160
|
-
<div className="
|
|
161
|
-
<
|
|
162
|
-
<div className="
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Review your selections, unlock exclusive perks, and check out with pharmacist-backed
|
|
177
|
-
confidence.
|
|
178
|
-
</p>
|
|
179
|
-
</div>
|
|
180
|
-
<div className="rounded-3xl bg-white/15 p-6 backdrop-blur-md">
|
|
181
|
-
<p className="text-sm font-semibold uppercase tracking-[0.35em] text-white/70">
|
|
182
|
-
Cart summary
|
|
183
|
-
</p>
|
|
184
|
-
<p className="mt-4 text-4xl font-semibold">{formatPrice(total)}</p>
|
|
185
|
-
<p className="text-sm text-white/70">Taxes and shipping calculated below</p>
|
|
186
|
-
</div>
|
|
187
|
-
</motion.div>
|
|
188
|
-
</div>
|
|
189
|
-
</section>
|
|
98
|
+
<div className="bg-white">
|
|
99
|
+
<div className="min-h-screen bg-white max-w-6xl mx-auto">
|
|
100
|
+
<div className="container mx-auto px-4 py-8">
|
|
101
|
+
<div className="mb-6">
|
|
102
|
+
<h1 className="text-2xl font-bold text-slate-900">Shopping Cart</h1>
|
|
103
|
+
<p className="text-sm text-gray-500 mt-1">
|
|
104
|
+
{itemCount} {itemCount === 1 ? 'item' : 'items'} in your cart
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="grid gap-8 lg:grid-cols-[1.5fr_1fr]">
|
|
108
|
+
{/* Shopping Cart Section */}
|
|
109
|
+
<motion.section
|
|
110
|
+
initial={{ opacity: 0, y: 24 }}
|
|
111
|
+
animate={{ opacity: 1, y: 0 }}
|
|
112
|
+
className="space-y-6"
|
|
113
|
+
>
|
|
190
114
|
|
|
191
|
-
<div className="relative -mt-16 pb-20">
|
|
192
|
-
<div className="container mx-auto px-4">
|
|
193
|
-
<div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
|
194
|
-
<motion.section
|
|
195
|
-
initial={{ opacity: 0, y: 24 }}
|
|
196
|
-
animate={{ opacity: 1, y: 0 }}
|
|
197
|
-
transition={{ delay: 0.05 }}
|
|
198
|
-
className="space-y-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-primary-50"
|
|
199
|
-
>
|
|
200
|
-
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
201
|
-
<h2 className="text-xl font-semibold text-slate-900">
|
|
202
|
-
Items ({cart.cartBody.items.length})
|
|
203
|
-
</h2>
|
|
204
|
-
<div className="inline-flex items-center gap-2 rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700">
|
|
205
|
-
<ShieldCheck className="h-4 w-4" />
|
|
206
|
-
Guaranteed cold-chain handling
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
{isLoading && (
|
|
210
|
-
<div className="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-sm text-slate-600">
|
|
211
|
-
<span className="inline-block h-3 w-3 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600" />
|
|
212
|
-
Updating cart…
|
|
213
|
-
</div>
|
|
214
|
-
)}
|
|
215
|
-
<div className="space-y-5">
|
|
216
|
-
{cart.cartBody.items.map((item: CartItemPopulated) => (
|
|
217
|
-
<CartItem key={item.productVariantId} item={item} />
|
|
218
|
-
))}
|
|
219
|
-
</div>
|
|
220
|
-
</motion.section>
|
|
221
115
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<div className="
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
Savings applied
|
|
234
|
-
</span>
|
|
116
|
+
{isLoading && (
|
|
117
|
+
<div className="flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm text-slate-600">
|
|
118
|
+
<span className="inline-block h-4 w-4 animate-spin rounded-full border-2 border-slate-300 border-t-slate-600" />
|
|
119
|
+
Updating cart…
|
|
120
|
+
</div>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<div className="space-y-4">
|
|
124
|
+
{cart.cartBody.items.map((item: CartItemPopulated) => (
|
|
125
|
+
<CartItem key={item.productVariantId} item={item} />
|
|
126
|
+
))}
|
|
235
127
|
</div>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
128
|
+
</motion.section>
|
|
129
|
+
|
|
130
|
+
{/* Order Summary Section */}
|
|
131
|
+
<motion.aside
|
|
132
|
+
initial={{ opacity: 0, y: 24 }}
|
|
133
|
+
animate={{ opacity: 1, y: 0 }}
|
|
134
|
+
transition={{ delay: 0.1 }}
|
|
135
|
+
className="space-y-6 lg:sticky lg:top-8 h-fit"
|
|
136
|
+
>
|
|
137
|
+
<div className="rounded-xl border-2 border-primary-200 bg-white p-6 shadow-md">
|
|
138
|
+
<h2 className="text-xl font-bold text-slate-900 mb-6">Order Summary</h2>
|
|
139
|
+
|
|
140
|
+
<div className="space-y-4 mb-6">
|
|
141
|
+
<div className="flex items-center justify-between text-sm">
|
|
142
|
+
<span className="text-gray-600">Subtotal ({itemCount} {itemCount === 1 ? 'item' : 'items'})</span>
|
|
143
|
+
<span className="font-semibold text-slate-900">{formatPrice(subtotal)}</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="flex items-center justify-between text-sm">
|
|
146
|
+
<span className="text-gray-600">Shipping</span>
|
|
147
|
+
<span className="font-semibold text-green-600">
|
|
148
|
+
Will be calculated at checkout
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
<div className="border-t border-gray-200 pt-4 mt-4">
|
|
152
|
+
<div className="flex items-center justify-between">
|
|
153
|
+
<span className="text-lg font-bold text-slate-900">Total</span>
|
|
154
|
+
<span className="text-2xl font-bold text-orange-600">{formatPrice(total)}</span>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
240
157
|
</div>
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
<
|
|
244
|
-
|
|
245
|
-
|
|
158
|
+
|
|
159
|
+
<div className="space-y-3">
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
onClick={() => router.push(buildPath('/checkout'))}
|
|
163
|
+
className="w-full rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-4 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2"
|
|
164
|
+
>
|
|
165
|
+
Proceed to Checkout
|
|
166
|
+
<ArrowRight className="h-5 w-5" />
|
|
167
|
+
</button>
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onClick={() => router.push(buildPath('/shop'))}
|
|
171
|
+
className="w-full rounded-full border-2 border-primary-300 bg-white px-4 py-3 text-sm font-medium text-slate-700 hover:bg-gray-50 transition-colors"
|
|
172
|
+
>
|
|
173
|
+
Continue Shopping
|
|
174
|
+
</button>
|
|
246
175
|
</div>
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
<
|
|
176
|
+
|
|
177
|
+
<div className="mt-6 space-y-3 pt-6 border-t border-gray-200">
|
|
178
|
+
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
179
|
+
<CheckCircle2 className="h-5 w-5 text-primary-600 flex-shrink-0 mt-0.5" />
|
|
180
|
+
<span>Easy returns within 30 days</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
183
|
+
<CheckCircle2 className="h-5 w-5 text-primary-600 flex-shrink-0 mt-0.5" />
|
|
184
|
+
<span>Secure checkout process</span>
|
|
251
185
|
</div>
|
|
252
|
-
<p className="mt-2 text-xs text-slate-500">
|
|
253
|
-
Prices include pharmacy-grade quality control and packaging.
|
|
254
|
-
</p>
|
|
255
186
|
</div>
|
|
256
187
|
</div>
|
|
257
|
-
<Button
|
|
258
|
-
size="lg"
|
|
259
|
-
className="mt-6 w-full"
|
|
260
|
-
onClick={() => router.push(buildPath('/checkout'))}
|
|
261
|
-
>
|
|
262
|
-
Secure checkout
|
|
263
|
-
<ArrowRight className="h-5 w-5" />
|
|
264
|
-
</Button>
|
|
265
|
-
<button
|
|
266
|
-
type="button"
|
|
267
|
-
onClick={() => router.push(buildPath('/shop'))}
|
|
268
|
-
className="mt-4 w-full text-sm font-semibold text-primary-600 transition hover:text-primary-700"
|
|
269
|
-
>
|
|
270
|
-
Continue shopping
|
|
271
|
-
</button>
|
|
272
|
-
</div>
|
|
273
188
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
189
|
+
<div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
|
|
190
|
+
<p className="font-semibold uppercase tracking-[0.3em]">Need help?</p>
|
|
191
|
+
<p className="mt-2 leading-relaxed">
|
|
192
|
+
Chat with a pharmacist to optimize your regimen or discuss substitutions before you
|
|
193
|
+
check out.
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</motion.aside>
|
|
197
|
+
</div>
|
|
282
198
|
</div>
|
|
283
199
|
</div>
|
|
284
200
|
</div>
|
|
285
|
-
</div>
|
|
286
201
|
);
|
|
287
202
|
}
|
|
@@ -172,9 +172,14 @@ export function CheckoutScreen() {
|
|
|
172
172
|
|
|
173
173
|
const rates = response.data.rates || [];
|
|
174
174
|
if (rates.length > 0) {
|
|
175
|
+
// Sort rates by price (cheapest first) and select the first one
|
|
176
|
+
const sortedRates = [...rates].sort((a, b) => parseFloat(a.amount) - parseFloat(b.amount));
|
|
177
|
+
const cheapestRate = sortedRates[0];
|
|
178
|
+
|
|
175
179
|
setShippingRates(rates);
|
|
176
180
|
setShippingRatesError(null);
|
|
177
|
-
setSelectedShippingRateId(
|
|
181
|
+
setSelectedShippingRateId(cheapestRate.objectId);
|
|
182
|
+
setShippingPrice(parseFloat(cheapestRate.amount));
|
|
178
183
|
return;
|
|
179
184
|
}
|
|
180
185
|
|
|
@@ -255,17 +260,14 @@ export function CheckoutScreen() {
|
|
|
255
260
|
})();
|
|
256
261
|
}, [isDelivery, selectedAddressId, cart]);
|
|
257
262
|
|
|
258
|
-
// Simulate fetching store addresses for pickup (replace with real API if available)
|
|
259
263
|
useEffect(() => {
|
|
260
264
|
if (!isDelivery) {
|
|
261
|
-
// Simulate store addresses
|
|
262
265
|
const stores = [
|
|
263
266
|
{ id: 'store1', name: 'Main Pharmacy', street1: '123 Main St', city: 'Seattle' },
|
|
264
267
|
{ id: 'store2', name: 'Eastside Pickup', street1: '456 East Ave', city: 'Bellevue' },
|
|
265
268
|
];
|
|
266
269
|
setStoreAddresses(stores);
|
|
267
270
|
setSelectedStoreAddressId(stores[0].id);
|
|
268
|
-
// Pickup mode: no shipping cost
|
|
269
271
|
setShippingPrice(0);
|
|
270
272
|
} else {
|
|
271
273
|
setStoreAddresses([]);
|
|
@@ -279,7 +281,6 @@ export function CheckoutScreen() {
|
|
|
279
281
|
{ id: 3, label: 'Payment', status: 'upcoming' as const },
|
|
280
282
|
];
|
|
281
283
|
|
|
282
|
-
// Unified checkout handler (admin-style)
|
|
283
284
|
const onSubmit = async (data: CheckoutFormData) => {
|
|
284
285
|
setError(null);
|
|
285
286
|
if (!isAuthenticated) {
|
|
@@ -304,7 +305,6 @@ export function CheckoutScreen() {
|
|
|
304
305
|
setError('Please select a shipping method.');
|
|
305
306
|
return;
|
|
306
307
|
}
|
|
307
|
-
// Validate shipping address fields
|
|
308
308
|
const requiredFields = ['name', 'street1', 'city', 'zip', 'country'];
|
|
309
309
|
for (const field of requiredFields) {
|
|
310
310
|
if (!data.shipping[field as keyof typeof data.shipping]) {
|
|
@@ -339,21 +339,17 @@ export function CheckoutScreen() {
|
|
|
339
339
|
setIsSubmitting(true);
|
|
340
340
|
toast('Submitting order...', { icon: <CreditCard className="h-4 w-4" /> });
|
|
341
341
|
try {
|
|
342
|
-
// Prepare items array for backend
|
|
343
342
|
const items = (cart?.cartBody?.items || []).map((item: any) => ({
|
|
344
343
|
productVariantId: String(item.productVariantId),
|
|
345
344
|
quantity: typeof item.quantity === 'number' ? item.quantity : 1,
|
|
346
345
|
})).filter((item: any) => item.productVariantId);
|
|
347
346
|
|
|
348
|
-
// Determine billing address ID for card payments (admin-style)
|
|
349
347
|
let billingAddressId: string | undefined = undefined;
|
|
350
348
|
if (isDelivery) {
|
|
351
349
|
billingAddressId = selectedAddressId || undefined;
|
|
352
350
|
} else if (paymentMethod === 'Card') {
|
|
353
351
|
billingAddressId = userAddresses.length > 0 ? userAddresses[0].id : undefined;
|
|
354
352
|
}
|
|
355
|
-
|
|
356
|
-
// Prepare order DTO (admin-style)
|
|
357
353
|
const orderDTO: any = {
|
|
358
354
|
items,
|
|
359
355
|
paymentMethod,
|
|
@@ -373,7 +369,6 @@ export function CheckoutScreen() {
|
|
|
373
369
|
isDelivery ? (selectedShippingRateId || undefined) : undefined,
|
|
374
370
|
billingAddressId
|
|
375
371
|
);
|
|
376
|
-
// Handle insufficient credit error (fallback: check for error message)
|
|
377
372
|
if (response?.data?.payment && response.data.payment.hostedInvoiceUrl === 'INSUFFICIENT_CREDIT') {
|
|
378
373
|
setError('You have insufficient credit to complete this order.');
|
|
379
374
|
toast.error('You have insufficient credit to complete this order.');
|
|
@@ -382,11 +377,23 @@ export function CheckoutScreen() {
|
|
|
382
377
|
if (paymentMethod === 'Card') {
|
|
383
378
|
const paymentUrl = response?.data?.payment?.hostedInvoiceUrl;
|
|
384
379
|
if (paymentUrl && paymentUrl !== 'INSUFFICIENT_CREDIT') {
|
|
385
|
-
window.open(
|
|
386
|
-
|
|
387
|
-
|
|
380
|
+
const newWindow = window.open('', '_blank');
|
|
381
|
+
if (newWindow) {
|
|
382
|
+
newWindow.location.href = paymentUrl;
|
|
383
|
+
await clearCart();
|
|
384
|
+
return;
|
|
385
|
+
} else {
|
|
386
|
+
window.location.href = paymentUrl;
|
|
387
|
+
await clearCart();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
} else if (!paymentUrl) {
|
|
391
|
+
console.error('No payment URL received in response:', response.data);
|
|
392
|
+
setError('Failed to initiate payment. Please try again or contact support.');
|
|
393
|
+
toast.error('Failed to initiate payment');
|
|
388
394
|
return;
|
|
389
395
|
}
|
|
396
|
+
|
|
390
397
|
toast.success('Order placed successfully!');
|
|
391
398
|
await clearCart();
|
|
392
399
|
router.push(buildPath(`/orders/${response.data?.id}`));
|
|
@@ -413,49 +420,8 @@ export function CheckoutScreen() {
|
|
|
413
420
|
const total = subtotal + shippingPrice + tax;
|
|
414
421
|
|
|
415
422
|
return (
|
|
416
|
-
<div className="min-h-screen bg-slate-50">
|
|
417
|
-
|
|
418
|
-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]" />
|
|
419
|
-
<div className="relative container mx-auto px-4 py-16">
|
|
420
|
-
<div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
|
|
421
|
-
<motion.div
|
|
422
|
-
initial={{ opacity: 0, y: 24 }}
|
|
423
|
-
animate={{ opacity: 1, y: 0 }}
|
|
424
|
-
className="space-y-6"
|
|
425
|
-
>
|
|
426
|
-
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|
|
427
|
-
<ShieldCheck className="h-4 w-4" />
|
|
428
|
-
Secure checkout
|
|
429
|
-
</span>
|
|
430
|
-
<h1 className="text-4xl font-bold md:text-5xl">
|
|
431
|
-
Provide delivery details
|
|
432
|
-
</h1>
|
|
433
|
-
<p className="text-white/75 md:text-lg">
|
|
434
|
-
Our pharmacists handle every package with care. Share your shipping and billing
|
|
435
|
-
information so we can dispatch your order within the next 12 hours.
|
|
436
|
-
</p>
|
|
437
|
-
<div className="flex flex-wrap gap-3">
|
|
438
|
-
{checkoutSteps.map((step) => (
|
|
439
|
-
<div
|
|
440
|
-
key={step.id}
|
|
441
|
-
className={`flex items-center gap-3 rounded-2xl px-4 py-2 text-sm font-semibold ${step.status === 'complete'
|
|
442
|
-
? 'bg-white/20 text-white'
|
|
443
|
-
: step.status === 'current'
|
|
444
|
-
? 'bg-white text-primary-700'
|
|
445
|
-
: 'bg-white/10 text-white/60'
|
|
446
|
-
}`}
|
|
447
|
-
>
|
|
448
|
-
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/20 text-xs font-bold">
|
|
449
|
-
{step.id}
|
|
450
|
-
</span>
|
|
451
|
-
{step.label}
|
|
452
|
-
</div>
|
|
453
|
-
))}
|
|
454
|
-
</div>
|
|
455
|
-
</motion.div>
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
</section>
|
|
423
|
+
<div className="min-h-screen bg-slate-50 mb-16">
|
|
424
|
+
|
|
459
425
|
|
|
460
426
|
<form onSubmit={handleSubmit(onSubmit)}>
|
|
461
427
|
{error && <div className="mb-4 text-red-600 font-semibold">{error}</div>}
|
|
@@ -515,7 +481,6 @@ export function CheckoutScreen() {
|
|
|
515
481
|
checked={selectedAddressId === addr.id}
|
|
516
482
|
onChange={() => {
|
|
517
483
|
setSelectedAddressId(addr.id);
|
|
518
|
-
// Update form with selected address
|
|
519
484
|
setValue('shipping.name', addr.name);
|
|
520
485
|
setValue('shipping.phone', addr.phone || (undefined as unknown as string));
|
|
521
486
|
setValue('shipping.street1', addr.street1);
|
|
@@ -638,9 +603,10 @@ export function CheckoutScreen() {
|
|
|
638
603
|
return (
|
|
639
604
|
<div
|
|
640
605
|
key={rate.objectId}
|
|
641
|
-
onClick={() =>
|
|
642
|
-
|
|
643
|
-
|
|
606
|
+
onClick={() => {
|
|
607
|
+
setSelectedShippingRateId(rate.objectId);
|
|
608
|
+
setShippingPrice(parseFloat(rate.amount));
|
|
609
|
+
}}
|
|
644
610
|
className={`relative p-5 border-2 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-md ${isSelected
|
|
645
611
|
? 'border-primary-500 bg-primary-50 ring-2 ring-primary-200'
|
|
646
612
|
: 'border-gray-200 hover:border-gray-300'
|
|
@@ -896,6 +862,9 @@ export function CheckoutScreen() {
|
|
|
896
862
|
|
|
897
863
|
{/* Cart Summary */}
|
|
898
864
|
<section className="mt-8 pt-6 border-t border-slate-100">
|
|
865
|
+
<h3 className="text-xs font-semibold text-slate-700 uppercase tracking-wider">
|
|
866
|
+
Cart Summary
|
|
867
|
+
</h3>
|
|
899
868
|
<div className="max-h-60 space-y-4 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-200 hover:scrollbar-thumb-slate-300">
|
|
900
869
|
{cart?.cartBody?.items?.map((item: any) => (
|
|
901
870
|
<div
|
|
@@ -66,7 +66,7 @@ export function LoginScreen() {
|
|
|
66
66
|
initial={{ opacity: 0, x: -24 }}
|
|
67
67
|
animate={{ opacity: 1, x: 0 }}
|
|
68
68
|
transition={{ duration: 0.4 }}
|
|
69
|
-
className="relative flex flex-col justify-between bg-gradient-to-br from-
|
|
69
|
+
className="relative flex flex-col justify-between bg-gradient-to-br from-slate-700 via-slate-600 to-slate-700 px-10 py-14 text-white"
|
|
70
70
|
>
|
|
71
71
|
<div className="space-y-6">
|
|
72
72
|
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|