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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hey-pharmacist-ecommerce",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Production-ready, multi-tenant e‑commerce UI + API adapter for Next.js with auth, carts, checkout, orders, theming, and pharmacist-focused UX.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"autoprefixer": "^10.4.0",
|
|
38
38
|
"eslint": "^8.0.0",
|
|
39
39
|
"eslint-config-next": "^14.0.0",
|
|
40
|
-
"next": "^
|
|
40
|
+
"next": "^16.0.10",
|
|
41
41
|
"postcss": "^8.4.0",
|
|
42
42
|
"react": "^18.2.0",
|
|
43
43
|
"react-dom": "^18.2.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"typescript": "^5.3.0"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"next": "^
|
|
49
|
+
"next": "^16.0.10",
|
|
50
50
|
"react": "^18.0.0",
|
|
51
51
|
"react-dom": "^18.0.0",
|
|
52
52
|
"react-hook-form": "^7.0.0"
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { MapPin, Plus, Edit3, Trash2, Star } from 'lucide-react';
|
|
5
|
+
import { useAddresses } from '@/hooks/useAddresses';
|
|
6
|
+
import { EmptyState } from './EmptyState';
|
|
7
|
+
import { Button } from './ui/Button';
|
|
8
|
+
import { Address } from '@/lib/Apis';
|
|
9
|
+
import { AddressFormModal } from './AddressFormModal';
|
|
10
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
11
|
+
import { ConfirmModal } from './ui/ConfirmModal';
|
|
12
|
+
|
|
13
|
+
export function AccountAddressesTab() {
|
|
14
|
+
const { addresses, isLoading, error, removeAddress, markAsDefault, refresh } = useAddresses();
|
|
15
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
16
|
+
const [editingAddress, setEditingAddress] = useState<Address | undefined>(undefined);
|
|
17
|
+
const [isAddressModalOpen, setIsAddressModalOpen] = useState(false);
|
|
18
|
+
const [selectedAddressId, setSelectedAddressId] = useState<string | null>(null);
|
|
19
|
+
const [addressToDelete, setAddressToDelete] = useState<Address | null>(null);
|
|
20
|
+
const notification = useNotification();
|
|
21
|
+
|
|
22
|
+
const handleDelete = async (address: Address) => {
|
|
23
|
+
if (!confirm(`Remove ${address.name}'s address?\nYou can add it back at any time.`)) return;
|
|
24
|
+
|
|
25
|
+
setDeletingId(address.id);
|
|
26
|
+
try {
|
|
27
|
+
await removeAddress(address.id);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('Failed to delete address:', error);
|
|
30
|
+
alert('Failed to delete address. Please try again.');
|
|
31
|
+
} finally {
|
|
32
|
+
setDeletingId(null);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const handleSetDefault = async (address: Address) => {
|
|
37
|
+
try {
|
|
38
|
+
await markAsDefault(address.id);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Failed to set default address:', error);
|
|
41
|
+
alert('Failed to set default address. Please try again.');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const handleConfirmDelete = async () => {
|
|
46
|
+
if (!addressToDelete) return;
|
|
47
|
+
|
|
48
|
+
setDeletingId(addressToDelete.id);
|
|
49
|
+
try {
|
|
50
|
+
await removeAddress(addressToDelete.id);
|
|
51
|
+
if (selectedAddressId === addressToDelete.id) {
|
|
52
|
+
setSelectedAddressId(null);
|
|
53
|
+
}
|
|
54
|
+
notification.success('Address deleted successfully');
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error('Failed to delete address:', error);
|
|
57
|
+
notification.error('Failed to delete address. Please try again.');
|
|
58
|
+
} finally {
|
|
59
|
+
setDeletingId(null);
|
|
60
|
+
setAddressToDelete(null);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (isLoading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="p-6">
|
|
67
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
68
|
+
{Array.from({ length: 2 }).map((_, index) => (
|
|
69
|
+
<div
|
|
70
|
+
key={index}
|
|
71
|
+
className="h-48 animate-pulse rounded-lg border border-slate-100 bg-slate-50"
|
|
72
|
+
/>
|
|
73
|
+
))}
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (error) {
|
|
80
|
+
return (
|
|
81
|
+
<div className="p-6">
|
|
82
|
+
<div className="rounded-lg border border-red-100 bg-red-50 p-6 text-sm text-red-700">
|
|
83
|
+
{error.message}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<>
|
|
91
|
+
<div className="p-6 bg-white rounded-xl">
|
|
92
|
+
<div className="mb-6 flex items-center justify-between">
|
|
93
|
+
<div>
|
|
94
|
+
<h3 className="text-lg font-semibold text-secondary">Saved Addresses</h3>
|
|
95
|
+
<p className="text-sm text-muted">Manage your delivery and billing addresses</p>
|
|
96
|
+
</div>
|
|
97
|
+
<button
|
|
98
|
+
className='flex flex-row items-center gap-2 px-6 py-2 border border-slate-200 rounded-xl text-slate-700 hover:opacity-80 transition-colors bg-secondary text-white text-sm'
|
|
99
|
+
onClick={() => {
|
|
100
|
+
setEditingAddress(undefined);
|
|
101
|
+
setIsAddressModalOpen(true);
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
<Plus className="h-4 w-4" />
|
|
105
|
+
Add Address
|
|
106
|
+
</button>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{addresses.length === 0 ? (
|
|
110
|
+
<EmptyState
|
|
111
|
+
icon={MapPin}
|
|
112
|
+
title="No addresses yet"
|
|
113
|
+
description="Save a shipping or billing address to speed through checkout."
|
|
114
|
+
actionLabel="Add your first address"
|
|
115
|
+
onAction={() => {
|
|
116
|
+
setEditingAddress(undefined);
|
|
117
|
+
setIsAddressModalOpen(true);
|
|
118
|
+
}}
|
|
119
|
+
/>
|
|
120
|
+
) : (
|
|
121
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
122
|
+
{addresses.map((address) => (
|
|
123
|
+
<div
|
|
124
|
+
key={address.id}
|
|
125
|
+
className="relative rounded-xl border border-slate-200 bg-white p-4 hover:shadow-md transition-shadow"
|
|
126
|
+
>
|
|
127
|
+
|
|
128
|
+
<div className="mb-3">
|
|
129
|
+
<div className='justify-between flex items-center'>
|
|
130
|
+
<div className="flex items-center gap-x-2">
|
|
131
|
+
<MapPin className="h-4 w-4 text-secondary" />
|
|
132
|
+
<p className="font-semibold text-secondary">{address.name}</p>
|
|
133
|
+
{address.isDefault && (
|
|
134
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-1 text-xs font-semibold text-green-700">
|
|
135
|
+
Default
|
|
136
|
+
</span>
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
<div className="flex items-center gap-x-2">
|
|
140
|
+
<button
|
|
141
|
+
onClick={() => {
|
|
142
|
+
setEditingAddress(address);
|
|
143
|
+
setIsAddressModalOpen(true);
|
|
144
|
+
}}
|
|
145
|
+
className="inline-flex items-center gap-1 rounded-full border border-slate-200 px-2 py-1 text-xs font-medium text-muted transition hover:border-primary-300 hover:text-primary-600"
|
|
146
|
+
>
|
|
147
|
+
<Edit3 className="h-3 w-3" />
|
|
148
|
+
</button>
|
|
149
|
+
{/* {!address.isDefault && (
|
|
150
|
+
<button
|
|
151
|
+
onClick={() => handleSetDefault(address)}
|
|
152
|
+
className="inline-flex items-center gap-1 rounded-full border border-amber-200 px-2 py-1 text-xs font-semibold text-amber-600 transition hover:border-amber-300 hover:text-amber-700"
|
|
153
|
+
>
|
|
154
|
+
<Star className="h-3 w-3" />
|
|
155
|
+
Make default
|
|
156
|
+
</button>
|
|
157
|
+
)} */}
|
|
158
|
+
<button
|
|
159
|
+
onClick={() => setAddressToDelete(address)}
|
|
160
|
+
disabled={deletingId === address.id}
|
|
161
|
+
className="inline-flex items-center gap-1 rounded-full border border-red-200 px-2 py-1 text-xs font-semibold text-red-600 transition hover:border-red-300 hover:text-red-700 disabled:opacity-50"
|
|
162
|
+
>
|
|
163
|
+
<Trash2 className="h-3 w-3" />
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<p className="text-sm text-muted mt-1">
|
|
168
|
+
{address.street1}
|
|
169
|
+
{address.street2 && `, ${address.street2}`}
|
|
170
|
+
</p>
|
|
171
|
+
<p className="text-sm text-muted">
|
|
172
|
+
{address.city}, {address.state} {address.zip}
|
|
173
|
+
</p>
|
|
174
|
+
<p className="text-sm text-muted">{address.country}</p>
|
|
175
|
+
{address.phone && (
|
|
176
|
+
<p className="text-sm text-slate-500 mt-1">{address.phone}</p>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
))}
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
<AddressFormModal
|
|
185
|
+
isOpen={isAddressModalOpen}
|
|
186
|
+
onClose={() => setIsAddressModalOpen(false)}
|
|
187
|
+
initialAddress={editingAddress}
|
|
188
|
+
onAddressAdded={() => {
|
|
189
|
+
refresh();
|
|
190
|
+
}}
|
|
191
|
+
onAddressUpdated={() => {
|
|
192
|
+
refresh();
|
|
193
|
+
}}
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
<ConfirmModal
|
|
197
|
+
isOpen={!!addressToDelete}
|
|
198
|
+
onClose={() => setAddressToDelete(null)}
|
|
199
|
+
onConfirm={handleConfirmDelete}
|
|
200
|
+
title="Delete Address"
|
|
201
|
+
message={`Are you sure you want to delete ${addressToDelete?.name}'s address? This action cannot be undone.`}
|
|
202
|
+
confirmText="Delete"
|
|
203
|
+
cancelText="Cancel"
|
|
204
|
+
variant="danger"
|
|
205
|
+
isLoading={deletingId === addressToDelete?.id}
|
|
206
|
+
/>
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Package, Truck } from 'lucide-react';
|
|
5
|
+
import { useCurrentOrders } from '@/hooks/useOrders';
|
|
6
|
+
import { EmptyState } from './EmptyState';
|
|
7
|
+
import { Badge } from './ui/Badge';
|
|
8
|
+
import { formatPrice, formatDate } from '@/lib/utils/format';
|
|
9
|
+
import Image from 'next/image';
|
|
10
|
+
|
|
11
|
+
export function AccountOrdersTab() {
|
|
12
|
+
const { orders, isLoading, error } = useCurrentOrders();
|
|
13
|
+
|
|
14
|
+
if (isLoading) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="py-6 px-3 pb-24">
|
|
17
|
+
<div className="space-y-4">
|
|
18
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
19
|
+
<div
|
|
20
|
+
key={index}
|
|
21
|
+
className="h-64 animate-pulse rounded-xl bg-slate-50"
|
|
22
|
+
/>
|
|
23
|
+
))}
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (error) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="py-6 px-3 pb-24">
|
|
32
|
+
<div className="rounded-lg border border-red-100 bg-red-50 p-6 text-sm text-red-700">
|
|
33
|
+
{error.message}
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (orders.length === 0) {
|
|
40
|
+
return (
|
|
41
|
+
<div className="py-6 px-3 pb-24">
|
|
42
|
+
<EmptyState
|
|
43
|
+
icon={Package}
|
|
44
|
+
title="No orders yet"
|
|
45
|
+
description="When you place orders, they'll appear here for easy tracking."
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="py-6 px-3 space-y-4 pb-24">
|
|
53
|
+
{orders.map((order) => {
|
|
54
|
+
const itemCount = order.items?.length || 0;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div
|
|
58
|
+
key={order._id}
|
|
59
|
+
className="rounded-xl border border-slate-200 bg-white p-6 hover:shadow-md transition-shadow"
|
|
60
|
+
>
|
|
61
|
+
{/* Order Header */}
|
|
62
|
+
<div className="flex items-start justify-between mb-4 pb-4 border-b border-slate-200">
|
|
63
|
+
<div className="flex items-center gap-3">
|
|
64
|
+
<div>
|
|
65
|
+
<h3 className="text-base font-semibold text-secondary">
|
|
66
|
+
Order ORD-{order._id?.slice(-6).toUpperCase()}
|
|
67
|
+
</h3>
|
|
68
|
+
{/* Order Date */}
|
|
69
|
+
<p className="text-xs text-muted mb-4">
|
|
70
|
+
Placed on {formatDate(order.createdAt || new Date(), 'long')}
|
|
71
|
+
</p>
|
|
72
|
+
</div>
|
|
73
|
+
<Badge
|
|
74
|
+
className="text-xs"
|
|
75
|
+
variant={
|
|
76
|
+
order.orderStatus === 'Delivered' ? 'success' :
|
|
77
|
+
order.orderStatus === 'In Transit' ? 'primary' :
|
|
78
|
+
order.orderStatus === 'Pending' ? 'warning' : 'gray'
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
{order.orderStatus}
|
|
82
|
+
</Badge>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="text-right">
|
|
85
|
+
<p className="text-xs text-muted mb-1">Total Amount</p>
|
|
86
|
+
<p className="text-lg font-bold text-secondary">
|
|
87
|
+
{formatPrice(order.grandTotal || 0)}
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
{/* Order Items */}
|
|
95
|
+
<div className="space-y-3 mb-4">
|
|
96
|
+
{order.items && order.items.length > 0 ? (
|
|
97
|
+
order.items.map((item, index) => {
|
|
98
|
+
const itemPrice = item.productVariantData?.finalPrice || 0;
|
|
99
|
+
const itemTotal = itemPrice * item.quantity;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div key={item.productVariantId || index} className="flex items-center gap-3">
|
|
103
|
+
<div className="relative w-12 h-12 rounded-lg bg-slate-100 flex-shrink-0 overflow-hidden">
|
|
104
|
+
<Image
|
|
105
|
+
src={item?.productVariantData?.productMedia?.[0]?.file || '/placeholder-product.jpg'}
|
|
106
|
+
alt={item?.productVariantData?.name || 'Product image'}
|
|
107
|
+
fill
|
|
108
|
+
className="object-cover"
|
|
109
|
+
sizes="48px"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div className="flex-1 min-w-0">
|
|
113
|
+
<p className="font-medium text-secondary text-sm truncate">
|
|
114
|
+
{item.productVariantData?.name || 'Unknown Product'}
|
|
115
|
+
</p>
|
|
116
|
+
<p className="text-xs text-muted">Qty: {item.quantity}</p>
|
|
117
|
+
</div>
|
|
118
|
+
<p className="font-semibold text-secondary text-sm">
|
|
119
|
+
{formatPrice(itemTotal)}
|
|
120
|
+
</p>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
})
|
|
124
|
+
) : (
|
|
125
|
+
<p className="text-sm text-muted text-center py-2">No items found</p>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
{/* Tracking Information */}
|
|
130
|
+
{/* {order.trackingNumber && (
|
|
131
|
+
<div className="mt-4 pt-4 border-t border-slate-200">
|
|
132
|
+
<div className="flex items-center gap-2 bg-blue-50 rounded-lg p-3">
|
|
133
|
+
<Truck className="h-4 w-4 text-blue-600" />
|
|
134
|
+
<div className="flex-1">
|
|
135
|
+
<p className="text-xs text-muted">Tracking Number:</p>
|
|
136
|
+
<p className="text-sm font-medium text-secondary">
|
|
137
|
+
{order.trackingNumber}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
<button className="text-xs text-primary-600 hover:text-primary-700 font-medium">
|
|
141
|
+
Track
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
)} */}
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { User, Mail, Phone, MapPin, Package, CheckCircle, Heart, Clock, Edit } from 'lucide-react';
|
|
5
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
6
|
+
import { useCurrentOrders } from '@/hooks/useOrders';
|
|
7
|
+
import { useWishlist } from '@/providers/WishlistProvider';
|
|
8
|
+
import { formatPrice, formatDate } from '@/lib/utils/format';
|
|
9
|
+
import { Badge } from './ui/Badge';
|
|
10
|
+
import { Button } from './ui/Button';
|
|
11
|
+
import { useRouter } from 'next/navigation';
|
|
12
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
13
|
+
|
|
14
|
+
export function AccountOverviewTab() {
|
|
15
|
+
const { user } = useAuth();
|
|
16
|
+
const { orders, isLoading: ordersLoading } = useCurrentOrders();
|
|
17
|
+
const { getWishlistCount } = useWishlist();
|
|
18
|
+
const router = useRouter();
|
|
19
|
+
const { buildPath } = useBasePath();
|
|
20
|
+
|
|
21
|
+
if (!user) return null;
|
|
22
|
+
|
|
23
|
+
// Calculate stats
|
|
24
|
+
const totalOrders = orders?.length || 0;
|
|
25
|
+
const deliveredOrders = orders?.filter(order => order.orderStatus === 'Delivered')?.length || 0;
|
|
26
|
+
const inTransitOrders = orders?.filter(order => order.orderStatus === 'In Transit')?.length || 0;
|
|
27
|
+
const savedItemsCount = getWishlistCount();
|
|
28
|
+
|
|
29
|
+
// Get recent orders (last 3)
|
|
30
|
+
const recentOrders = orders?.slice(0, 3) || [];
|
|
31
|
+
|
|
32
|
+
const stats = [
|
|
33
|
+
{
|
|
34
|
+
icon: Package,
|
|
35
|
+
label: 'Total Orders',
|
|
36
|
+
value: totalOrders,
|
|
37
|
+
color: 'bg-[#DBEAFE] text-[#5B9BD5]',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
icon: CheckCircle,
|
|
41
|
+
label: 'Delivered',
|
|
42
|
+
value: deliveredOrders,
|
|
43
|
+
color: 'bg-[#DCFCE7] text-[#00A63E]',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
icon: Heart,
|
|
47
|
+
label: 'Saved Items',
|
|
48
|
+
value: savedItemsCount,
|
|
49
|
+
color: 'bg-[#FFEDD4] text-[#FF6B35]',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
icon: Clock,
|
|
53
|
+
label: 'In Transit',
|
|
54
|
+
value: inTransitOrders,
|
|
55
|
+
color: 'bg-[#F3E8FF] text-[#9810FA]',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div className="py-6 px-3 space-y-6 pb-24">
|
|
61
|
+
{/* Stats Cards */}
|
|
62
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
63
|
+
{stats.map((stat, index) => {
|
|
64
|
+
const Icon = stat.icon;
|
|
65
|
+
return (
|
|
66
|
+
<div
|
|
67
|
+
key={index}
|
|
68
|
+
className="rounded-xl border border-slate-200 bg-white p-4 hover:shadow-md transition-shadow"
|
|
69
|
+
>
|
|
70
|
+
<div className="flex items-center gap-3">
|
|
71
|
+
<div className={`flex h-12 w-12 items-center justify-center rounded-lg ${stat.color}`}>
|
|
72
|
+
<Icon className="h-6 w-6" />
|
|
73
|
+
</div>
|
|
74
|
+
<div>
|
|
75
|
+
<p className="text-2xl font-bold text-secondary">{stat.value}</p>
|
|
76
|
+
<p className="text-sm text-slate-600">{stat.label}</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
);
|
|
81
|
+
})}
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Profile Information */}
|
|
85
|
+
<div className="rounded-xl border border-slate-200 bg-white p-6">
|
|
86
|
+
<div className="flex items-center justify-between mb-4">
|
|
87
|
+
<h3 className="text-lg font-semibold text-secondary">Profile Information</h3>
|
|
88
|
+
<button
|
|
89
|
+
onClick={() => router.push(buildPath('/account/edit'))}
|
|
90
|
+
className="flex items-center gap-1 text-sm text-primary-600 hover:text-primary-700"
|
|
91
|
+
>
|
|
92
|
+
<Edit className="h-4 w-4" />
|
|
93
|
+
Edit
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
97
|
+
<div className="flex items-start gap-3">
|
|
98
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#DBEAFE]">
|
|
99
|
+
<User className="h-5 w-5 text-[#5B9BD5]" />
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<p className="text-xs text-muted">Full Name</p>
|
|
103
|
+
<p className="text-sm font-medium text-secondary">
|
|
104
|
+
{user.firstname} {user.lastname}
|
|
105
|
+
</p>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div className="flex items-start gap-3">
|
|
109
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#DBEAFE]">
|
|
110
|
+
<Mail className="h-5 w-5 text-[#5B9BD5]" />
|
|
111
|
+
</div>
|
|
112
|
+
<div>
|
|
113
|
+
<p className="text-xs text-muted">E-mail Address</p>
|
|
114
|
+
<p className="text-sm font-medium text-secondary">{user.email}</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="flex items-start gap-3">
|
|
118
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#DBEAFE]">
|
|
119
|
+
<Phone className="h-5 w-5 text-[#5B9BD5]" />
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<p className="text-xs text-muted">Phone Number</p>
|
|
123
|
+
<p className="text-sm font-medium text-secondary">
|
|
124
|
+
{user.phoneNumber || 'Not provided'}
|
|
125
|
+
</p>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex items-start gap-3">
|
|
129
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-[#DBEAFE]">
|
|
130
|
+
<MapPin className="h-5 w-5 text-[#5B9BD5]" />
|
|
131
|
+
</div>
|
|
132
|
+
<div>
|
|
133
|
+
<p className="text-xs text-muted">Date of Birth</p>
|
|
134
|
+
<p className="text-sm font-medium text-secondary">Not provided</p>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Recent Orders */}
|
|
141
|
+
<div className="rounded-xl border border-slate-200 bg-white p-6">
|
|
142
|
+
<div className="flex items-center justify-between mb-4">
|
|
143
|
+
<h3 className="text-lg font-semibold text-secondary">Recent Orders</h3>
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => {
|
|
146
|
+
// Switch to orders tab
|
|
147
|
+
const event = new CustomEvent('switchTab', { detail: 'orders' });
|
|
148
|
+
window.dispatchEvent(event);
|
|
149
|
+
}}
|
|
150
|
+
className="text-sm text-primary-600 hover:text-primary-700"
|
|
151
|
+
>
|
|
152
|
+
View All →
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
{ordersLoading ? (
|
|
156
|
+
<div className="space-y-3">
|
|
157
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
158
|
+
<div
|
|
159
|
+
key={index}
|
|
160
|
+
className="h-16 animate-pulse rounded-lg bg-slate-50"
|
|
161
|
+
/>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
) : recentOrders.length === 0 ? (
|
|
165
|
+
<p className="text-sm text-muted text-center py-8">No orders yet</p>
|
|
166
|
+
) : (
|
|
167
|
+
<div className="space-y-3">
|
|
168
|
+
{recentOrders.map((order) => (
|
|
169
|
+
<div
|
|
170
|
+
key={order._id}
|
|
171
|
+
className="flex items-center justify-between rounded-lg bg-slate-50 p-4 hover:bg-slate-100 transition-colors"
|
|
172
|
+
>
|
|
173
|
+
<div className="flex items-center gap-3">
|
|
174
|
+
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white">
|
|
175
|
+
<Package className="h-5 w-5 text-slate-600" />
|
|
176
|
+
</div>
|
|
177
|
+
<div>
|
|
178
|
+
<div className='flex items-center gap-2'>
|
|
179
|
+
<p className="text-sm font-medium text-secondary">
|
|
180
|
+
ORD-{order._id?.slice(-6).toUpperCase()}
|
|
181
|
+
</p>
|
|
182
|
+
<Badge className='p-1 text-xs'
|
|
183
|
+
variant={
|
|
184
|
+
order.orderStatus === 'Delivered' ? 'success' :
|
|
185
|
+
order.orderStatus === 'In Transit' ? 'primary' :
|
|
186
|
+
order.orderStatus === 'Pending' ? 'warning' : 'gray'
|
|
187
|
+
}>
|
|
188
|
+
{order.orderStatus}
|
|
189
|
+
</Badge>
|
|
190
|
+
</div>
|
|
191
|
+
<p className="text-xs text-muted">
|
|
192
|
+
{formatDate(order.createdAt || new Date(), 'short')}
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
<div className="flex items-center gap-3">
|
|
197
|
+
|
|
198
|
+
<p className="text-sm font-semibold text-secondary">
|
|
199
|
+
{formatPrice(order.grandTotal || 0)}
|
|
200
|
+
</p>
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|