hey-pharmacist-ecommerce 1.1.13 → 1.1.15
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 +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1039 -857
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1039 -856
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AccountAddressesTab.tsx +209 -0
- package/src/components/AccountOrdersTab.tsx +151 -0
- package/src/components/AccountOverviewTab.tsx +209 -0
- package/src/components/AccountPaymentTab.tsx +116 -0
- package/src/components/AccountSavedItemsTab.tsx +76 -0
- package/src/components/AccountSettingsTab.tsx +116 -0
- package/src/components/AddressFormModal.tsx +23 -10
- package/src/components/CartItem.tsx +60 -56
- package/src/components/Header.tsx +69 -16
- package/src/components/Notification.tsx +148 -0
- package/src/components/ProductCard.tsx +215 -178
- package/src/components/QuickViewModal.tsx +314 -0
- package/src/components/TabNavigation.tsx +48 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/ConfirmModal.tsx +84 -0
- package/src/hooks/usePaymentMethods.ts +58 -0
- package/src/index.ts +0 -1
- package/src/providers/CartProvider.tsx +22 -6
- package/src/providers/EcommerceProvider.tsx +8 -7
- package/src/providers/FavoritesProvider.tsx +10 -3
- package/src/providers/NotificationProvider.tsx +79 -0
- package/src/providers/WishlistProvider.tsx +34 -9
- package/src/screens/AddressesScreen.tsx +72 -61
- package/src/screens/CartScreen.tsx +48 -32
- package/src/screens/ChangePasswordScreen.tsx +155 -0
- package/src/screens/CheckoutScreen.tsx +162 -125
- package/src/screens/EditProfileScreen.tsx +165 -0
- package/src/screens/LoginScreen.tsx +59 -72
- package/src/screens/NewAddressScreen.tsx +16 -10
- package/src/screens/ProductDetailScreen.tsx +334 -234
- package/src/screens/ProfileScreen.tsx +190 -200
- package/src/screens/RegisterScreen.tsx +51 -70
- package/src/screens/SearchResultsScreen.tsx +2 -1
- package/src/screens/ShopScreen.tsx +260 -384
- package/src/screens/WishlistScreen.tsx +226 -224
- package/src/styles/globals.css +9 -0
- package/src/screens/CategoriesScreen.tsx +0 -122
- package/src/screens/HomeScreen.tsx +0 -211
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback } from 'react';
|
|
4
|
+
import { NotificationContainer, NotificationData, NotificationType } from '@/components/Notification';
|
|
5
|
+
|
|
6
|
+
interface NotificationContextValue {
|
|
7
|
+
success: (message: string, description?: string, duration?: number) => void;
|
|
8
|
+
error: (message: string, description?: string, duration?: number) => void;
|
|
9
|
+
warning: (message: string, description?: string, duration?: number) => void;
|
|
10
|
+
info: (message: string, description?: string, duration?: number) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const NotificationContext = createContext<NotificationContextValue | undefined>(undefined);
|
|
14
|
+
|
|
15
|
+
export function useNotification() {
|
|
16
|
+
const context = useContext(NotificationContext);
|
|
17
|
+
if (!context) {
|
|
18
|
+
throw new Error('useNotification must be used within NotificationProvider');
|
|
19
|
+
}
|
|
20
|
+
return context;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface NotificationProviderProps {
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function NotificationProvider({ children }: NotificationProviderProps) {
|
|
28
|
+
const [notifications, setNotifications] = useState<NotificationData[]>([]);
|
|
29
|
+
|
|
30
|
+
const addNotification = useCallback(
|
|
31
|
+
(type: NotificationType, message: string, description?: string, duration?: number) => {
|
|
32
|
+
const id = Math.random().toString(36).substring(2, 9);
|
|
33
|
+
const notification: NotificationData = {
|
|
34
|
+
id,
|
|
35
|
+
type,
|
|
36
|
+
message,
|
|
37
|
+
description,
|
|
38
|
+
duration: duration || 4000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
setNotifications((prev) => [...prev, notification]);
|
|
42
|
+
},
|
|
43
|
+
[]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const dismissNotification = useCallback((id: string) => {
|
|
47
|
+
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
|
48
|
+
}, []);
|
|
49
|
+
|
|
50
|
+
const value: NotificationContextValue = {
|
|
51
|
+
success: useCallback(
|
|
52
|
+
(message: string, description?: string, duration?: number) =>
|
|
53
|
+
addNotification('success', message, description, duration),
|
|
54
|
+
[addNotification]
|
|
55
|
+
),
|
|
56
|
+
error: useCallback(
|
|
57
|
+
(message: string, description?: string, duration?: number) =>
|
|
58
|
+
addNotification('error', message, description, duration),
|
|
59
|
+
[addNotification]
|
|
60
|
+
),
|
|
61
|
+
warning: useCallback(
|
|
62
|
+
(message: string, description?: string, duration?: number) =>
|
|
63
|
+
addNotification('warning', message, description, duration),
|
|
64
|
+
[addNotification]
|
|
65
|
+
),
|
|
66
|
+
info: useCallback(
|
|
67
|
+
(message: string, description?: string, duration?: number) =>
|
|
68
|
+
addNotification('info', message, description, duration),
|
|
69
|
+
[addNotification]
|
|
70
|
+
),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<NotificationContext.Provider value={value}>
|
|
75
|
+
{children}
|
|
76
|
+
<NotificationContainer notifications={notifications} onDismiss={dismissNotification} />
|
|
77
|
+
</NotificationContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -4,10 +4,10 @@ import React, { createContext, useContext, useState, useEffect, ReactNode, useCa
|
|
|
4
4
|
import { WishlistApi } from '@/lib/Apis/apis/wishlist-api';
|
|
5
5
|
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
6
6
|
import { useAuth } from './AuthProvider';
|
|
7
|
-
import { toast } from 'sonner';
|
|
8
7
|
import { useMemo } from 'react';
|
|
9
8
|
import { Wishlist } from '@/lib/Apis';
|
|
10
9
|
import { Product } from '@/lib/Apis/models';
|
|
10
|
+
import { useNotification } from './NotificationProvider';
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
interface WishlistContextType extends Wishlist {
|
|
@@ -31,6 +31,7 @@ export function WishlistProvider({ children }: { children: ReactNode }) {
|
|
|
31
31
|
} as unknown as Wishlist);
|
|
32
32
|
|
|
33
33
|
const { isAuthenticated } = useAuth() || {};
|
|
34
|
+
const notification = useNotification();
|
|
34
35
|
const wishlistApi = useMemo(() => new WishlistApi(AXIOS_CONFIG), []);
|
|
35
36
|
|
|
36
37
|
const fetchWishlist = useCallback(async () => {
|
|
@@ -74,14 +75,20 @@ export function WishlistProvider({ children }: { children: ReactNode }) {
|
|
|
74
75
|
|
|
75
76
|
const addToWishlist = async (product: Product) => {
|
|
76
77
|
if (!isAuthenticated) {
|
|
77
|
-
|
|
78
|
+
notification.error(
|
|
79
|
+
'Sign-in required',
|
|
80
|
+
'Please sign in to add items to your wishlist.'
|
|
81
|
+
);
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
try {
|
|
82
86
|
// First check if the item is already in the wishlist
|
|
83
87
|
if (isInWishlist(product?._id || '')) {
|
|
84
|
-
|
|
88
|
+
notification.info(
|
|
89
|
+
'Already saved',
|
|
90
|
+
'This item is already in your wishlist.'
|
|
91
|
+
);
|
|
85
92
|
return;
|
|
86
93
|
}
|
|
87
94
|
|
|
@@ -92,10 +99,16 @@ export function WishlistProvider({ children }: { children: ReactNode }) {
|
|
|
92
99
|
// This ensures we're in sync with the server and prevents duplicates
|
|
93
100
|
await fetchWishlist();
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
notification.success(
|
|
103
|
+
'Added to wishlist',
|
|
104
|
+
`${product?.name || 'Item'} was added to your wishlist.`
|
|
105
|
+
);
|
|
96
106
|
} catch (error) {
|
|
97
107
|
console.error('Error adding to wishlist:', error);
|
|
98
|
-
|
|
108
|
+
notification.error(
|
|
109
|
+
'Could not add to wishlist',
|
|
110
|
+
'Something went wrong while adding this item. Please try again.'
|
|
111
|
+
);
|
|
99
112
|
throw error;
|
|
100
113
|
}
|
|
101
114
|
};
|
|
@@ -113,10 +126,16 @@ export function WishlistProvider({ children }: { children: ReactNode }) {
|
|
|
113
126
|
|
|
114
127
|
});
|
|
115
128
|
|
|
116
|
-
|
|
129
|
+
notification.success(
|
|
130
|
+
'Removed from wishlist',
|
|
131
|
+
'The item has been removed from your wishlist.'
|
|
132
|
+
);
|
|
117
133
|
} catch (error) {
|
|
118
134
|
console.error('Error removing from wishlist:', error);
|
|
119
|
-
|
|
135
|
+
notification.error(
|
|
136
|
+
'Could not remove item',
|
|
137
|
+
'Something went wrong while removing this item. Please try again.'
|
|
138
|
+
);
|
|
120
139
|
throw error;
|
|
121
140
|
}
|
|
122
141
|
};
|
|
@@ -128,10 +147,16 @@ export function WishlistProvider({ children }: { children: ReactNode }) {
|
|
|
128
147
|
...prev,
|
|
129
148
|
products: [],
|
|
130
149
|
}));
|
|
131
|
-
|
|
150
|
+
notification.success(
|
|
151
|
+
'Wishlist cleared',
|
|
152
|
+
'All items have been removed from your wishlist.'
|
|
153
|
+
);
|
|
132
154
|
} catch (error) {
|
|
133
155
|
console.error('Error clearing wishlist:', error);
|
|
134
|
-
|
|
156
|
+
notification.error(
|
|
157
|
+
'Could not clear wishlist',
|
|
158
|
+
'We could not clear your wishlist. Please try again.'
|
|
159
|
+
);
|
|
135
160
|
throw error;
|
|
136
161
|
}
|
|
137
162
|
};
|
|
@@ -23,7 +23,6 @@ import { Input } from '@/components/ui/Input';
|
|
|
23
23
|
import { Modal } from '@/components/ui/Modal';
|
|
24
24
|
import { EmptyState } from '@/components/EmptyState';
|
|
25
25
|
import { useAddresses } from '@/hooks/useAddresses';
|
|
26
|
-
import { toast } from 'sonner';
|
|
27
26
|
import { Address, AddressAddressTypeEnum, CreateAddressDtoAddressTypeEnum, UpdateAddressDto, UpdateAddressDtoAddressTypeEnum } from '@/lib/Apis';
|
|
28
27
|
|
|
29
28
|
const addressFormSchema = z.object({
|
|
@@ -86,6 +85,9 @@ export function AddressesScreen() {
|
|
|
86
85
|
|
|
87
86
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
88
87
|
const [editingAddress, setEditingAddress] = useState<Address | null>(null);
|
|
88
|
+
const [banner, setBanner] = useState<{ type: 'success' | 'error'; message: string } | null>(
|
|
89
|
+
null
|
|
90
|
+
);
|
|
89
91
|
|
|
90
92
|
const {
|
|
91
93
|
register,
|
|
@@ -147,10 +149,10 @@ export function AddressesScreen() {
|
|
|
147
149
|
if (editingAddress) {
|
|
148
150
|
const response = await updateAddress(editingAddress.id, payload);
|
|
149
151
|
if (response) {
|
|
150
|
-
|
|
152
|
+
setBanner({ type: 'success', message: 'Address updated successfully' });
|
|
151
153
|
closeModal();
|
|
152
154
|
} else {
|
|
153
|
-
|
|
155
|
+
setBanner({ type: 'error', message: 'Unable to update address' });
|
|
154
156
|
}
|
|
155
157
|
} else {
|
|
156
158
|
const response = await addAddress({
|
|
@@ -165,10 +167,10 @@ export function AddressesScreen() {
|
|
|
165
167
|
addressType: values.addressType as CreateAddressDtoAddressTypeEnum,
|
|
166
168
|
});
|
|
167
169
|
if (response) {
|
|
168
|
-
|
|
170
|
+
setBanner({ type: 'success', message: 'Address added successfully' });
|
|
169
171
|
closeModal();
|
|
170
172
|
} else {
|
|
171
|
-
|
|
173
|
+
setBanner({ type: 'error', message: 'Failed to add address' });
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
};
|
|
@@ -181,10 +183,12 @@ export function AddressesScreen() {
|
|
|
181
183
|
|
|
182
184
|
try {
|
|
183
185
|
await removeAddress(address.id);
|
|
184
|
-
|
|
186
|
+
setBanner({ type: 'success', message: 'Address removed successfully' });
|
|
185
187
|
} catch (error) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
+
setBanner({
|
|
189
|
+
type: 'error',
|
|
190
|
+
message:
|
|
191
|
+
error instanceof Error ? error.message : 'Failed to remove address. Please try again.',
|
|
188
192
|
});
|
|
189
193
|
}
|
|
190
194
|
};
|
|
@@ -192,10 +196,12 @@ export function AddressesScreen() {
|
|
|
192
196
|
const handleSetDefault = async (address: Address) => {
|
|
193
197
|
try {
|
|
194
198
|
await markAsDefault(address.id);
|
|
195
|
-
|
|
199
|
+
setBanner({ type: 'success', message: `${address.name} is now your default address` });
|
|
196
200
|
} catch (error) {
|
|
197
|
-
|
|
198
|
-
|
|
201
|
+
setBanner({
|
|
202
|
+
type: 'error',
|
|
203
|
+
message:
|
|
204
|
+
error instanceof Error ? error.message : 'Failed to set default address. Try again.',
|
|
199
205
|
});
|
|
200
206
|
}
|
|
201
207
|
};
|
|
@@ -226,65 +232,70 @@ export function AddressesScreen() {
|
|
|
226
232
|
}, [addresses]);
|
|
227
233
|
|
|
228
234
|
return (
|
|
229
|
-
<div className="min-h-screen bg-slate-50">
|
|
230
|
-
<
|
|
231
|
-
<div className="
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
initial={{ opacity: 0, y: 24 }}
|
|
236
|
-
animate={{ opacity: 1, y: 0 }}
|
|
237
|
-
className="space-y-5"
|
|
238
|
-
>
|
|
239
|
-
<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">
|
|
235
|
+
<div className="min-h-screen bg-slate-50 text-slate-900">
|
|
236
|
+
<div className="container mx-auto px-4 pb-16 pt-10">
|
|
237
|
+
<div className="rounded-3xl border border-slate-200 bg-white/95 p-6 shadow-xl shadow-primary-50">
|
|
238
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
<div className="inline-flex items-center gap-2 rounded-full bg-slate-900 text-white px-4 py-1 text-xs font-semibold uppercase tracking-[0.32em]">
|
|
240
241
|
<MapPin className="h-4 w-4" />
|
|
241
242
|
Address book
|
|
242
|
-
</
|
|
243
|
-
<h1 className="text-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<p className="max-w-2xl text-white/80 md:text-lg">
|
|
247
|
-
Add home, office, or loved ones' addresses and toggle a default for lightning-fast
|
|
248
|
-
checkout and delivery.
|
|
243
|
+
</div>
|
|
244
|
+
<h1 className="text-3xl font-semibold md:text-4xl">Manage where your care arrives</h1>
|
|
245
|
+
<p className="max-w-2xl text-sm text-slate-600">
|
|
246
|
+
Save home, office, or loved ones' addresses and choose a default for faster checkout and delivery.
|
|
249
247
|
</p>
|
|
250
|
-
<div className="flex flex-wrap
|
|
251
|
-
<span className="inline-flex items-center gap-2 rounded-full bg-
|
|
252
|
-
<Sparkles className="h-4 w-4 text-
|
|
253
|
-
Default
|
|
248
|
+
<div className="flex flex-wrap gap-3 text-xs text-slate-500">
|
|
249
|
+
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-2">
|
|
250
|
+
<Sparkles className="h-4 w-4 text-primary-600" />
|
|
251
|
+
Default: {defaultAddress ? defaultAddress.name : 'Not set'}
|
|
254
252
|
</span>
|
|
255
|
-
<Button variant="ghost" className="text-white hover:bg-white/10" onClick={openCreateModal}>
|
|
256
|
-
<Plus className="h-5 w-5" />
|
|
257
|
-
Add address
|
|
258
|
-
</Button>
|
|
259
253
|
</div>
|
|
260
|
-
</
|
|
254
|
+
</div>
|
|
255
|
+
<div className="flex items-center gap-3">
|
|
256
|
+
{banner && (
|
|
257
|
+
<div
|
|
258
|
+
className={`flex items-start gap-2 rounded-2xl border px-3 py-2 text-xs ${
|
|
259
|
+
banner.type === 'success'
|
|
260
|
+
? 'border-green-200 bg-green-50 text-green-700'
|
|
261
|
+
: 'border-red-200 bg-red-50 text-red-700'
|
|
262
|
+
}`}
|
|
263
|
+
>
|
|
264
|
+
<span className="mt-[1px] text-base">{banner.type === 'success' ? '✔' : '!'}</span>
|
|
265
|
+
<span>{banner.message}</span>
|
|
266
|
+
</div>
|
|
267
|
+
)}
|
|
268
|
+
<Button
|
|
269
|
+
variant="outline"
|
|
270
|
+
size="md"
|
|
271
|
+
className="border-slate-300 text-slate-800 hover:bg-white"
|
|
272
|
+
onClick={openCreateModal}
|
|
273
|
+
>
|
|
274
|
+
<Plus className="h-5 w-5" />
|
|
275
|
+
Add address
|
|
276
|
+
</Button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
261
279
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
<p className="text-white">{stat.helper}</p>
|
|
275
|
-
</div>
|
|
276
|
-
<span className="text-3xl font-semibold text-white">{stat.value}</span>
|
|
280
|
+
<div className="mt-6 grid gap-4 sm:grid-cols-3">
|
|
281
|
+
{stats.map((stat) => (
|
|
282
|
+
<div
|
|
283
|
+
key={stat.id}
|
|
284
|
+
className="rounded-2xl border border-slate-200 bg-slate-50/70 px-4 py-3 text-sm text-slate-600"
|
|
285
|
+
>
|
|
286
|
+
<p className="text-xs font-semibold uppercase tracking-[0.28em] text-slate-500">
|
|
287
|
+
{stat.label}
|
|
288
|
+
</p>
|
|
289
|
+
<div className="mt-1 flex items-center justify-between">
|
|
290
|
+
<p className="text-slate-800">{stat.helper}</p>
|
|
291
|
+
<span className="text-xl font-semibold text-slate-900">{stat.value}</span>
|
|
277
292
|
</div>
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
280
295
|
</div>
|
|
281
296
|
</div>
|
|
282
|
-
</section>
|
|
283
|
-
|
|
284
|
-
<div className="relative -mt-16 pb-20">
|
|
285
|
-
<div className="container mx-auto px-4">
|
|
286
|
-
|
|
287
297
|
|
|
298
|
+
<div className="mt-10">
|
|
288
299
|
{isLoading ? (
|
|
289
300
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-3">
|
|
290
301
|
{Array.from({ length: 3 }).map((_, index) => (
|
|
@@ -317,7 +328,7 @@ export function AddressesScreen() {
|
|
|
317
328
|
key={address.id}
|
|
318
329
|
initial={{ opacity: 0, y: 24 }}
|
|
319
330
|
animate={{ opacity: 1, y: 0 }}
|
|
320
|
-
className="group relative flex h-full flex-col rounded-3xl border border-slate-
|
|
331
|
+
className="group relative flex h-full flex-col rounded-3xl border border-slate-200 bg-white p-6 shadow-sm transition hover:-translate-y-1 hover:border-primary-200 hover:shadow-lg"
|
|
321
332
|
>
|
|
322
333
|
{address.isDefault && (
|
|
323
334
|
<span className="absolute right-6 top-6 inline-flex items-center gap-2 rounded-full bg-amber-100 px-3 py-1 text-xs font-semibold text-amber-700">
|
|
@@ -17,22 +17,22 @@ import { formatPrice } from '@/lib/utils/format';
|
|
|
17
17
|
import { useRouter } from 'next/navigation';
|
|
18
18
|
import { CartItemPopulated } from '@/lib/Apis';
|
|
19
19
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
20
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
21
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
export function CartScreen() {
|
|
23
25
|
const router = useRouter();
|
|
24
26
|
const { cart, isLoading } = useCart();
|
|
25
27
|
const { buildPath } = useBasePath();
|
|
28
|
+
const { isAuthenticated, user } = useAuth();
|
|
29
|
+
const notification = useNotification();
|
|
26
30
|
|
|
27
31
|
if (!cart || cart.cartBody.items.length === 0) {
|
|
28
32
|
return (
|
|
29
33
|
<div className="bg-white">
|
|
30
34
|
<div className="min-h-screen bg-white max-w-6xl mx-auto">
|
|
31
35
|
<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>
|
|
36
36
|
|
|
37
37
|
<div className="flex flex-col items-center justify-center py-20">
|
|
38
38
|
<motion.div
|
|
@@ -42,15 +42,15 @@ export function CartScreen() {
|
|
|
42
42
|
>
|
|
43
43
|
<div className="flex justify-center">
|
|
44
44
|
<div className="rounded-full bg-gray-100 p-6">
|
|
45
|
-
<ShoppingBag className="h-12 w-12 text-
|
|
45
|
+
<ShoppingBag className="h-12 w-12 text-secondary" />
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
49
|
<div className="space-y-2">
|
|
50
|
-
<h2 className="text-2xl font-bold text-
|
|
50
|
+
<h2 className="text-2xl font-bold text-secondary">
|
|
51
51
|
Your cart is empty
|
|
52
52
|
</h2>
|
|
53
|
-
<p className="text-
|
|
53
|
+
<p className="text-muted">
|
|
54
54
|
Start adding products to your cart to see them here.
|
|
55
55
|
</p>
|
|
56
56
|
</div>
|
|
@@ -59,7 +59,7 @@ export function CartScreen() {
|
|
|
59
59
|
<button
|
|
60
60
|
type="button"
|
|
61
61
|
onClick={() => router.push(buildPath('/shop'))}
|
|
62
|
-
className="rounded-
|
|
62
|
+
className="rounded-xl border-2 border-primary bg-primary text-white px-6 py-3 text-sm font-medium transition-colors flex items-center justify-center gap-2 hover:opacity-80"
|
|
63
63
|
>
|
|
64
64
|
Discover products
|
|
65
65
|
<ArrowRight className="h-5 w-5" />
|
|
@@ -68,15 +68,15 @@ export function CartScreen() {
|
|
|
68
68
|
|
|
69
69
|
<div className="mt-8 space-y-3 pt-6 border-t border-gray-200">
|
|
70
70
|
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
71
|
-
<CheckCircle2 className="h-5 w-5 text-
|
|
71
|
+
<CheckCircle2 className="h-5 w-5 text-secondary flex-shrink-0 mt-0.5" />
|
|
72
72
|
<span>Free shipping on all orders</span>
|
|
73
73
|
</div>
|
|
74
74
|
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
75
|
-
<CheckCircle2 className="h-5 w-5 text-
|
|
75
|
+
<CheckCircle2 className="h-5 w-5 text-secondary flex-shrink-0 mt-0.5" />
|
|
76
76
|
<span>Easy returns within 30 days</span>
|
|
77
77
|
</div>
|
|
78
78
|
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
79
|
-
<CheckCircle2 className="h-5 w-5 text-
|
|
79
|
+
<CheckCircle2 className="h-5 w-5 text-secondary flex-shrink-0 mt-0.5" />
|
|
80
80
|
<span>Secure checkout process</span>
|
|
81
81
|
</div>
|
|
82
82
|
</div>
|
|
@@ -94,22 +94,38 @@ export function CartScreen() {
|
|
|
94
94
|
const total = subtotal + shipping + tax;
|
|
95
95
|
const itemCount = cart.cartBody.items.length;
|
|
96
96
|
|
|
97
|
+
const handleSubmit = async (e?: React.FormEvent<HTMLFormElement> | React.MouseEvent) => {
|
|
98
|
+
if (!isAuthenticated) {
|
|
99
|
+
notification.error(
|
|
100
|
+
'Sign-in required',
|
|
101
|
+
'Please log in to complete your checkout.'
|
|
102
|
+
);
|
|
103
|
+
setTimeout(() => router.push(buildPath('/login?redirect=/checkout')), 50);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
router.push(buildPath('/checkout'));
|
|
108
|
+
};
|
|
109
|
+
|
|
97
110
|
return (
|
|
98
|
-
<div className="bg-white">
|
|
99
|
-
<div className="
|
|
111
|
+
<div className="min-h-screen bg-white">
|
|
112
|
+
<div className="max-w-[1400px] mx-auto px-8 md:px-12">
|
|
100
113
|
<div className="container mx-auto px-4 py-8">
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
<
|
|
114
|
+
{/* Header */}
|
|
115
|
+
<div className="mb-12">
|
|
116
|
+
<h1 className="font-['Poppins',sans-serif] font-semibold text-secondary tracking-[-2px] mb-2 text-4xl">
|
|
117
|
+
Shopping Cart
|
|
118
|
+
</h1>
|
|
119
|
+
<p className="font-['Poppins',sans-serif] text-[16px] text-muted">
|
|
104
120
|
{itemCount} {itemCount === 1 ? 'item' : 'items'} in your cart
|
|
105
121
|
</p>
|
|
106
122
|
</div>
|
|
107
|
-
<div className="grid
|
|
123
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
|
108
124
|
{/* Shopping Cart Section */}
|
|
109
125
|
<motion.section
|
|
110
126
|
initial={{ opacity: 0, y: 24 }}
|
|
111
127
|
animate={{ opacity: 1, y: 0 }}
|
|
112
|
-
className="space-y-6"
|
|
128
|
+
className="space-y-6 lg:col-span-2"
|
|
113
129
|
>
|
|
114
130
|
|
|
115
131
|
|
|
@@ -132,15 +148,15 @@ export function CartScreen() {
|
|
|
132
148
|
initial={{ opacity: 0, y: 24 }}
|
|
133
149
|
animate={{ opacity: 1, y: 0 }}
|
|
134
150
|
transition={{ delay: 0.1 }}
|
|
135
|
-
className="space-y-6 lg:sticky lg:top-
|
|
151
|
+
className="space-y-6 lg:sticky lg:top-24 h-fit lg:col-span-1"
|
|
136
152
|
>
|
|
137
|
-
<div className="rounded-
|
|
138
|
-
<h2 className="
|
|
153
|
+
<div className="bg-gradient-to-br from-[#5B9BD5]/10 to-[#2B4B7C]/10 rounded-[24px] p-8 border-2 border-[#5B9BD5]/20 sticky top-24">
|
|
154
|
+
<h2 className="font-['Poppins',sans-serif] font-semibold text-secondary mb-6">Order Summary</h2>
|
|
139
155
|
|
|
140
156
|
<div className="space-y-4 mb-6">
|
|
141
|
-
<div className="flex items-center justify-between
|
|
142
|
-
<span className="text-
|
|
143
|
-
<span className="font-semibold text-
|
|
157
|
+
<div className="flex items-center justify-between">
|
|
158
|
+
<span className="font-['Poppins',sans-serif] text-[14px] text-muted">Subtotal ({itemCount} {itemCount === 1 ? 'item' : 'items'})</span>
|
|
159
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[14px] text-secondary">{formatPrice(subtotal)}</span>
|
|
144
160
|
</div>
|
|
145
161
|
<div className="flex items-center justify-between text-sm">
|
|
146
162
|
<span className="text-gray-600">Shipping</span>
|
|
@@ -150,7 +166,7 @@ export function CartScreen() {
|
|
|
150
166
|
</div>
|
|
151
167
|
<div className="border-t border-gray-200 pt-4 mt-4">
|
|
152
168
|
<div className="flex items-center justify-between">
|
|
153
|
-
<span className="text-lg font-bold text-
|
|
169
|
+
<span className="text-lg font-bold text-secondary">Total</span>
|
|
154
170
|
<span className="text-2xl font-bold text-orange-600">{formatPrice(total)}</span>
|
|
155
171
|
</div>
|
|
156
172
|
</div>
|
|
@@ -158,8 +174,8 @@ export function CartScreen() {
|
|
|
158
174
|
|
|
159
175
|
<div className="space-y-3">
|
|
160
176
|
<button
|
|
161
|
-
type="
|
|
162
|
-
onClick={
|
|
177
|
+
type="submit"
|
|
178
|
+
onClick={handleSubmit}
|
|
163
179
|
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
180
|
>
|
|
165
181
|
Proceed to Checkout
|
|
@@ -168,7 +184,7 @@ export function CartScreen() {
|
|
|
168
184
|
<button
|
|
169
185
|
type="button"
|
|
170
186
|
onClick={() => router.push(buildPath('/shop'))}
|
|
171
|
-
className="w-full rounded-full border-2 border-
|
|
187
|
+
className="w-full rounded-full border-2 border-muted bg-white px-4 py-3 text-sm font-medium text-slate-700 hover:bg-gray-50 transition-colors"
|
|
172
188
|
>
|
|
173
189
|
Continue Shopping
|
|
174
190
|
</button>
|
|
@@ -176,23 +192,23 @@ export function CartScreen() {
|
|
|
176
192
|
|
|
177
193
|
<div className="mt-6 space-y-3 pt-6 border-t border-gray-200">
|
|
178
194
|
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
179
|
-
<CheckCircle2 className="h-5 w-5 text-
|
|
195
|
+
<CheckCircle2 className="h-5 w-5 text-muted flex-shrink-0 mt-0.5" />
|
|
180
196
|
<span>Easy returns within 30 days</span>
|
|
181
197
|
</div>
|
|
182
198
|
<div className="flex items-start gap-3 text-sm text-slate-600">
|
|
183
|
-
<CheckCircle2 className="h-5 w-5 text-
|
|
199
|
+
<CheckCircle2 className="h-5 w-5 text-muted flex-shrink-0 mt-0.5" />
|
|
184
200
|
<span>Secure checkout process</span>
|
|
185
201
|
</div>
|
|
186
202
|
</div>
|
|
187
203
|
</div>
|
|
188
204
|
|
|
189
|
-
<div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
|
|
205
|
+
{/* <div className="rounded-3xl border border-primary-100 bg-primary-50/70 p-6 text-sm text-primary-700 shadow-sm">
|
|
190
206
|
<p className="font-semibold uppercase tracking-[0.3em]">Need help?</p>
|
|
191
207
|
<p className="mt-2 leading-relaxed">
|
|
192
208
|
Chat with a pharmacist to optimize your regimen or discuss substitutions before you
|
|
193
209
|
check out.
|
|
194
210
|
</p>
|
|
195
|
-
</div>
|
|
211
|
+
</div> */}
|
|
196
212
|
</motion.aside>
|
|
197
213
|
</div>
|
|
198
214
|
</div>
|