hey-pharmacist-ecommerce 1.0.4 → 1.0.6

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 (74) hide show
  1. package/README.md +107 -1
  2. package/dist/index.d.mts +3636 -316
  3. package/dist/index.d.ts +3636 -316
  4. package/dist/index.js +6802 -3865
  5. package/dist/index.js.map +1 -1
  6. package/dist/index.mjs +6756 -3817
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +17 -14
  9. package/src/components/AddressFormModal.tsx +171 -0
  10. package/src/components/CartItem.tsx +17 -12
  11. package/src/components/FilterChips.tsx +195 -0
  12. package/src/components/Header.tsx +121 -71
  13. package/src/components/OrderCard.tsx +18 -25
  14. package/src/components/ProductCard.tsx +209 -72
  15. package/src/components/ui/Button.tsx +13 -5
  16. package/src/components/ui/Card.tsx +46 -0
  17. package/src/hooks/useAddresses.ts +83 -0
  18. package/src/hooks/useOrders.ts +37 -19
  19. package/src/hooks/useProducts.ts +55 -63
  20. package/src/hooks/useWishlistProducts.ts +75 -0
  21. package/src/index.ts +3 -19
  22. package/src/lib/Apis/api.ts +1 -0
  23. package/src/lib/Apis/apis/cart-api.ts +3 -3
  24. package/src/lib/Apis/apis/inventory-api.ts +0 -108
  25. package/src/lib/Apis/apis/stores-api.ts +70 -0
  26. package/src/lib/Apis/apis/wishlist-api.ts +447 -0
  27. package/src/lib/Apis/models/cart-item-populated.ts +0 -1
  28. package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
  29. package/src/lib/Apis/models/create-variant-dto.ts +26 -33
  30. package/src/lib/Apis/models/extended-product-dto.ts +20 -24
  31. package/src/lib/Apis/models/index.ts +2 -1
  32. package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
  33. package/src/lib/Apis/models/order.ts +3 -8
  34. package/src/lib/Apis/models/populated-order.ts +3 -8
  35. package/src/lib/Apis/models/product-variant.ts +29 -0
  36. package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
  37. package/src/lib/Apis/models/wishlist.ts +51 -0
  38. package/src/lib/Apis/wrapper.ts +18 -7
  39. package/src/lib/api-adapter/index.ts +0 -12
  40. package/src/lib/types/index.ts +16 -61
  41. package/src/lib/utils/colors.ts +7 -4
  42. package/src/lib/utils/format.ts +1 -1
  43. package/src/lib/validations/address.ts +14 -0
  44. package/src/providers/AuthProvider.tsx +61 -31
  45. package/src/providers/CartProvider.tsx +18 -28
  46. package/src/providers/EcommerceProvider.tsx +7 -0
  47. package/src/providers/FavoritesProvider.tsx +86 -0
  48. package/src/providers/ThemeProvider.tsx +16 -1
  49. package/src/providers/WishlistProvider.tsx +174 -0
  50. package/src/screens/AddressesScreen.tsx +484 -0
  51. package/src/screens/CartScreen.tsx +120 -84
  52. package/src/screens/CategoriesScreen.tsx +120 -0
  53. package/src/screens/CheckoutScreen.tsx +919 -241
  54. package/src/screens/CurrentOrdersScreen.tsx +125 -61
  55. package/src/screens/HomeScreen.tsx +209 -0
  56. package/src/screens/LoginScreen.tsx +133 -88
  57. package/src/screens/NewAddressScreen.tsx +187 -0
  58. package/src/screens/OrdersScreen.tsx +162 -50
  59. package/src/screens/ProductDetailScreen.tsx +641 -190
  60. package/src/screens/ProfileScreen.tsx +192 -116
  61. package/src/screens/RegisterScreen.tsx +193 -144
  62. package/src/screens/SearchResultsScreen.tsx +165 -0
  63. package/src/screens/ShopScreen.tsx +1110 -146
  64. package/src/screens/WishlistScreen.tsx +428 -0
  65. package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
  66. package/src/lib/api/auth.ts +0 -81
  67. package/src/lib/api/cart.ts +0 -42
  68. package/src/lib/api/orders.ts +0 -53
  69. package/src/lib/api/products.ts +0 -51
  70. package/src/lib/api-adapter/auth-adapter.ts +0 -196
  71. package/src/lib/api-adapter/cart-adapter.ts +0 -193
  72. package/src/lib/api-adapter/mappers.ts +0 -147
  73. package/src/lib/api-adapter/orders-adapter.ts +0 -195
  74. package/src/lib/api-adapter/products-adapter.ts +0 -194
@@ -1,340 +1,1018 @@
1
1
  'use client';
2
2
 
3
- import React, { useState } from 'react';
3
+ import React, { useState, useEffect } from 'react';
4
4
  import { motion } from 'framer-motion';
5
5
  import { useForm } from 'react-hook-form';
6
6
  import { zodResolver } from '@hookform/resolvers/zod';
7
7
  import { z } from 'zod';
8
- import { Lock, CreditCard } from 'lucide-react';
8
+ import {
9
+ Check as CheckIcon,
10
+ CreditCard,
11
+ Lock,
12
+ MapPin,
13
+ PackageCheck,
14
+ Plus,
15
+ ShieldCheck,
16
+ Truck,
17
+ } from 'lucide-react';
9
18
  import { Button } from '@/components/ui/Button';
10
19
  import { Input } from '@/components/ui/Input';
11
20
  import { useCart } from '@/providers/CartProvider';
12
21
  import { useAuth } from '@/providers/AuthProvider';
13
- import { ordersApi, CreateOrderData } from '@/lib/api/orders';
22
+ import { OrdersApi } from '@/lib/Apis/apis/orders-api';
23
+ import { ShippingApi } from '@/lib/Apis/apis/shipping-api';
24
+ import { useAddresses } from '@/hooks/useAddresses';
14
25
  import { formatPrice } from '@/lib/utils/format';
15
26
  import { useRouter } from 'next/navigation';
16
27
  import { toast } from 'sonner';
28
+ import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
29
+ import { Card } from '@/components/ui/Card';
30
+ import { AddressFormModal } from '@/components/AddressFormModal';
31
+ import { Edit3, Trash2 } from 'lucide-react';
17
32
 
18
- const addressSchema = z.object({
19
- fullName: z.string().min(2, 'Full name is required'),
20
- addressLine1: z.string().min(5, 'Address is required'),
21
- addressLine2: z.string().optional(),
22
- city: z.string().min(2, 'City is required'),
23
- state: z.string().min(2, 'State is required'),
24
- zipCode: z.string().min(5, 'ZIP code is required'),
25
- country: z.string().min(2, 'Country is required'),
26
- phone: z.string().min(10, 'Phone number is required'),
27
- });
33
+ import { addressSchema } from '@/lib/validations/address';
34
+ import Image from 'next/image';
28
35
 
29
36
  const checkoutSchema = z.object({
30
37
  shipping: addressSchema,
31
38
  billing: addressSchema,
32
- sameAsShipping: z.boolean(),
39
+ sameAsShipping: z.boolean().default(true),
33
40
  });
34
41
 
35
42
  type CheckoutFormData = z.infer<typeof checkoutSchema>;
36
43
 
44
+
45
+ const SHIPPING_THRESHOLD = 100;
46
+ const TAX_RATE = 0.08;
47
+
48
+ // Add enums for payment methods
49
+ const PAYMENT_METHODS = [
50
+ {
51
+ label: 'Card',
52
+ value: 'Card',
53
+ icon: <CreditCard className="w-5 h-5" />,
54
+ description: 'Pay securely with your credit or debit card',
55
+ className: 'border-blue-500 hover:bg-blue-50',
56
+ activeClass: 'bg-blue-50 border-blue-500 text-blue-700',
57
+ },
58
+ {
59
+ label: 'Cash',
60
+ value: 'Cash',
61
+ icon: <PackageCheck className="w-5 h-5" />,
62
+ description: 'Pay with cash on delivery or at pickup',
63
+ className: 'border-amber-500 hover:bg-amber-50',
64
+ activeClass: 'bg-amber-50 border-amber-500 text-amber-700',
65
+ },
66
+ {
67
+ label: 'Credit',
68
+ value: 'Credit',
69
+ icon: <ShieldCheck className="w-5 h-5" />,
70
+ description: 'Use your account credit',
71
+ className: 'border-emerald-500 hover:bg-emerald-50',
72
+ activeClass: 'bg-emerald-50 border-emerald-500 text-emerald-700',
73
+ },
74
+ ];
75
+
37
76
  export function CheckoutScreen() {
38
77
  const router = useRouter();
39
- const { cart } = useCart();
78
+ const { cart, clearCart } = useCart();
40
79
  const { isAuthenticated, user } = useAuth();
41
80
  const [isSubmitting, setIsSubmitting] = useState(false);
42
- const [sameAsShipping, setSameAsShipping] = useState(true);
81
+ const [isDelivery, setIsDelivery] = useState(true); // delivery or pickup
82
+ const [paymentMethod, setPaymentMethod] = useState('Card');
83
+ const [error, setError] = useState<string | null>(null);
84
+ const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
85
+ const [selectedStoreAddressId, setSelectedStoreAddressId] = useState<string | null>(null);
86
+ const [storeAddresses, setStoreAddresses] = useState<any[]>([]); // For pickup selection
87
+ const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
88
+ const [editingAddress, setEditingAddress] = useState<any | null>(null);
89
+ const [shippingPrice, setShippingPrice] = useState(0);
43
90
 
91
+
92
+ // Use the addresses hook
93
+ const {
94
+ addresses: userAddresses,
95
+ defaultAddress
96
+ } = useAddresses();
97
+ const [selectedShippingRateId, setSelectedShippingRateId] = useState<string | null>(null);
98
+ const [shippingRates, setShippingRates] = useState<any[]>([]);
99
+ const [shippingRatesLoading, setShippingRatesLoading] = useState(false);
100
+ const [shippingRatesError, setShippingRatesError] = useState<string | null>(null);
101
+ const [storeData, setStoreData] = useState<any>(null); // Simulate store data if needed
102
+ const { addresses, isLoading, refresh, removeAddress } = useAddresses();
44
103
  const {
45
104
  register,
46
105
  handleSubmit,
47
106
  formState: { errors },
107
+ watch,
108
+ setValue,
48
109
  } = useForm<CheckoutFormData>({
49
110
  resolver: zodResolver(checkoutSchema),
50
111
  defaultValues: {
51
112
  sameAsShipping: true,
52
113
  shipping: {
53
- fullName: user ? `${user.firstName} ${user.lastName}` : '',
54
- phone: user?.phone || '',
114
+ name: user ? `${user.firstname} ${user.lastname}` : '',
115
+ phone: user?.phoneNumber || undefined,
55
116
  country: 'United States',
56
117
  },
57
118
  billing: {
58
- fullName: user ? `${user.firstName} ${user.lastName}` : '',
59
- phone: user?.phone || '',
119
+ name: user ? `${user.firstname} ${user.lastname}` : '',
120
+ phone: user?.phoneNumber || undefined,
60
121
  country: 'United States',
61
122
  },
62
123
  },
63
124
  });
64
125
 
126
+ const sameAsShipping = watch('sameAsShipping', true);
127
+
128
+ // Sync billing address with shipping address when 'Billing same as shipping' is checked
129
+ useEffect(() => {
130
+ if (sameAsShipping) {
131
+ setValue('billing.name', watch('shipping.name'));
132
+ setValue('billing.phone', watch('shipping.phone'));
133
+ setValue('billing.street1', watch('shipping.street1'));
134
+ setValue('billing.street2', watch('shipping.street2'));
135
+ setValue('billing.city', watch('shipping.city'));
136
+ setValue('billing.state', watch('shipping.state'));
137
+ setValue('billing.zip', watch('shipping.zip'));
138
+ setValue('billing.country', watch('shipping.country'));
139
+ }
140
+ // eslint-disable-next-line react-hooks/exhaustive-deps
141
+ }, [sameAsShipping, watch('shipping.name'), watch('shipping.phone'), watch('shipping.street1'), watch('shipping.street2'), watch('shipping.city'), watch('shipping.state'), watch('shipping.zip'), watch('shipping.country')]);
142
+
143
+ // const handleAddressAdded = async (address: any) => {
144
+ // await refresh();
145
+ // };
146
+
147
+ useEffect(() => {
148
+ if (defaultAddress && !selectedAddressId) {
149
+ setSelectedAddressId(defaultAddress.id);
150
+ // Update form with default address
151
+ setValue('shipping.name', defaultAddress.name);
152
+ setValue('shipping.phone', defaultAddress.phone || (undefined as unknown as string));
153
+ setValue('shipping.street1', defaultAddress.street1);
154
+ setValue('shipping.street2', defaultAddress.street2 || '');
155
+ setValue('shipping.city', defaultAddress.city);
156
+ setValue('shipping.state', defaultAddress.state);
157
+ setValue('shipping.zip', defaultAddress.zip);
158
+ setValue('shipping.country', defaultAddress.country);
159
+ }
160
+ }, [defaultAddress, selectedAddressId, setValue]);
161
+
162
+ // Handle shipping rates response and errors
163
+ const handleShippingResponse = (response: any) => {
164
+ if (!response?.data) {
165
+ setShippingRatesError('Unable to calculate shipping rates');
166
+ setShippingRates([]);
167
+ setSelectedShippingRateId(null);
168
+ return;
169
+ }
170
+
171
+ const rates = response.data.rates || [];
172
+ if (rates.length > 0) {
173
+ setShippingRates(rates);
174
+ setShippingRatesError(null);
175
+ setSelectedShippingRateId(rates[0].objectId); // Select first rate by default
176
+ return;
177
+ }
178
+
179
+ // If no rates, show the most relevant error message from messages
180
+ const messages = response.data.messages || [];
181
+ // Prefer messages that mention 'shipping', 'service area', or are from USPS
182
+ const relevantMessage = messages.find((msg: any) =>
183
+ msg.text.toLowerCase().includes('shipping') ||
184
+ msg.text.toLowerCase().includes('service area') ||
185
+ msg.source === 'USPS'
186
+ ) || messages[0];
187
+ setShippingRatesError(relevantMessage?.text || 'Shipping is not available to this address');
188
+ setShippingRates([]);
189
+ setSelectedShippingRateId(null);
190
+ };
191
+
192
+ // Define shipping message types
193
+ interface ShippingMessage {
194
+ source: string;
195
+ code: string;
196
+ text: string;
197
+ }
198
+
199
+ interface ShippingRate {
200
+ objectId: string;
201
+ provider: string;
202
+ servicelevel: {
203
+ name: string;
204
+ token: string;
205
+ };
206
+ amount: string;
207
+ amountLocal: string;
208
+ currency: string;
209
+ currencyLocal: string;
210
+ durationTerms?: string;
211
+ estimatedDays?: number;
212
+ providerImage75?: string;
213
+ attributes?: string[];
214
+ test?: boolean;
215
+ }
216
+
217
+ interface ShippingResponse {
218
+ data?: {
219
+ messages: ShippingMessage[];
220
+ rates: ShippingRate[];
221
+ status: string;
222
+ };
223
+ }
224
+
225
+
226
+ useEffect(() => {
227
+ if (!isDelivery || !selectedAddressId || !cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
228
+ setShippingRates([]);
229
+ setSelectedShippingRateId(null);
230
+ return;
231
+ }
232
+ setShippingRatesLoading(true);
233
+ setShippingRatesError(null);
234
+ (async () => {
235
+ try {
236
+ const api = new ShippingApi(AXIOS_CONFIG);
237
+ const cartBody = {
238
+ items: cart?.cartBody?.items?.map((item: any) => ({
239
+ productVariantId: String(item.productVariantId),
240
+ quantity: typeof item.quantity === 'number' ? item.quantity : 1,
241
+ })),
242
+ discount: undefined,
243
+ };
244
+ const res = await api.getShippingRates(cartBody, selectedAddressId);
245
+ handleShippingResponse(res);
246
+ } catch (e: any) {
247
+ setShippingRates([]);
248
+ setSelectedShippingRateId(null);
249
+ setShippingRatesError(e?.message || 'Failed to fetch shipping rates');
250
+ } finally {
251
+ setShippingRatesLoading(false);
252
+ }
253
+ })();
254
+ }, [isDelivery, selectedAddressId, cart]);
255
+
256
+ // Simulate fetching store addresses for pickup (replace with real API if available)
257
+ useEffect(() => {
258
+ if (!isDelivery) {
259
+ // Simulate store addresses
260
+ const stores = [
261
+ { id: 'store1', name: 'Main Pharmacy', street1: '123 Main St', city: 'Seattle' },
262
+ { id: 'store2', name: 'Eastside Pickup', street1: '456 East Ave', city: 'Bellevue' },
263
+ ];
264
+ setStoreAddresses(stores);
265
+ setSelectedStoreAddressId(stores[0].id);
266
+ // Pickup mode: no shipping cost
267
+ setShippingPrice(0);
268
+ } else {
269
+ setStoreAddresses([]);
270
+ setSelectedStoreAddressId(null);
271
+ }
272
+ }, [isDelivery]);
273
+
274
+ const checkoutSteps = [
275
+ { id: 1, label: 'Cart', status: 'complete' as const },
276
+ { id: 2, label: 'Details', status: 'current' as const },
277
+ { id: 3, label: 'Payment', status: 'upcoming' as const },
278
+ ];
279
+
280
+ // Unified checkout handler (admin-style)
65
281
  const onSubmit = async (data: CheckoutFormData) => {
282
+ setError(null);
66
283
  if (!isAuthenticated) {
67
284
  toast.error('Please login to continue');
68
- router.push('/login?redirect=/checkout');
285
+ setTimeout(() => router.push('/login?redirect=/checkout'), 50);
69
286
  return;
70
287
  }
288
+ if (!cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
289
+ setError('Your cart is empty. Please add items to your cart.');
290
+ return;
291
+ }
292
+ if (!paymentMethod) {
293
+ setError('Please select a payment method.');
294
+ return;
295
+ }
296
+ if (isDelivery) {
297
+ if (!selectedAddressId) {
298
+ setError('Please select a shipping address.');
299
+ return;
300
+ }
301
+ if (!selectedShippingRateId) {
302
+ setError('Please select a shipping method.');
303
+ return;
304
+ }
305
+ // Validate shipping address fields
306
+ const requiredFields = ['name', 'street1', 'city', 'zip', 'country'];
307
+ for (const field of requiredFields) {
308
+ if (!data.shipping[field as keyof typeof data.shipping]) {
309
+ setError(`Please fill in the shipping address: ${field}.`);
310
+ return;
311
+ }
312
+ }
313
+ } else {
314
+ if (!selectedStoreAddressId) {
315
+ setError('Please select a pickup location.');
316
+ return;
317
+ }
318
+ if (storeAddresses.length === 0) {
319
+ setError('Store pickup location is not available.');
320
+ return;
321
+ }
322
+ if (paymentMethod === 'Card' && userAddresses.length === 0) {
323
+ setError('Card payments require a billing address. Please add an address to your account or choose a different payment method.');
324
+ return;
325
+ }
326
+ }
327
+ if (!sameAsShipping) {
328
+ const requiredBillingFields = ['fullName', 'addressLine1', 'city', 'zipCode', 'country'];
329
+ for (const field of requiredBillingFields) {
330
+ if (!data.billing[field as keyof typeof data.billing]) {
331
+ setError(`Please fill in the billing address: ${field}.`);
332
+ return;
333
+ }
334
+ }
335
+ }
71
336
 
72
337
  setIsSubmitting(true);
338
+ toast('Submitting order...', { icon: <CreditCard className="h-4 w-4" /> });
73
339
  try {
74
- const orderData: CreateOrderData = {
340
+ // Prepare items array for backend
341
+ const items = (cart?.cartBody?.items || []).map((item: any) => ({
342
+ productVariantId: String(item.productVariantId),
343
+ quantity: typeof item.quantity === 'number' ? item.quantity : 1,
344
+ })).filter((item: any) => item.productVariantId);
345
+
346
+ // Determine billing address ID for card payments (admin-style)
347
+ let billingAddressId: string | undefined = undefined;
348
+ if (isDelivery) {
349
+ billingAddressId = selectedAddressId || undefined;
350
+ } else if (paymentMethod === 'Card') {
351
+ billingAddressId = userAddresses.length > 0 ? userAddresses[0].id : undefined;
352
+ }
353
+
354
+ // Prepare order DTO (admin-style)
355
+ const orderDTO: any = {
356
+ items,
357
+ paymentMethod,
358
+ orderStatus: 'Pending',
359
+ chargeTax: true,
360
+ orderRemindingDates: [],
75
361
  shippingAddress: data.shipping,
76
362
  billingAddress: sameAsShipping ? data.shipping : data.billing,
77
- sameAsShipping,
363
+ pickupStoreId: !isDelivery ? selectedStoreAddressId : undefined,
78
364
  };
79
365
 
80
- const response = await ordersApi.createOrder(orderData);
81
-
82
- if (response.success && response.data.stripeCheckoutUrl) {
83
- // Redirect to Stripe checkout
84
- window.location.href = response.data.stripeCheckoutUrl;
366
+ const api = new OrdersApi(AXIOS_CONFIG);
367
+ const response = await api.createCheckout(
368
+ orderDTO,
369
+ isDelivery,
370
+ user?.id,
371
+ isDelivery ? (selectedShippingRateId || undefined) : undefined,
372
+ billingAddressId
373
+ );
374
+ // Handle insufficient credit error (fallback: check for error message)
375
+ if (response?.data?.payment && response.data.payment.hostedInvoiceUrl === 'INSUFFICIENT_CREDIT') {
376
+ setError('You have insufficient credit to complete this order.');
377
+ toast.error('You have insufficient credit to complete this order.');
378
+ return;
379
+ }
380
+ if (paymentMethod === 'Card') {
381
+ const paymentUrl = response?.data?.payment?.hostedInvoiceUrl;
382
+ if (paymentUrl && paymentUrl !== 'INSUFFICIENT_CREDIT') {
383
+ window.open(paymentUrl, '_blank');
384
+ await clearCart();
385
+
386
+ return;
387
+ }
388
+ toast.success('Order placed successfully!');
389
+ await clearCart();
390
+ router.push(`/orders/${response.data?.id}`);
85
391
  } else {
86
392
  toast.success('Order placed successfully!');
87
- router.push(`/orders/${response.data.id}`);
393
+ await clearCart();
394
+ router.push(`/orders/${response.data?.id}`);
88
395
  }
89
- } catch (error: any) {
90
- toast.error(error.response?.data?.message || 'Failed to place order');
396
+ } catch (err: any) {
397
+ const msg = err?.message || (err?.response?.data?.message) || 'Failed to place order';
398
+ setError(msg);
399
+ toast.error(msg);
91
400
  } finally {
92
401
  setIsSubmitting(false);
93
402
  }
94
403
  };
95
404
 
96
- if (!cart || cart.items.length === 0) {
405
+ if (!cart || cart?.cartBody?.items?.length === 0 || !cart?.cartBody?.items) {
97
406
  router.push('/cart');
98
407
  return null;
99
408
  }
100
-
101
409
  const subtotal = cart.total;
102
- const shipping = subtotal > 50 ? 0 : 10;
103
- const tax = subtotal * 0.1;
104
- const total = subtotal + shipping + tax;
410
+ const tax = 0;
411
+ const total = subtotal + shippingPrice + tax;
105
412
 
106
413
  return (
107
- <div className="min-h-screen bg-gray-50 py-12">
108
- <div className="container mx-auto px-4 max-w-6xl">
109
- {/* Header */}
110
- <motion.div
111
- initial={{ opacity: 0, y: 20 }}
112
- animate={{ opacity: 1, y: 0 }}
113
- className="mb-8"
114
- >
115
- <h1 className="text-4xl font-bold text-gray-900 mb-2">Checkout</h1>
116
- <p className="text-gray-600">Complete your order</p>
117
- </motion.div>
118
-
119
- <form onSubmit={handleSubmit(onSubmit)}>
120
- <div className="grid lg:grid-cols-3 gap-8">
121
- {/* Forms */}
122
- <div className="lg:col-span-2 space-y-6">
123
- {/* Shipping Address */}
124
- <motion.div
125
- initial={{ opacity: 0, y: 20 }}
126
- animate={{ opacity: 1, y: 0 }}
127
- transition={{ delay: 0.1 }}
128
- className="bg-white rounded-2xl p-6 shadow-sm"
129
- >
130
- <h2 className="text-2xl font-bold text-gray-900 mb-6">
131
- Shipping Address
132
- </h2>
133
- <div className="grid grid-cols-2 gap-4">
134
- <div className="col-span-2">
135
- <Input
136
- label="Full Name"
137
- {...register('shipping.fullName')}
138
- error={errors.shipping?.fullName?.message}
139
- />
140
- </div>
141
- <div className="col-span-2">
142
- <Input
143
- label="Address Line 1"
144
- {...register('shipping.addressLine1')}
145
- error={errors.shipping?.addressLine1?.message}
146
- />
414
+ <div className="min-h-screen bg-slate-50">
415
+ <section className="relative overflow-hidden bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] text-white">
416
+ <div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.35),_transparent_60%)]" />
417
+ <div className="relative container mx-auto px-4 py-16">
418
+ <div className="grid gap-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
419
+ <motion.div
420
+ initial={{ opacity: 0, y: 24 }}
421
+ animate={{ opacity: 1, y: 0 }}
422
+ className="space-y-6"
423
+ >
424
+ <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">
425
+ <ShieldCheck className="h-4 w-4" />
426
+ Secure checkout
427
+ </span>
428
+ <h1 className="text-4xl font-bold md:text-5xl">
429
+ Provide delivery details
430
+ </h1>
431
+ <p className="text-white/75 md:text-lg">
432
+ Our pharmacists handle every package with care. Share your shipping and billing
433
+ information so we can dispatch your order within the next 12 hours.
434
+ </p>
435
+ <div className="flex flex-wrap gap-3">
436
+ {checkoutSteps.map((step) => (
437
+ <div
438
+ key={step.id}
439
+ className={`flex items-center gap-3 rounded-2xl px-4 py-2 text-sm font-semibold ${step.status === 'complete'
440
+ ? 'bg-white/20 text-white'
441
+ : step.status === 'current'
442
+ ? 'bg-white text-primary-700'
443
+ : 'bg-white/10 text-white/60'
444
+ }`}
445
+ >
446
+ <span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/20 text-xs font-bold">
447
+ {step.id}
448
+ </span>
449
+ {step.label}
147
450
  </div>
148
- <div className="col-span-2">
149
- <Input
150
- label="Address Line 2 (Optional)"
151
- {...register('shipping.addressLine2')}
152
- />
451
+ ))}
452
+ </div>
453
+ </motion.div>
454
+ </div>
455
+ </div>
456
+ </section>
457
+
458
+ <form onSubmit={handleSubmit(onSubmit)}>
459
+ {error && <div className="mb-4 text-red-600 font-semibold">{error}</div>}
460
+ <div className="pt-12 container mx-auto grid gap-10 px-4 lg:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">
461
+ <motion.div
462
+ initial={{ opacity: 0, y: 24 }}
463
+ animate={{ opacity: 1, y: 0 }}
464
+ className="space-y-8"
465
+ >
466
+ <section className="rounded-3xl border border-slate-100 bg-white p-6 shadow-lg shadow-primary-50">
467
+ <div className="flex flex-wrap items-center justify-between gap-4">
468
+ <div>
469
+ <h2 className="text-xl font-semibold text-slate-900">
470
+ Shipping information
471
+ </h2>
472
+ <p className="text-sm text-slate-500">
473
+ We use temperature-aware packaging and real-time tracking on every shipment.
474
+ </p>
475
+ </div>
476
+ <span 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">
477
+ <Truck className="h-4 w-4" />
478
+ Dispatch in 12h
479
+ </span>
480
+ </div>
481
+
482
+ <div className="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
483
+ {/* Address selection for delivery */}
484
+ {isDelivery && (
485
+ <div className="md:col-span-2 space-y-4">
486
+ <div className="flex items-center justify-between">
487
+ <label className="block font-semibold">Select Address</label>
488
+ <Button
489
+ type="button"
490
+ variant="outline"
491
+ size="sm"
492
+ onClick={() => { setEditingAddress(null); setIsAddressModalOpen(true); }}
493
+ >
494
+ <Plus className="h-4 w-4 mr-2" />
495
+ Add New Address
496
+ </Button>
497
+ </div>
498
+
499
+ {userAddresses.length > 0 ? (
500
+ <div className="grid gap-4">
501
+ {userAddresses.map(addr => (
502
+ <label
503
+ key={addr.id}
504
+ className={`group relative flex items-start gap-3 p-4 rounded-2xl border ${selectedAddressId === addr.id
505
+ ? 'border-primary-500 bg-primary-50 shadow-sm'
506
+ : 'border-slate-200 bg-white'
507
+ } cursor-pointer hover:border-primary-300 transition-colors`}
508
+ >
509
+ <input
510
+ type="radio"
511
+ name="selectedAddress"
512
+ value={addr.id}
513
+ checked={selectedAddressId === addr.id}
514
+ onChange={() => {
515
+ setSelectedAddressId(addr.id);
516
+ // Update form with selected address
517
+ setValue('shipping.name', addr.name);
518
+ setValue('shipping.phone', addr.phone || (undefined as unknown as string));
519
+ setValue('shipping.street1', addr.street1);
520
+ setValue('shipping.street2', addr.street2 || '');
521
+ setValue('shipping.city', addr.city);
522
+ setValue('shipping.state', addr.state);
523
+ setValue('shipping.zip', addr.zip);
524
+ setValue('shipping.country', addr.country);
525
+ }}
526
+ className="mt-1"
527
+ />
528
+ <div className="flex-1">
529
+ <p className="font-semibold text-slate-900">{addr.name}</p>
530
+ <p className="text-sm text-slate-600">{addr.street1}</p>
531
+ {addr.street2 && (
532
+ <p className="text-sm text-slate-600">{addr.street2}</p>
533
+ )}
534
+ <p className="text-sm text-slate-600">
535
+ {addr.city}, {addr.state} {addr.zip}
536
+ </p>
537
+ <p className="text-sm text-slate-600">{addr.country}</p>
538
+ {addr.phone && (
539
+ <p className="text-sm text-slate-600 mt-1">{addr.phone}</p>
540
+ )}
541
+ <div className="mt-3 flex items-center gap-2">
542
+ {addr.isDefault && (
543
+ <span className="inline-flex items-center gap-1 rounded-full bg-primary-100 px-2.5 py-0.5 text-xs font-semibold text-primary-700">
544
+ Default
545
+ </span>
546
+ )}
547
+ <button
548
+ type="button"
549
+ onClick={(e) => { e.preventDefault(); setEditingAddress(addr); setIsAddressModalOpen(true); }}
550
+ className="inline-flex items-center gap-1 rounded-full border border-slate-200 px-2.5 py-0.5 text-xs font-medium text-slate-600 hover:border-primary-300 hover:text-primary-600"
551
+ >
552
+ <Edit3 className="h-3.5 w-3.5" /> Edit
553
+ </button>
554
+ <button
555
+ type="button"
556
+ onClick={async (e) => {
557
+ e.preventDefault();
558
+ const yes = window.confirm('Delete this address?');
559
+ if (!yes) return;
560
+ try {
561
+ await removeAddress(addr.id);
562
+ if (selectedAddressId === addr.id) setSelectedAddressId(null);
563
+ toast.success('Address deleted');
564
+ } catch (e) {
565
+ toast.error('Failed to delete address');
566
+ }
567
+ }}
568
+ className="inline-flex items-center gap-1 rounded-full border border-red-200 px-2.5 py-0.5 text-xs font-semibold text-red-600 hover:border-red-300 hover:text-red-700"
569
+ >
570
+ <Trash2 className="h-3.5 w-3.5" /> Delete
571
+ </button>
572
+ </div>
573
+ </div>
574
+ </label>
575
+ ))}
576
+ </div>
577
+ ) : (
578
+ <div className="text-center py-8 bg-slate-50 rounded-lg">
579
+ <MapPin className="h-12 w-12 mx-auto text-slate-400" />
580
+ <p className="mt-2 text-slate-600">No addresses found</p>
581
+ <p className="text-sm text-slate-500">Add a new address to continue</p>
582
+ </div>
583
+ )}
153
584
  </div>
154
- <Input
155
- label="City"
156
- {...register('shipping.city')}
157
- error={errors.shipping?.city?.message}
158
- />
159
- <Input
160
- label="State"
161
- {...register('shipping.state')}
162
- error={errors.shipping?.state?.message}
163
- />
164
- <Input
165
- label="ZIP Code"
166
- {...register('shipping.zipCode')}
167
- error={errors.shipping?.zipCode?.message}
168
- />
169
- <Input
170
- label="Country"
171
- {...register('shipping.country')}
172
- error={errors.shipping?.country?.message}
173
- />
174
- <div className="col-span-2">
175
- <Input
176
- label="Phone Number"
177
- {...register('shipping.phone')}
178
- error={errors.shipping?.phone?.message}
179
- />
585
+ )}
586
+ {/* Store address selection for pickup */}
587
+ {!isDelivery && storeAddresses.length > 0 && (
588
+ <div className="md:col-span-2">
589
+ <label className="block mb-2 font-semibold">Select Pickup Location</label>
590
+ <select
591
+ className="w-full border rounded p-2"
592
+ value={selectedStoreAddressId || ''}
593
+ onChange={e => setSelectedStoreAddressId(e.target.value)}
594
+ >
595
+ {storeAddresses.map(addr => (
596
+ <option key={addr.id} value={addr.id}>{addr.name} - {addr.street1}, {addr.city}</option>
597
+ ))}
598
+ </select>
180
599
  </div>
600
+ )}
601
+ </div>
602
+ </section>
603
+
604
+ {/* Shipping Options - Only show if there's a shipping cost */}
605
+ {isDelivery && selectedAddressId && (
606
+ <Card className="p-6 border border-gray-200 rounded-xl shadow-sm">
607
+ <div className="flex items-center gap-3 text-xl font-semibold text-gray-900 pb-4 mb-6 border-b">
608
+ <Truck className="text-accent w-6 h-6" />
609
+ <span>Shipping Options</span>
181
610
  </div>
182
- </motion.div>
183
-
184
- {/* Billing Address */}
185
- <motion.div
186
- initial={{ opacity: 0, y: 20 }}
187
- animate={{ opacity: 1, y: 0 }}
188
- transition={{ delay: 0.2 }}
189
- className="bg-white rounded-2xl p-6 shadow-sm"
190
- >
191
- <h2 className="text-2xl font-bold text-gray-900 mb-6">
192
- Billing Address
193
- </h2>
194
-
195
- <label className="flex items-center gap-3 mb-6 cursor-pointer">
196
- <input
197
- type="checkbox"
198
- checked={sameAsShipping}
199
- onChange={(e) => setSameAsShipping(e.target.checked)}
200
- className="w-5 h-5 text-primary-600 rounded"
201
- />
202
- <span className="text-gray-700">Same as shipping address</span>
203
- </label>
204
-
205
- {!sameAsShipping && (
206
- <div className="grid grid-cols-2 gap-4">
207
- <div className="col-span-2">
208
- <Input
209
- label="Full Name"
210
- {...register('billing.fullName')}
211
- error={errors.billing?.fullName?.message}
212
- />
213
- </div>
214
- <div className="col-span-2">
215
- <Input
216
- label="Address Line 1"
217
- {...register('billing.addressLine1')}
218
- error={errors.billing?.addressLine1?.message}
219
- />
220
- </div>
221
- <div className="col-span-2">
222
- <Input
223
- label="Address Line 2 (Optional)"
224
- {...register('billing.addressLine2')}
225
- />
611
+
612
+ {shippingRatesLoading ? (
613
+ <div className="flex items-center justify-center py-12">
614
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-accent"></div>
615
+ <span className="ml-3 text-gray-600">Loading shipping options...</span>
616
+ </div>
617
+ ) : shippingRatesError ? (
618
+ <div className="text-center py-12">
619
+ <div className="w-16 h-16 mx-auto mb-4 bg-red-100 rounded-full flex items-center justify-center">
620
+ <svg className="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
621
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
622
+ </svg>
226
623
  </div>
227
- <Input
228
- label="City"
229
- {...register('billing.city')}
230
- error={errors.billing?.city?.message}
231
- />
232
- <Input
233
- label="State"
234
- {...register('billing.state')}
235
- error={errors.billing?.state?.message}
236
- />
237
- <Input
238
- label="ZIP Code"
239
- {...register('billing.zipCode')}
240
- error={errors.billing?.zipCode?.message}
241
- />
242
- <Input
243
- label="Country"
244
- {...register('billing.country')}
245
- error={errors.billing?.country?.message}
246
- />
247
- <div className="col-span-2">
248
- <Input
249
- label="Phone Number"
250
- {...register('billing.phone')}
251
- error={errors.billing?.phone?.message}
252
- />
624
+ <p className="text-red-500 text-lg font-medium">Error loading shipping options</p>
625
+ <p className="text-red-400 text-sm mt-1">
626
+ {shippingRatesError}
627
+ </p>
628
+ </div>
629
+ ) : shippingRates && shippingRates.length > 0 ? (
630
+ <div className="space-y-4">
631
+ {shippingRates.map((rate) => {
632
+ const isSelected = !!selectedShippingRateId && selectedShippingRateId === rate.objectId;
633
+ const isTest = rate.test;
634
+ const hasAttributes = rate.attributes && rate.attributes.length > 0;
635
+
636
+ return (
637
+ <div
638
+ key={rate.objectId}
639
+ onClick={() => setSelectedShippingRateId(rate.objectId)}
640
+ onMouseEnter={() => setShippingPrice(parseFloat(rate.amount))}
641
+ onMouseLeave={() => setShippingPrice(parseFloat(rate.amountLocal))}
642
+ className={`relative p-5 border-2 rounded-xl cursor-pointer transition-all duration-200 hover:shadow-md ${isSelected
643
+ ? 'border-primary-500 bg-primary-50 ring-2 ring-primary-200'
644
+ : 'border-gray-200 hover:border-gray-300'
645
+ }`}
646
+ >
647
+ <div className="flex items-start justify-between">
648
+ <div className="flex items-start gap-4 flex-1">
649
+ {/* Provider Logo */}
650
+ <div className="flex-shrink-0">
651
+ <Image
652
+ src={rate.providerImage75 || '/placeholder-product.jpg'}
653
+ alt={rate.provider}
654
+ className="w-12 h-12 rounded-lg object-contain bg-white border border-gray-200 p-1"
655
+ onError={(e: any) => {
656
+ const target = e.target as HTMLImageElement;
657
+ target.style.display = 'none';
658
+ }}
659
+ width={48}
660
+ height={48}
661
+ />
662
+ </div>
663
+
664
+ {/* Main Content */}
665
+ <div className="flex-1 min-w-0">
666
+ <div className="flex items-center gap-2 mb-2">
667
+ <h3 className="text-lg font-semibold text-gray-900">
668
+ {rate.provider} {rate.servicelevel?.name}
669
+ </h3>
670
+ {isTest && (
671
+ <span className="px-2 py-1 text-xs font-medium bg-orange-100 text-orange-800 rounded-full">
672
+ TEST
673
+ </span>
674
+ )}
675
+ </div>
676
+
677
+ {/* Attributes */}
678
+ {hasAttributes && (
679
+ <div className="flex flex-wrap gap-2 mb-3">
680
+ {rate.attributes.map((attr: string) => (
681
+ <span
682
+ key={attr}
683
+ className={`px-2 py-1 text-xs font-medium rounded-full ${attr === 'FASTEST'
684
+ ? 'bg-green-100 text-green-800'
685
+ : attr === 'BESTVALUE'
686
+ ? 'bg-blue-100 text-blue-800'
687
+ : attr === 'CHEAPEST'
688
+ ? 'bg-purple-100 text-purple-800'
689
+ : 'bg-gray-100 text-gray-800'
690
+ }`}
691
+ >
692
+ {attr}
693
+ </span>
694
+ ))}
695
+ </div>
696
+ )}
697
+
698
+ {/* Delivery Info */}
699
+ <div className="space-y-1 text-sm text-gray-600">
700
+ {rate.durationTerms && (
701
+ <div className="flex items-center gap-2">
702
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
703
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
704
+ </svg>
705
+ <span>{rate.durationTerms}</span>
706
+ </div>
707
+ )}
708
+
709
+ {rate.estimatedDays && (
710
+ <div className="flex items-center gap-2">
711
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
712
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
713
+ </svg>
714
+ <span>Estimated {rate.estimatedDays} day{rate.estimatedDays !== 1 ? 's' : ''}</span>
715
+ </div>
716
+ )}
717
+
718
+ <div className="flex items-center gap-2">
719
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
720
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
721
+ </svg>
722
+ <span>Carrier: {rate.provider}</span>
723
+ </div>
724
+
725
+ <div className="flex items-center gap-2">
726
+ <svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
727
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1" />
728
+ </svg>
729
+ <span>Currency: {rate.currency}</span>
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </div>
734
+
735
+ {/* Price */}
736
+ <div className="flex flex-col items-end">
737
+ <div className="text-2xl font-bold text-gray-900">
738
+ {formatPrice(parseFloat(rate.amount))}
739
+ </div>
740
+ {rate.amount !== rate.amountLocal && (
741
+ <div className="text-sm text-gray-500">
742
+ {rate.amountLocal} {rate.currencyLocal}
743
+ </div>
744
+ )}
745
+ </div>
746
+ </div>
747
+
748
+ {/* Selection Indicator */}
749
+ {isSelected && (
750
+ <div className="absolute top-3 right-3">
751
+ <div className="w-6 h-6 bg-primary-500 rounded-full flex items-center justify-center">
752
+ <CheckIcon className="w-4 h-4 text-white" />
753
+ </div>
754
+ </div>
755
+ )}
756
+ </div>
757
+ );
758
+ })}
759
+ </div>
760
+ ) : (
761
+ <div className="text-center py-12">
762
+ <div className="w-16 h-16 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
763
+ <Truck className="w-8 h-8 text-gray-400" />
253
764
  </div>
765
+ <p className="text-gray-500 text-lg font-medium">No shipping options available</p>
766
+ <p className="text-gray-400 text-sm mt-1">Please check the shipping address or try a different location.</p>
254
767
  </div>
255
768
  )}
256
- </motion.div>
257
- </div>
258
769
 
770
+ </Card>
771
+ )}
772
+ </motion.div>
773
+
774
+ <motion.aside
775
+ initial={{ opacity: 0, y: 32 }}
776
+ animate={{ opacity: 1, y: 0 }}
777
+ transition={{ duration: 0.5, ease: 'easeOut', delay: 0.1 }}
778
+ className="space-y-10 lg:sticky lg:top-24"
779
+ >
259
780
  {/* Order Summary */}
260
- <div className="lg:col-span-1">
261
- <motion.div
262
- initial={{ opacity: 0, y: 20 }}
263
- animate={{ opacity: 1, y: 0 }}
264
- transition={{ delay: 0.3 }}
265
- className="bg-white rounded-2xl p-6 shadow-sm sticky top-24"
266
- >
267
- <h2 className="text-2xl font-bold text-gray-900 mb-6">
268
- Order Summary
269
- </h2>
270
-
271
- {/* Items */}
272
- <div className="space-y-4 mb-6 max-h-64 overflow-y-auto">
273
- {cart.items.map((item) => (
274
- <div key={item.productId} className="flex gap-3">
275
- <div className="w-16 h-16 rounded-lg bg-gray-100 flex-shrink-0" />
276
- <div className="flex-1 min-w-0">
277
- <p className="font-medium text-gray-900 text-sm truncate">
278
- {item.product.name}
781
+ <div className="rounded-3xl border border-slate-100 bg-white p-6 shadow-xl shadow-primary-50/50 transition hover:shadow-primary-100/60">
782
+ <h2 className="text-2xl font-semibold text-slate-900">Order Summary</h2>
783
+
784
+ {/* Delivery Method */}
785
+ <section className="mt-6 space-y-4">
786
+ <h3 className="text-xs font-semibold text-slate-700 uppercase tracking-wider">
787
+ Delivery Method
788
+ </h3>
789
+ <div className="grid grid-cols-1 gap-3">
790
+ {[
791
+ {
792
+ label: 'Delivery',
793
+ icon: <Truck className="w-5 h-5" />,
794
+ value: true,
795
+ desc: 'Shipped to your address',
796
+ },
797
+ {
798
+ label: 'Pickup',
799
+ icon: <MapPin className="w-5 h-5" />,
800
+ value: false,
801
+ desc: 'Collect from pharmacy',
802
+ },
803
+ ].map((option) => {
804
+ const active = isDelivery === option.value
805
+ return (
806
+ <button
807
+ key={option.label}
808
+ type="button"
809
+ onClick={() => setIsDelivery(option.value)}
810
+ aria-pressed={active}
811
+ className={`relative flex w-full items-center justify-between rounded-xl border-2 p-3 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-400 focus-visible:ring-offset-2 ${active
812
+ ? 'border-primary-500 bg-primary-50 shadow-sm'
813
+ : 'border-slate-200 bg-white hover:border-primary-200 hover:bg-primary-50/40'
814
+ }`}
815
+ >
816
+ <div className="flex items-center gap-3">
817
+ <div
818
+ className={`p-2 rounded-lg ${active
819
+ ? 'bg-primary-100 text-primary-600'
820
+ : 'bg-slate-100 text-slate-600 group-hover:bg-slate-50'
821
+ }`}
822
+ >
823
+ {option.icon}
824
+ </div>
825
+ <div className="text-left">
826
+ <div className="font-medium text-slate-900">
827
+ {option.label}
828
+ </div>
829
+ <p className="text-xs text-slate-500">{option.desc}</p>
830
+ </div>
831
+ </div>
832
+ {active && (
833
+ <div className="w-5 h-5 rounded-full bg-primary-500 flex items-center justify-center text-white shadow-sm">
834
+ <CheckIcon className="w-3 h-3" />
835
+ </div>
836
+ )}
837
+ </button>
838
+ )
839
+ })}
840
+ </div>
841
+ </section>
842
+
843
+ {/* Payment Method */}
844
+ <section className="mt-8 space-y-4">
845
+ <h3 className="text-xs font-semibold text-slate-700 uppercase tracking-wider">
846
+ Payment Method
847
+ </h3>
848
+ <div className="space-y-3">
849
+ {PAYMENT_METHODS.map((pm) => {
850
+ const active = paymentMethod === pm.value
851
+ return (
852
+ <button
853
+ key={pm.value}
854
+ type="button"
855
+ onClick={() => setPaymentMethod(pm.value)}
856
+ className={`w-full flex items-center justify-between rounded-xl border-2 p-3 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ${active
857
+ ? `${pm.activeClass} border-current shadow-sm`
858
+ : `${pm.className} border-slate-200 hover:border-primary-200 hover:shadow-sm`
859
+ }`}
860
+ >
861
+ <div className="flex items-center gap-3">
862
+ <div
863
+ className={`p-1.5 rounded-md ${pm.value === 'Card'
864
+ ? 'bg-blue-100 text-blue-600'
865
+ : pm.value === 'Cash'
866
+ ? 'bg-amber-100 text-amber-600'
867
+ : 'bg-emerald-100 text-emerald-600'
868
+ } ${active ? 'bg-opacity-30' : 'bg-opacity-100'}`}
869
+ >
870
+ {pm.icon}
871
+ </div>
872
+ <span className="text-sm font-medium text-slate-900">
873
+ {pm.label}
874
+ </span>
875
+ </div>
876
+ {active && (
877
+ <div className="w-4 h-4 rounded-full bg-primary-500 flex items-center justify-center text-white shadow-sm">
878
+ <CheckIcon className="w-2.5 h-2.5" />
879
+ </div>
880
+ )}
881
+ </button>
882
+ )
883
+ })}
884
+ </div>
885
+ <p className="text-xs text-slate-500 mt-2 pl-1 leading-relaxed">
886
+ {paymentMethod === 'Card' &&
887
+ 'You will be redirected to a secure payment page.'}
888
+ {paymentMethod === 'Cash' &&
889
+ 'Pay with cash at the time of delivery or pickup.'}
890
+ {paymentMethod === 'Credit' &&
891
+ 'Use your available account credit for this order.'}
892
+ </p>
893
+ </section>
894
+
895
+ {/* Cart Summary */}
896
+ <section className="mt-8 pt-6 border-t border-slate-100">
897
+ <div className="max-h-60 space-y-4 overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-slate-200 hover:scrollbar-thumb-slate-300">
898
+ {cart?.cartBody?.items?.map((item: any) => (
899
+ <div
900
+ key={item.productId}
901
+ className="flex gap-4 rounded-2xl border border-slate-100 p-4 hover:bg-slate-50/50 transition"
902
+ >
903
+ <div className="flex h-16 w-16 items-center justify-center rounded-xl bg-slate-100 text-slate-400">
904
+ <Image src={item.productVariantData.productMedia?.[0]?.file || '/placeholder-product.jpg'}
905
+ alt={item.productVariantData.name} width={64} height={64} className="object-contain" />
906
+ </div>
907
+
908
+ <div className="flex-1">
909
+ <p className="text-sm font-semibold text-slate-900">
910
+ {item?.productVariantData?.name}
279
911
  </p>
280
- <p className="text-sm text-gray-500">Qty: {item.quantity}</p>
912
+ <p className="text-xs text-slate-500">Qty {item.quantity}</p>
281
913
  </div>
282
- <p className="font-medium text-gray-900 text-sm">
283
- {formatPrice(item.product.price * item.quantity)}
914
+ <p className="text-sm font-semibold text-slate-900">
915
+ {formatPrice(item.productVariantData.finalPrice * item.quantity)}
284
916
  </p>
285
917
  </div>
286
918
  ))}
287
919
  </div>
288
920
 
289
- <div className="space-y-3 border-t border-gray-200 pt-4 mb-6">
290
- <div className="flex justify-between text-gray-700">
921
+ {/* Totals */}
922
+ <div className="mt-6 space-y-3 text-sm text-slate-600">
923
+ <div className="flex items-center justify-between">
291
924
  <span>Subtotal</span>
292
- <span className="font-medium">{formatPrice(subtotal)}</span>
293
- </div>
294
- <div className="flex justify-between text-gray-700">
295
- <span>Shipping</span>
296
- <span className="font-medium">
297
- {shipping === 0 ? (
298
- <span className="text-green-600">FREE</span>
299
- ) : (
300
- formatPrice(shipping)
301
- )}
925
+ <span className="font-semibold text-slate-900">
926
+ {formatPrice(subtotal)}
302
927
  </span>
303
928
  </div>
304
- <div className="flex justify-between text-gray-700">
305
- <span>Tax</span>
306
- <span className="font-medium">{formatPrice(tax)}</span>
307
- </div>
308
- <div className="border-t border-gray-200 pt-3">
309
- <div className="flex justify-between">
310
- <span className="text-xl font-bold text-gray-900">Total</span>
311
- <span className="text-2xl font-bold text-gray-900">
312
- {formatPrice(total)}
929
+ {isDelivery && (
930
+ <div className="flex items-center justify-between">
931
+ <span>Shipping</span>
932
+ <span className="font-semibold">
933
+ {formatPrice(shippingPrice)}
313
934
  </span>
314
935
  </div>
936
+ )}
937
+ <div className="flex items-center justify-between">
938
+ <span>Estimated tax</span>
939
+ <span className="font-semibold">{formatPrice(tax)}</span>
940
+ </div>
941
+ <div className="rounded-2xl bg-slate-50 p-4">
942
+ <div className="flex items-center justify-between text-base font-semibold text-slate-900">
943
+ <span>Total Due</span>
944
+ <span>{formatPrice(total)}</span>
945
+ </div>
946
+ <p className="mt-1 text-xs text-slate-500">
947
+ Tax is estimated. Final amount confirmed after payment.
948
+ </p>
315
949
  </div>
316
950
  </div>
951
+ </section>
317
952
 
318
- <Button
319
- type="submit"
320
- size="lg"
321
- isLoading={isSubmitting}
322
- className="w-full"
323
- >
324
- <CreditCard className="w-5 h-5" />
325
- Place Order
326
- </Button>
327
-
328
- <div className="flex items-center justify-center gap-2 text-sm text-gray-500 mt-4">
329
- <Lock className="w-4 h-4" />
330
- <span>Secure checkout powered by Stripe</span>
331
- </div>
332
- </motion.div>
953
+ {/* Checkout Button */}
954
+ <Button
955
+ type="submit"
956
+ size="lg"
957
+ isLoading={isSubmitting}
958
+ className="mt-6 w-full transition-transform hover:scale-[1.02] active:scale-[0.99]"
959
+ >
960
+ <CreditCard className="h-5 w-5" />
961
+ {isSubmitting ? 'Placing order...' : 'Place Secure Order'}
962
+ </Button>
963
+
964
+ <p className="mt-4 flex items-center justify-center gap-2 text-xs text-slate-500">
965
+ <Lock className="h-4 w-4" />
966
+ Fully encrypted checkout — cancel anytime before shipment.
967
+ </p>
333
968
  </div>
334
- </div>
335
- </form>
336
- </div>
969
+
970
+ {/* Why Patients Choose Us */}
971
+ <div className="rounded-3xl border border-primary-100 bg-primary-50/80 p-6 text-sm text-primary-700 shadow-sm hover:shadow-md transition-shadow">
972
+ <p className="font-semibold uppercase tracking-[0.3em] text-primary-800">
973
+ Why Patients Choose Us
974
+ </p>
975
+ <ul className="mt-4 space-y-3 text-primary-700 leading-relaxed">
976
+ <li className="flex items-start gap-3">
977
+ <PackageCheck className="mt-0.5 h-4 w-4 shrink-0" />
978
+ <span>Pharmacy-grade verification on every order.</span>
979
+ </li>
980
+ <li className="flex items-start gap-3">
981
+ <ShieldCheck className="mt-0.5 h-4 w-4 shrink-0" />
982
+ <span>Cold-chain logistics for sensitive medications.</span>
983
+ </li>
984
+ <li className="flex items-start gap-3">
985
+ <Truck className="mt-0.5 h-4 w-4 shrink-0" />
986
+ <span>Real-time tracking and SMS updates from prep to delivery.</span>
987
+ </li>
988
+ </ul>
989
+ </div>
990
+ </motion.aside>
991
+
992
+ </div>
993
+ </form>
994
+ <AddressFormModal
995
+ isOpen={isAddressModalOpen}
996
+ onClose={() => setIsAddressModalOpen(false)}
997
+ initialAddress={editingAddress}
998
+ onAddressAdded={(addr) => {
999
+ refresh().then(() => {
1000
+ setSelectedAddressId(addr.id);
1001
+ setValue('shipping.name', addr.name);
1002
+ setValue('shipping.phone', addr.phone || (undefined as unknown as string));
1003
+ setValue('shipping.street1', addr.street1);
1004
+ setValue('shipping.street2', addr.street2 || '');
1005
+ setValue('shipping.city', addr.city);
1006
+ setValue('shipping.state', addr.state);
1007
+ setValue('shipping.zip', addr.zip);
1008
+ setValue('shipping.country', addr.country);
1009
+ });
1010
+ }}
1011
+ onAddressUpdated={() => {
1012
+ refresh();
1013
+ }}
1014
+ />
1015
+ {/* </div> */}
337
1016
  </div>
338
1017
  );
339
1018
  }
340
-