hey-pharmacist-ecommerce 1.1.30 → 1.1.32
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 +1451 -1303
- package/dist/index.d.ts +1451 -1303
- package/dist/index.js +6162 -1563
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5854 -1271
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/components/AccountReviewsTab.tsx +97 -0
- package/src/components/AccountSettingsTab.tsx +0 -50
- package/src/components/CouponCodeInput.tsx +190 -0
- package/src/components/Header.tsx +5 -1
- package/src/components/Notification.tsx +1 -1
- package/src/components/NotificationBell.tsx +33 -0
- package/src/components/NotificationCard.tsx +211 -0
- package/src/components/NotificationDrawer.tsx +188 -0
- package/src/components/OrderCard.tsx +164 -99
- package/src/components/ProductCard.tsx +3 -3
- package/src/components/ProductReviewsSection.tsx +30 -0
- package/src/components/RatingDistribution.tsx +86 -0
- package/src/components/ReviewCard.tsx +59 -0
- package/src/components/ReviewForm.tsx +207 -0
- package/src/components/ReviewPromptBanner.tsx +98 -0
- package/src/components/ReviewsList.tsx +151 -0
- package/src/components/StarRating.tsx +98 -0
- package/src/components/TabNavigation.tsx +1 -1
- package/src/components/ui/Button.tsx +1 -1
- package/src/hooks/useDiscounts.ts +7 -0
- package/src/hooks/useOrders.ts +15 -0
- package/src/hooks/useReviews.ts +230 -0
- package/src/index.ts +25 -0
- package/src/lib/Apis/apis/discounts-api.ts +23 -72
- package/src/lib/Apis/apis/notifications-api.ts +196 -231
- package/src/lib/Apis/apis/products-api.ts +84 -0
- package/src/lib/Apis/apis/review-api.ts +283 -4
- package/src/lib/Apis/apis/stores-api.ts +180 -0
- package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
- package/src/lib/Apis/models/cart-body-populated.ts +3 -3
- package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
- package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
- package/src/lib/Apis/models/create-discount-dto.ts +31 -92
- package/src/lib/Apis/models/create-review-dto.ts +4 -4
- package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
- package/src/lib/Apis/models/create-store-dto.ts +6 -0
- package/src/lib/Apis/models/discount.ts +37 -98
- package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
- package/src/lib/Apis/models/index.ts +13 -7
- package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
- package/src/lib/Apis/models/manual-order-dto.ts +3 -3
- package/src/lib/Apis/models/populated-discount.ts +41 -101
- package/src/lib/Apis/models/preference-update-item.ts +59 -0
- package/src/lib/Apis/models/product-light-dto.ts +40 -0
- package/src/lib/Apis/models/{check-notifications-response-dto.ts → review-status-dto.ts} +8 -7
- package/src/lib/Apis/models/review.ts +9 -3
- package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
- package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
- package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
- package/src/lib/Apis/models/store-entity.ts +6 -0
- package/src/lib/Apis/models/store.ts +6 -0
- package/src/lib/Apis/models/update-discount-dto.ts +31 -92
- package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
- package/src/lib/Apis/models/update-review-dto.ts +4 -4
- package/src/lib/Apis/models/update-store-dto.ts +6 -0
- package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
- package/src/lib/utils/discount.ts +155 -0
- package/src/lib/validations/discount.ts +11 -0
- package/src/providers/CartProvider.tsx +2 -2
- package/src/providers/DiscountProvider.tsx +97 -0
- package/src/providers/EcommerceProvider.tsx +13 -5
- package/src/providers/NotificationCenterProvider.tsx +420 -0
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/CheckoutScreen.tsx +39 -12
- package/src/screens/NotificationSettingsScreen.tsx +321 -0
- package/src/screens/OrderDetailScreen.tsx +283 -0
- package/src/screens/OrderReviewsScreen.tsx +308 -0
- package/src/screens/OrdersScreen.tsx +31 -7
- package/src/screens/ProductDetailScreen.tsx +24 -11
- package/src/screens/ProfileScreen.tsx +5 -0
- package/src/styles/globals.css +4 -0
- package/styles/base.css +6 -0
- package/styles/globals.css +3 -0
- package/src/lib/Apis/models/create-notification-dto.ts +0 -75
- package/src/lib/Apis/models/notification.ts +0 -93
- package/src/lib/Apis/models/single-notification-dto.ts +0 -99
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { useRouter } from 'next/navigation';
|
|
5
|
+
import { PopulatedOrder } from '@/lib/Apis/models';
|
|
6
|
+
import { ReviewForm } from '@/components/ReviewForm';
|
|
7
|
+
import { ArrowLeft, Package } from 'lucide-react';
|
|
8
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
9
|
+
import Image from 'next/image';
|
|
10
|
+
import { useCurrentOrders } from '@/hooks/useOrders';
|
|
11
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
12
|
+
|
|
13
|
+
interface SelectedProduct {
|
|
14
|
+
productId: string;
|
|
15
|
+
productName: string;
|
|
16
|
+
productVariantId: string;
|
|
17
|
+
variantName: string;
|
|
18
|
+
variantImage?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function OrderReviewsScreen() {
|
|
22
|
+
const router = useRouter();
|
|
23
|
+
const { isAuthenticated } = useAuth();
|
|
24
|
+
const { orders, isLoading, error, refetch } = useCurrentOrders();
|
|
25
|
+
const [selectedOrder, setSelectedOrder] = useState<PopulatedOrder | null>(null);
|
|
26
|
+
const [selectedProduct, setSelectedProduct] = useState<SelectedProduct | null>(null);
|
|
27
|
+
|
|
28
|
+
// If not authenticated, redirect to login
|
|
29
|
+
React.useEffect(() => {
|
|
30
|
+
if (!isLoading && !isAuthenticated) {
|
|
31
|
+
router.push('/login');
|
|
32
|
+
}
|
|
33
|
+
}, [isAuthenticated, isLoading, router]);
|
|
34
|
+
|
|
35
|
+
const handleOrderClick = (order: PopulatedOrder) => {
|
|
36
|
+
console.log('Selected order:', order.id || order._id, 'Status:', order.orderStatus);
|
|
37
|
+
console.log('Full order data:', JSON.stringify(order, null, 2));
|
|
38
|
+
setSelectedOrder(order);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const handleProductClick = (product: SelectedProduct) => {
|
|
42
|
+
console.log('Selected product:', product.productId, 'Variant:', product.productVariantId);
|
|
43
|
+
setSelectedProduct(product);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleReviewSuccess = () => {
|
|
47
|
+
console.log('Review submitted successfully!');
|
|
48
|
+
setSelectedProduct(null);
|
|
49
|
+
setSelectedOrder(null);
|
|
50
|
+
refetch();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="container mx-auto px-4 py-8">
|
|
56
|
+
<div className="max-w-4xl mx-auto">
|
|
57
|
+
<div className="animate-pulse space-y-4">
|
|
58
|
+
<div className="h-8 bg-gray-200 rounded w-1/3" />
|
|
59
|
+
<div className="space-y-3">
|
|
60
|
+
{[1, 2, 3].map((i) => (
|
|
61
|
+
<div key={i} className="h-24 bg-gray-200 rounded" />
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="container mx-auto px-4 py-8">
|
|
73
|
+
<div className="max-w-4xl mx-auto">
|
|
74
|
+
<div className="text-center py-12">
|
|
75
|
+
<p className="text-red-600 mb-4">{error.message}</p>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => router.back()}
|
|
78
|
+
className="text-[#E67E50] hover:underline"
|
|
79
|
+
>
|
|
80
|
+
Go Back
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Show review form if a product is selected
|
|
89
|
+
if (selectedProduct && selectedOrder) {
|
|
90
|
+
return (
|
|
91
|
+
<div className="container mx-auto px-4 py-8">
|
|
92
|
+
<div className="max-w-2xl mx-auto">
|
|
93
|
+
<button
|
|
94
|
+
onClick={() => setSelectedProduct(null)}
|
|
95
|
+
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
|
|
96
|
+
>
|
|
97
|
+
<ArrowLeft className="size-5" />
|
|
98
|
+
Back to Products
|
|
99
|
+
</button>
|
|
100
|
+
|
|
101
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6 mb-6">
|
|
102
|
+
<div className="flex items-center gap-4 mb-6">
|
|
103
|
+
{selectedProduct.variantImage && (
|
|
104
|
+
<div className="relative w-20 h-20 rounded-lg overflow-hidden bg-gray-100">
|
|
105
|
+
<Image
|
|
106
|
+
src={selectedProduct.variantImage}
|
|
107
|
+
alt={selectedProduct.productName}
|
|
108
|
+
fill
|
|
109
|
+
className="object-cover"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
<div>
|
|
114
|
+
<h3 className="font-semibold text-lg text-gray-900">
|
|
115
|
+
{selectedProduct.productName}
|
|
116
|
+
</h3>
|
|
117
|
+
<p className="text-sm text-gray-600">{selectedProduct.variantName}</p>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<ReviewForm
|
|
122
|
+
productId={selectedProduct.productId}
|
|
123
|
+
productVariantId={selectedProduct.productVariantId}
|
|
124
|
+
orderId={(() => {
|
|
125
|
+
const resolvedOrderId = selectedOrder.id || selectedOrder._id || '';
|
|
126
|
+
console.log('Passing orderId to ReviewForm:', resolvedOrderId);
|
|
127
|
+
console.log('Order userId:', selectedOrder.userId);
|
|
128
|
+
return resolvedOrderId;
|
|
129
|
+
})()}
|
|
130
|
+
onSuccess={handleReviewSuccess}
|
|
131
|
+
onCancel={() => setSelectedProduct(null)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Show order products if an order is selected
|
|
140
|
+
if (selectedOrder) {
|
|
141
|
+
return (
|
|
142
|
+
<div className="container mx-auto px-4 py-8">
|
|
143
|
+
<div className="max-w-4xl mx-auto">
|
|
144
|
+
<button
|
|
145
|
+
onClick={() => setSelectedOrder(null)}
|
|
146
|
+
className="flex items-center gap-2 text-gray-600 hover:text-gray-900 mb-6"
|
|
147
|
+
>
|
|
148
|
+
<ArrowLeft className="size-5" />
|
|
149
|
+
Back to Orders
|
|
150
|
+
</button>
|
|
151
|
+
|
|
152
|
+
<div className="bg-white rounded-lg border border-gray-200 p-6 mb-6">
|
|
153
|
+
<div className="flex items-center justify-between mb-4">
|
|
154
|
+
<div>
|
|
155
|
+
<h2 className="text-xl font-semibold text-gray-900">
|
|
156
|
+
Order #{(selectedOrder.id || selectedOrder._id || '').slice(-8)}
|
|
157
|
+
</h2>
|
|
158
|
+
<p className="text-sm text-gray-600">Status: {selectedOrder.orderStatus}</p>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="space-y-4">
|
|
164
|
+
{selectedOrder.items?.map((item: any, index: number) => {
|
|
165
|
+
// Debug: Log the full item structure
|
|
166
|
+
console.log('Order item structure:', JSON.stringify(item, null, 2));
|
|
167
|
+
|
|
168
|
+
// CartItemPopulated structure:
|
|
169
|
+
// - productVariantId: string (the ID we need)
|
|
170
|
+
// - productVariantData: ProductVariant object
|
|
171
|
+
// - quantity: number
|
|
172
|
+
const variantData = item.productVariantData || item.variant;
|
|
173
|
+
const variantId = item.productVariantId || item._id || item.id;
|
|
174
|
+
|
|
175
|
+
// Extract images from media array (ProductVariant uses 'media' not 'images')
|
|
176
|
+
const variantImages = variantData?.media?.map((m: any) => m.url || m.src || m) || variantData?.images || [];
|
|
177
|
+
const firstImage = variantImages[0];
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<div
|
|
181
|
+
key={`${variantId}-${index}`}
|
|
182
|
+
className="bg-white rounded-lg border border-gray-200 p-4 cursor-pointer hover:border-[#E67E50] hover:shadow-md transition-all"
|
|
183
|
+
onClick={() => {
|
|
184
|
+
// Try to extract productId from various possible locations
|
|
185
|
+
const possibleProductId =
|
|
186
|
+
item.productId ||
|
|
187
|
+
item.product?._id ||
|
|
188
|
+
item.product?.id ||
|
|
189
|
+
variantData?.productId ||
|
|
190
|
+
variantData?.product?._id ||
|
|
191
|
+
variantData?.product?.id ||
|
|
192
|
+
variantId; // Fallback to variantId if no productId found
|
|
193
|
+
|
|
194
|
+
const productData = {
|
|
195
|
+
productId: possibleProductId,
|
|
196
|
+
productName: variantData?.name || item.product?.name || 'Product',
|
|
197
|
+
productVariantId: variantId,
|
|
198
|
+
variantName: variantData?.name || 'Default',
|
|
199
|
+
variantImage: firstImage,
|
|
200
|
+
};
|
|
201
|
+
console.log('Extracted product data:', productData);
|
|
202
|
+
handleProductClick(productData);
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<div className="flex items-center gap-4">
|
|
206
|
+
{firstImage && (
|
|
207
|
+
<div className="relative w-16 h-16 rounded-lg overflow-hidden bg-gray-100 flex-shrink-0">
|
|
208
|
+
<Image
|
|
209
|
+
src={firstImage}
|
|
210
|
+
alt={variantData?.name || 'Product'}
|
|
211
|
+
fill
|
|
212
|
+
className="object-cover"
|
|
213
|
+
/>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
<div className="flex-1">
|
|
217
|
+
<h3 className="font-medium text-gray-900">{variantData?.name || 'Product'}</h3>
|
|
218
|
+
<p className="text-sm text-gray-600">{variantData?.description || ''}</p>
|
|
219
|
+
<p className="text-xs text-gray-500 mt-1">Quantity: {item.quantity}</p>
|
|
220
|
+
</div>
|
|
221
|
+
<div className="flex items-center gap-2 text-[#E67E50]">
|
|
222
|
+
<span className="text-sm font-medium">Review Now</span>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
);
|
|
227
|
+
})}
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Show completed orders list
|
|
235
|
+
const completedOrders = orders.filter(order =>
|
|
236
|
+
order.orderStatus && ['Delivered', 'Picked Up', 'Fulfilled'].includes(order.orderStatus)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="container mx-auto px-4 py-8">
|
|
241
|
+
<div className="max-w-4xl mx-auto">
|
|
242
|
+
<div className="mb-8">
|
|
243
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-2">Review Your Orders</h1>
|
|
244
|
+
<p className="text-gray-600">
|
|
245
|
+
Share your experience with products you've purchased
|
|
246
|
+
</p>
|
|
247
|
+
<p className="text-xs text-gray-500 mt-2">
|
|
248
|
+
You can only review orders that have been delivered or completed. Reviews help other customers make informed decisions!
|
|
249
|
+
</p>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
{completedOrders.length === 0 ? (
|
|
253
|
+
<div className="text-center py-12 bg-white rounded-lg border border-gray-200">
|
|
254
|
+
<Package className="size-16 text-gray-300 mx-auto mb-4" />
|
|
255
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
256
|
+
No completed orders
|
|
257
|
+
</h3>
|
|
258
|
+
<p className="text-gray-600 mb-6">
|
|
259
|
+
Complete an order to leave a review
|
|
260
|
+
</p>
|
|
261
|
+
<button
|
|
262
|
+
onClick={() => router.push('/shop')}
|
|
263
|
+
className="px-6 py-3 bg-[#E67E50] text-white rounded-lg hover:bg-[#d66f40] transition-colors"
|
|
264
|
+
>
|
|
265
|
+
Start Shopping
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
) : (
|
|
269
|
+
<div className="space-y-4">
|
|
270
|
+
{completedOrders.map((order) => (
|
|
271
|
+
<div
|
|
272
|
+
key={order.id || order._id}
|
|
273
|
+
className="bg-white rounded-lg border border-gray-200 p-6 cursor-pointer hover:border-[#E67E50] hover:shadow-md transition-all"
|
|
274
|
+
onClick={() => handleOrderClick(order)}
|
|
275
|
+
>
|
|
276
|
+
<div className="flex items-center justify-between">
|
|
277
|
+
<div className="flex-1">
|
|
278
|
+
<div className="flex items-center gap-3 mb-2">
|
|
279
|
+
<h3 className="font-semibold text-lg text-gray-900">
|
|
280
|
+
Order #{(order.id || order._id || '').slice(-8)}
|
|
281
|
+
</h3>
|
|
282
|
+
<span className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded">
|
|
283
|
+
{order.orderStatus}
|
|
284
|
+
</span>
|
|
285
|
+
</div>
|
|
286
|
+
<p className="text-sm text-gray-600 mb-1">
|
|
287
|
+
{order.items?.length || 0} {order.items?.length === 1 ? 'item' : 'items'}
|
|
288
|
+
</p>
|
|
289
|
+
{order.createdAt && (
|
|
290
|
+
<p className="text-xs text-gray-500">
|
|
291
|
+
{formatDistanceToNow(new Date(order.createdAt), { addSuffix: true })}
|
|
292
|
+
</p>
|
|
293
|
+
)}
|
|
294
|
+
</div>
|
|
295
|
+
<div className="text-right">
|
|
296
|
+
<div className="flex items-center gap-2 text-[#E67E50]">
|
|
297
|
+
<span className="text-sm font-medium">Review Products</span>
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
))}
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
</div>
|
|
306
|
+
</div>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
@@ -12,6 +12,7 @@ import { FilterChips } from '@/components/FilterChips';
|
|
|
12
12
|
import { useRouter } from 'next/navigation';
|
|
13
13
|
import { ManualOrderDTOOrderStatusEnum, PaymentPaymentStatusEnum } from '@/lib/Apis';
|
|
14
14
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
15
|
+
import { ReviewPromptBanner } from '@/components/ReviewPromptBanner';
|
|
15
16
|
|
|
16
17
|
const STATUS_FILTERS = ['All', ...Object.values(ManualOrderDTOOrderStatusEnum)];
|
|
17
18
|
const PAYMENT_FILTERS = ['All', ...Object.values(PaymentPaymentStatusEnum)];
|
|
@@ -24,7 +25,7 @@ export function OrdersScreen() {
|
|
|
24
25
|
const [page, setPage] = useState(1);
|
|
25
26
|
const [selectedFilter, setSelectedFilter] = useState<StatusFilterType>('All');
|
|
26
27
|
const [selectedPaymentFilter, setSelectedPaymentFilter] = useState<PaymentFilterType>('All');
|
|
27
|
-
const { orders, isLoading, pagination } = useOrders(
|
|
28
|
+
const { orders, isLoading, pagination, deleteOrder } = useOrders(
|
|
28
29
|
page,
|
|
29
30
|
10,
|
|
30
31
|
selectedFilter,
|
|
@@ -44,24 +45,41 @@ export function OrdersScreen() {
|
|
|
44
45
|
});
|
|
45
46
|
}, [orders, selectedFilter, selectedPaymentFilter]);
|
|
46
47
|
|
|
48
|
+
// Find the first completed order that hasn't been dismissed
|
|
49
|
+
const completedOrderForPrompt = useMemo(() => {
|
|
50
|
+
return filteredOrders.find(order =>
|
|
51
|
+
order.orderStatus && ['Delivered', 'Picked Up', 'Fulfilled'].includes(order.orderStatus)
|
|
52
|
+
);
|
|
53
|
+
}, [filteredOrders]);
|
|
54
|
+
|
|
47
55
|
const hasOrders = filteredOrders.length > 0;
|
|
48
56
|
const MAX_VISIBLE_FILTERS = 4;
|
|
49
57
|
|
|
50
58
|
return (
|
|
51
|
-
<div className="min-h-screen bg-
|
|
59
|
+
<div className="min-h-screen bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB]">
|
|
52
60
|
<div className="container mx-auto px-4 py-8 max-w-6xl">
|
|
53
61
|
<motion.div
|
|
54
62
|
initial={{ opacity: 0, y: 24 }}
|
|
55
63
|
animate={{ opacity: 1, y: 0 }}
|
|
56
64
|
className="space-y-6"
|
|
57
65
|
>
|
|
58
|
-
<div className="mb-
|
|
59
|
-
<h1 className="text-
|
|
60
|
-
<p className="text-sm text-
|
|
61
|
-
|
|
66
|
+
<div className="mb-8">
|
|
67
|
+
<h1 className="text-3xl font-black text-secondary tracking-tight">Order History</h1>
|
|
68
|
+
<p className="text-sm font-medium text-muted mt-1 flex items-center gap-2">
|
|
69
|
+
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
|
70
|
+
{filteredOrders.length} {filteredOrders.length === 1 ? 'record found' : 'records found'} for your account
|
|
62
71
|
</p>
|
|
63
72
|
</div>
|
|
64
73
|
|
|
74
|
+
{/* Review Prompt Banner */}
|
|
75
|
+
{!isLoading && completedOrderForPrompt && (
|
|
76
|
+
<ReviewPromptBanner
|
|
77
|
+
orderId={completedOrderForPrompt.id || completedOrderForPrompt._id || ''}
|
|
78
|
+
orderNumber={(completedOrderForPrompt.id || completedOrderForPrompt._id || '').slice(-8)}
|
|
79
|
+
itemCount={completedOrderForPrompt.items?.length || 0}
|
|
80
|
+
/>
|
|
81
|
+
)}
|
|
82
|
+
|
|
65
83
|
{/* Filters */}
|
|
66
84
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
67
85
|
<FilterChips
|
|
@@ -96,7 +114,13 @@ export function OrdersScreen() {
|
|
|
96
114
|
{isLoading ? (
|
|
97
115
|
Array.from({ length: 3 }).map((_, index) => <OrderCardSkeleton key={index} />)
|
|
98
116
|
) : hasOrders ? (
|
|
99
|
-
filteredOrders.map((order) =>
|
|
117
|
+
filteredOrders.map((order) => (
|
|
118
|
+
<OrderCard
|
|
119
|
+
key={order.id || order._id}
|
|
120
|
+
order={order}
|
|
121
|
+
onDelete={deleteOrder}
|
|
122
|
+
/>
|
|
123
|
+
))
|
|
100
124
|
) : (
|
|
101
125
|
<EmptyState
|
|
102
126
|
icon={Package}
|
|
@@ -36,6 +36,8 @@ import { useWishlist } from '@/providers/WishlistProvider';
|
|
|
36
36
|
import { ProductsApi, ProductVariant, ProductVariantInventoryStatusEnum } from '@/lib/Apis';
|
|
37
37
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
38
38
|
import { Category } from '@/lib/types';
|
|
39
|
+
import { useProductReviews } from '@/hooks/useReviews';
|
|
40
|
+
import { ProductReviewsSection } from '@/components/ProductReviewsSection';
|
|
39
41
|
|
|
40
42
|
const safeFormatDate = (date?: Date | string, format: 'long' | 'short' = 'long'): string => {
|
|
41
43
|
if (!date) return 'N/A';
|
|
@@ -60,6 +62,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
60
62
|
const { addToCart } = useCart();
|
|
61
63
|
const { isAuthenticated } = useAuth();
|
|
62
64
|
const notification = useNotification();
|
|
65
|
+
const { reviews, isLoading: reviewsLoading } = useProductReviews(productId);
|
|
63
66
|
|
|
64
67
|
// State declarations
|
|
65
68
|
const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
|
|
@@ -97,6 +100,24 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
97
100
|
return productData;
|
|
98
101
|
}, [productData, selectedVariant, initialProductData]);
|
|
99
102
|
|
|
103
|
+
// Calculate average rating and review count from actual reviews
|
|
104
|
+
const reviewStats = useMemo(() => {
|
|
105
|
+
if (!reviews || reviews.length === 0) {
|
|
106
|
+
return {
|
|
107
|
+
averageRating: product?.rating || 0,
|
|
108
|
+
reviewCount: 0,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const totalRating = reviews.reduce((sum, review) => sum + (review.rating || 0), 0);
|
|
113
|
+
const averageRating = totalRating / reviews.length;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
averageRating: Math.round(averageRating * 10) / 10, // Round to 1 decimal place
|
|
117
|
+
reviewCount: reviews.length,
|
|
118
|
+
};
|
|
119
|
+
}, [reviews, product?.rating]);
|
|
120
|
+
|
|
100
121
|
const getVariantImages = () => {
|
|
101
122
|
if (selectedVariant?.media?.length) {
|
|
102
123
|
return selectedVariant.media.map((media: any) => ({
|
|
@@ -450,7 +471,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
450
471
|
{[...Array(5)].map((_, i) => (
|
|
451
472
|
<Star
|
|
452
473
|
key={i}
|
|
453
|
-
className={`size-4 ${i < Math.floor(
|
|
474
|
+
className={`size-4 ${i < Math.floor(reviewStats.averageRating)
|
|
454
475
|
? 'text-[#E67E50] fill-[#E67E50]'
|
|
455
476
|
: 'text-gray-300'
|
|
456
477
|
}`}
|
|
@@ -458,7 +479,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
458
479
|
))}
|
|
459
480
|
</div>
|
|
460
481
|
<span className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
461
|
-
{
|
|
482
|
+
{reviewStats.averageRating} ({reviewStats.reviewCount} {reviewStats.reviewCount === 1 ? 'review' : 'reviews'})
|
|
462
483
|
</span>
|
|
463
484
|
</div>
|
|
464
485
|
{selectedVariant && (
|
|
@@ -754,15 +775,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
754
775
|
)}
|
|
755
776
|
|
|
756
777
|
{activeTab === 'reviews' && (
|
|
757
|
-
<
|
|
758
|
-
<Star className="size-12 text-[#E67E50] mx-auto mb-4" />
|
|
759
|
-
<h3 className="font-['Poppins',sans-serif] font-semibold text-secondary mb-2">
|
|
760
|
-
{product.rating} out of 5 stars
|
|
761
|
-
</h3>
|
|
762
|
-
<p className="font-['Poppins',sans-serif] text-[14px] text-muted">
|
|
763
|
-
Based on {product.reviewCount} customer reviews
|
|
764
|
-
</p>
|
|
765
|
-
</div>
|
|
778
|
+
<ProductReviewsSection productId={productId} />
|
|
766
779
|
)}
|
|
767
780
|
</div>
|
|
768
781
|
</div>
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Settings,
|
|
12
12
|
LogOut,
|
|
13
13
|
ChevronDown,
|
|
14
|
+
Star,
|
|
14
15
|
} from 'lucide-react';
|
|
15
16
|
import { useAuth } from '@/providers/AuthProvider';
|
|
16
17
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
@@ -22,10 +23,12 @@ import { AccountSavedItemsTab } from '@/components/AccountSavedItemsTab';
|
|
|
22
23
|
import { AccountPaymentTab } from '@/components/AccountPaymentTab';
|
|
23
24
|
import { AccountAddressesTab } from '@/components/AccountAddressesTab';
|
|
24
25
|
import { AccountSettingsTab } from '@/components/AccountSettingsTab';
|
|
26
|
+
import { AccountReviewsTab } from '@/components/AccountReviewsTab';
|
|
25
27
|
|
|
26
28
|
const tabs = [
|
|
27
29
|
{ id: 'overview', label: 'Overview', icon: User },
|
|
28
30
|
{ id: 'orders', label: 'Orders', icon: Package },
|
|
31
|
+
{ id: 'reviews', label: 'My Reviews', icon: Star },
|
|
29
32
|
{ id: 'saved-items', label: 'Saved Items', icon: Heart },
|
|
30
33
|
// { id: 'payment', label: 'Payment', icon: CreditCard },
|
|
31
34
|
{ id: 'addresses', label: 'Addresses', icon: MapPin },
|
|
@@ -75,6 +78,8 @@ export default function AccountPage() {
|
|
|
75
78
|
return <AccountOverviewTab />;
|
|
76
79
|
case 'orders':
|
|
77
80
|
return <AccountOrdersTab />;
|
|
81
|
+
case 'reviews':
|
|
82
|
+
return <AccountReviewsTab />;
|
|
78
83
|
case 'saved-items':
|
|
79
84
|
return <AccountSavedItemsTab />;
|
|
80
85
|
// case 'payment':
|
package/src/styles/globals.css
CHANGED
package/styles/base.css
CHANGED
package/styles/globals.css
CHANGED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/* tslint:disable */
|
|
2
|
-
/* eslint-disable */
|
|
3
|
-
/**
|
|
4
|
-
* Hey Pharamcist API
|
|
5
|
-
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
|
|
6
|
-
*
|
|
7
|
-
* OpenAPI spec version: 1.0
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* NOTE: This class is auto generated by the swagger code generator program.
|
|
11
|
-
* https://github.com/swagger-api/swagger-codegen.git
|
|
12
|
-
* Do not edit the class manually.
|
|
13
|
-
*/
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
* @export
|
|
17
|
-
* @interface CreateNotificationDto
|
|
18
|
-
*/
|
|
19
|
-
export interface CreateNotificationDto {
|
|
20
|
-
_id?: string;
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
* @type {string}
|
|
24
|
-
* @memberof CreateNotificationDto
|
|
25
|
-
*/
|
|
26
|
-
title: string;
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
* @type {string}
|
|
30
|
-
* @memberof CreateNotificationDto
|
|
31
|
-
*/
|
|
32
|
-
content: string;
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
35
|
-
* @type {string}
|
|
36
|
-
* @memberof CreateNotificationDto
|
|
37
|
-
*/
|
|
38
|
-
notificationType: string;
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
* @type {string}
|
|
42
|
-
* @memberof CreateNotificationDto
|
|
43
|
-
*/
|
|
44
|
-
notificationLink: string;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
* @type {Array<string>}
|
|
48
|
-
* @memberof CreateNotificationDto
|
|
49
|
-
*/
|
|
50
|
-
sentTo: Array<string>;
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
* @type {Array<string>}
|
|
54
|
-
* @memberof CreateNotificationDto
|
|
55
|
-
*/
|
|
56
|
-
readBy: Array<string>;
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
* @type {Array<string>}
|
|
60
|
-
* @memberof CreateNotificationDto
|
|
61
|
-
*/
|
|
62
|
-
clearedBy: Array<string>;
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* @type {boolean}
|
|
66
|
-
* @memberof CreateNotificationDto
|
|
67
|
-
*/
|
|
68
|
-
isAdminNotification: boolean;
|
|
69
|
-
/**
|
|
70
|
-
*
|
|
71
|
-
* @type {string}
|
|
72
|
-
* @memberof CreateNotificationDto
|
|
73
|
-
*/
|
|
74
|
-
storeId: string;
|
|
75
|
-
}
|