hey-pharmacist-ecommerce 1.1.29 → 1.1.31

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 (104) hide show
  1. package/dist/index.d.mts +10957 -1331
  2. package/dist/index.d.ts +10957 -1331
  3. package/dist/index.js +12364 -5144
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +9353 -2205
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +4 -3
  8. package/src/components/AccountReviewsTab.tsx +97 -0
  9. package/src/components/CouponCodeInput.tsx +190 -0
  10. package/src/components/Header.tsx +5 -1
  11. package/src/components/Notification.tsx +1 -1
  12. package/src/components/NotificationBell.tsx +33 -0
  13. package/src/components/NotificationCard.tsx +211 -0
  14. package/src/components/NotificationDrawer.tsx +195 -0
  15. package/src/components/OrderCard.tsx +164 -99
  16. package/src/components/ProductReviewsSection.tsx +30 -0
  17. package/src/components/RatingDistribution.tsx +86 -0
  18. package/src/components/ReviewCard.tsx +59 -0
  19. package/src/components/ReviewForm.tsx +207 -0
  20. package/src/components/ReviewPromptBanner.tsx +98 -0
  21. package/src/components/ReviewsList.tsx +151 -0
  22. package/src/components/StarRating.tsx +98 -0
  23. package/src/hooks/useDiscounts.ts +7 -0
  24. package/src/hooks/useOrders.ts +15 -0
  25. package/src/hooks/useReviews.ts +230 -0
  26. package/src/hooks/useStoreCapabilities.ts +87 -0
  27. package/src/index.ts +29 -0
  28. package/src/lib/Apis/apis/auth-api.ts +19 -7
  29. package/src/lib/Apis/apis/categories-api.ts +97 -0
  30. package/src/lib/Apis/apis/discounts-api.ts +23 -72
  31. package/src/lib/Apis/apis/notifications-api.ts +196 -231
  32. package/src/lib/Apis/apis/products-api.ts +181 -0
  33. package/src/lib/Apis/apis/review-api.ts +283 -4
  34. package/src/lib/Apis/apis/shipping-api.ts +105 -0
  35. package/src/lib/Apis/apis/stores-api.ts +536 -0
  36. package/src/lib/Apis/apis/sub-categories-api.ts +97 -0
  37. package/src/lib/Apis/apis/users-api.ts +8 -8
  38. package/src/lib/Apis/models/address-created-request.ts +0 -12
  39. package/src/lib/Apis/models/address.ts +0 -12
  40. package/src/lib/Apis/models/api-key-info-dto.ts +49 -0
  41. package/src/lib/Apis/models/bulk-channel-toggle-dto.ts +52 -0
  42. package/src/lib/Apis/models/cart-body-populated.ts +3 -3
  43. package/src/lib/Apis/models/channel-settings-dto.ts +39 -0
  44. package/src/lib/Apis/models/{discount-paginated-response.ts → completed-order-dto.ts} +21 -16
  45. package/src/lib/Apis/models/create-address-dto.ts +0 -12
  46. package/src/lib/Apis/models/create-discount-dto.ts +31 -100
  47. package/src/lib/Apis/models/create-review-dto.ts +4 -4
  48. package/src/lib/Apis/models/create-shippo-account-dto.ts +45 -0
  49. package/src/lib/Apis/models/create-store-address-dto.ts +0 -12
  50. package/src/lib/Apis/models/create-store-dto-settings.ts +51 -0
  51. package/src/lib/Apis/models/create-store-dto.ts +13 -0
  52. package/src/lib/Apis/models/create-variant-dto.ts +0 -6
  53. package/src/lib/Apis/models/discount.ts +37 -106
  54. package/src/lib/Apis/models/discounts-insights-dto.ts +12 -0
  55. package/src/lib/Apis/models/index.ts +24 -7
  56. package/src/lib/Apis/models/{manual-discount.ts → manual-discount-dto.ts} +10 -10
  57. package/src/lib/Apis/models/manual-order-dto.ts +3 -3
  58. package/src/lib/Apis/models/populated-discount.ts +41 -109
  59. package/src/lib/Apis/models/preference-update-item.ts +59 -0
  60. package/src/lib/Apis/models/product-light-dto.ts +40 -0
  61. package/src/lib/Apis/models/product-variant.ts +0 -6
  62. package/src/lib/Apis/models/reorder-categories-dto.ts +27 -0
  63. package/src/lib/Apis/models/reorder-products-dto.ts +49 -0
  64. package/src/lib/Apis/models/{check-notifications-response-dto.ts → reorder-products-success-response-dto.ts} +7 -7
  65. package/src/lib/Apis/models/reorder-subcategories-dto.ts +33 -0
  66. package/src/lib/Apis/models/reorder-success-response-dto.ts +33 -0
  67. package/src/lib/Apis/models/review-status-dto.ts +34 -0
  68. package/src/lib/Apis/models/review.ts +9 -3
  69. package/src/lib/Apis/models/reviewable-order-dto.ts +58 -0
  70. package/src/lib/Apis/models/reviewable-product-dto.ts +81 -0
  71. package/src/lib/Apis/models/shipment-with-order.ts +18 -0
  72. package/src/lib/Apis/models/shipment.ts +18 -0
  73. package/src/lib/Apis/models/shippo-account-response-dto.ts +51 -0
  74. package/src/lib/Apis/models/store-api-keys-response-dto.ts +34 -0
  75. package/src/lib/Apis/models/store-capabilities-dto.ts +63 -0
  76. package/src/lib/Apis/models/store-entity.ts +13 -0
  77. package/src/lib/Apis/models/store.ts +13 -0
  78. package/src/lib/Apis/models/update-address-dto.ts +0 -12
  79. package/src/lib/Apis/models/update-api-keys-dto.ts +39 -0
  80. package/src/lib/Apis/models/update-discount-dto.ts +31 -100
  81. package/src/lib/Apis/models/update-manual-shipment-status-dto.ts +47 -0
  82. package/src/lib/Apis/models/update-notification-settings-dto.ts +28 -0
  83. package/src/lib/Apis/models/update-review-dto.ts +4 -4
  84. package/src/lib/Apis/models/update-store-dto.ts +13 -0
  85. package/src/lib/Apis/models/update-variant-dto.ts +0 -6
  86. package/src/lib/Apis/models/{pick-type-class.ts → variant-light-dto.ts} +20 -14
  87. package/src/lib/utils/discount.ts +155 -0
  88. package/src/lib/validations/discount.ts +11 -0
  89. package/src/providers/CartProvider.tsx +2 -2
  90. package/src/providers/DiscountProvider.tsx +97 -0
  91. package/src/providers/EcommerceProvider.tsx +13 -5
  92. package/src/providers/NotificationCenterProvider.tsx +436 -0
  93. package/src/screens/CartScreen.tsx +1 -1
  94. package/src/screens/CheckoutScreen.tsx +402 -290
  95. package/src/screens/NotificationSettingsScreen.tsx +413 -0
  96. package/src/screens/OrderDetailScreen.tsx +283 -0
  97. package/src/screens/OrderReviewsScreen.tsx +308 -0
  98. package/src/screens/OrdersScreen.tsx +31 -7
  99. package/src/screens/ProductDetailScreen.tsx +24 -11
  100. package/src/screens/ProfileScreen.tsx +5 -0
  101. package/src/screens/ResetPasswordScreen.tsx +10 -4
  102. package/src/lib/Apis/models/create-notification-dto.ts +0 -75
  103. package/src/lib/Apis/models/notification.ts +0 -93
  104. package/src/lib/Apis/models/single-notification-dto.ts +0 -99
@@ -0,0 +1,195 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useRef } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { X, Settings, CheckCheck, BellOff } from 'lucide-react';
6
+ import { useNotificationCenter } from '@/providers/NotificationCenterProvider';
7
+ import { NotificationCard } from './NotificationCard';
8
+ import { useRouter } from 'next/navigation';
9
+ import { useBasePath } from '@/providers/BasePathProvider';
10
+
11
+ export function NotificationDrawer() {
12
+ const {
13
+ isDrawerOpen,
14
+ closeDrawer,
15
+ notifications,
16
+ unreadCount,
17
+ isLoading,
18
+ markAsRead,
19
+ markAllAsRead,
20
+ deleteNotification,
21
+ loadMore,
22
+ hasMore,
23
+ } = useNotificationCenter();
24
+
25
+ const router = useRouter();
26
+ const { buildPath } = useBasePath();
27
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
28
+
29
+ // Debug logging
30
+ console.log('NotificationDrawer render:', {
31
+ isDrawerOpen,
32
+ notificationsCount: notifications.length,
33
+ notifications: notifications.slice(0, 2), // First 2 for brevity
34
+ unreadCount,
35
+ isLoading,
36
+ });
37
+
38
+ // Handle scroll for infinite loading
39
+ const handleScroll = () => {
40
+ if (!scrollContainerRef.current || isLoading || !hasMore) return;
41
+
42
+ const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
43
+ const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
44
+
45
+ if (scrollPercentage > 0.8) {
46
+ loadMore();
47
+ }
48
+ };
49
+
50
+ // Close drawer on escape key
51
+ useEffect(() => {
52
+ const handleEscape = (e: KeyboardEvent) => {
53
+ if (e.key === 'Escape' && isDrawerOpen) {
54
+ closeDrawer();
55
+ }
56
+ };
57
+
58
+ document.addEventListener('keydown', handleEscape);
59
+ return () => document.removeEventListener('keydown', handleEscape);
60
+ }, [isDrawerOpen, closeDrawer]);
61
+
62
+ // Prevent body scroll when drawer is open
63
+ useEffect(() => {
64
+ if (isDrawerOpen) {
65
+ document.body.style.overflow = 'hidden';
66
+ } else {
67
+ document.body.style.overflow = '';
68
+ }
69
+
70
+ return () => {
71
+ document.body.style.overflow = '';
72
+ };
73
+ }, [isDrawerOpen]);
74
+
75
+ const handleSettingsClick = () => {
76
+ closeDrawer();
77
+ router.push(buildPath('/account/notifications'));
78
+ };
79
+
80
+ return (
81
+ <AnimatePresence>
82
+ {isDrawerOpen && (
83
+ <>
84
+ {/* Backdrop */}
85
+ <motion.div
86
+ initial={{ opacity: 0 }}
87
+ animate={{ opacity: 1 }}
88
+ exit={{ opacity: 0 }}
89
+ className="fixed inset-0 bg-black/50 z-40"
90
+ onClick={closeDrawer}
91
+ />
92
+
93
+ {/* Drawer */}
94
+ <motion.div
95
+ initial={{ x: '100%' }}
96
+ animate={{ x: 0 }}
97
+ exit={{ x: '100%' }}
98
+ transition={{ type: 'spring', damping: 25, stiffness: 200 }}
99
+ className="fixed right-0 top-0 bottom-0 w-full sm:w-[480px] bg-white shadow-2xl z-50 flex flex-col"
100
+ >
101
+ {/* Header */}
102
+ <div className="flex items-center justify-between p-4 border-b border-gray-200 bg-white">
103
+ <div className="flex items-center gap-3">
104
+ <h2 className="text-xl font-bold text-gray-900">Notifications</h2>
105
+ {unreadCount > 0 && (
106
+ <span className="bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
107
+ {unreadCount}
108
+ </span>
109
+ )}
110
+ </div>
111
+
112
+ <div className="flex items-center gap-2">
113
+ {/* Mark all as read */}
114
+ {unreadCount > 0 && (
115
+ <button
116
+ onClick={markAllAsRead}
117
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
118
+ title="Mark all as read"
119
+ >
120
+ <CheckCheck className="w-5 h-5 text-gray-600" />
121
+ </button>
122
+ )}
123
+
124
+ {/* Settings */}
125
+ <button
126
+ onClick={handleSettingsClick}
127
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
128
+ title="Notification settings"
129
+ >
130
+ <Settings className="w-5 h-5 text-gray-600" />
131
+ </button>
132
+
133
+ {/* Close */}
134
+ <button
135
+ onClick={closeDrawer}
136
+ className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
137
+ aria-label="Close"
138
+ >
139
+ <X className="w-5 h-5 text-gray-600" />
140
+ </button>
141
+ </div>
142
+ </div>
143
+
144
+ {/* Notification List */}
145
+ <div
146
+ ref={scrollContainerRef}
147
+ onScroll={handleScroll}
148
+ className="flex-1 overflow-y-auto p-4 space-y-3"
149
+ >
150
+ {notifications.length === 0 && !isLoading ? (
151
+ // Empty state
152
+ <div className="flex flex-col items-center justify-center h-full text-center py-12">
153
+ <div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
154
+ <BellOff className="w-10 h-10 text-gray-400" />
155
+ </div>
156
+ <h3 className="text-lg font-semibold text-gray-900 mb-2">
157
+ No notifications yet
158
+ </h3>
159
+ <p className="text-sm text-gray-500 max-w-xs">
160
+ When you receive notifications, they'll appear here
161
+ </p>
162
+ </div>
163
+ ) : (
164
+ <>
165
+ {notifications.map((notification) => (
166
+ <NotificationCard
167
+ key={notification._id}
168
+ notification={notification}
169
+ onMarkAsRead={markAsRead}
170
+ onDelete={deleteNotification}
171
+ />
172
+ ))}
173
+
174
+ {/* Loading indicator */}
175
+ {isLoading && (
176
+ <div className="flex justify-center py-4">
177
+ <div className="w-6 h-6 border-2 border-primary-600 border-t-transparent rounded-full animate-spin" />
178
+ </div>
179
+ )}
180
+
181
+ {/* End of list indicator */}
182
+ {!hasMore && notifications.length > 0 && (
183
+ <div className="text-center py-4 text-sm text-gray-500">
184
+ You're all caught up! 🎉
185
+ </div>
186
+ )}
187
+ </>
188
+ )}
189
+ </div>
190
+ </motion.div>
191
+ </>
192
+ )}
193
+ </AnimatePresence>
194
+ );
195
+ }
@@ -1,126 +1,191 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
4
- import { motion } from 'framer-motion';
5
- import { CreditCard } from 'lucide-react';
3
+ import React, { useRef, useState } from 'react';
4
+ import { motion, useMotionValue, useTransform } from 'framer-motion';
5
+ import { CreditCard, Star, Trash2 } from 'lucide-react';
6
6
  import { PaymentPaymentMethodEnum, PaymentPaymentStatusEnum, PopulatedOrder } from '@/lib/Apis';
7
7
  import { formatPrice, formatDate } from '@/lib/utils/format';
8
8
  import { Badge } from './ui/Badge';
9
9
  import Image from 'next/image';
10
+ import { useRouter } from 'next/navigation';
11
+ import { useBasePath } from '@/providers/BasePathProvider';
10
12
 
11
13
  interface OrderCardProps {
12
14
  order: PopulatedOrder;
15
+ onDelete?: (id: string) => void;
13
16
  }
14
17
 
15
- export function OrderCard({ order }: OrderCardProps) {
18
+ export function OrderCard({ order, onDelete }: OrderCardProps) {
19
+ const router = useRouter();
20
+ const { buildPath } = useBasePath();
21
+ const [isDragging, setIsDragging] = useState(false);
22
+ const dragX = useMotionValue(0);
23
+
24
+ // Create an opacity transformation based on the drag distance
25
+ const deleteOpacity = useTransform(dragX, [-100, -20], [1, 0]);
26
+
16
27
  const config = order.orderStatus;
17
28
  const itemCount = order.items?.length || 0;
18
29
  const showPriceBreakdown = (order.shippingCost && order.shippingCost > 0) ||
19
30
  (order.tax && order.tax > 0) ||
20
31
  (order.discountedAmount && order.discountedAmount > 0);
21
32
 
33
+ const isCompletedOrder = order.orderStatus &&
34
+ ['Delivered', 'Picked Up', 'Fulfilled'].includes(order.orderStatus);
35
+
36
+ const handleReviewClick = (e: React.MouseEvent) => {
37
+ e.stopPropagation();
38
+ router.push(buildPath('/reviews'));
39
+ };
40
+
41
+ const handleCardClick = () => {
42
+ if (isDragging) return;
43
+ router.push(buildPath(`/account/orders/${order._id || order.id}`));
44
+ };
45
+
22
46
  return (
23
- <motion.div
24
- initial={{ opacity: 0, y: 20 }}
25
- animate={{ opacity: 1, y: 0 }}
26
- className="rounded-lg border border-slate-200 bg-white p-4 shadow-xs hover:shadow-md transition-shadow"
27
- >
28
- {/* Header - Compact */}
29
- <div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
30
- <div className="flex items-center gap-3">
31
- <h3 className="text-base font-bold text-slate-900">
32
- Order #{order?._id?.slice(0, 8) || ''}
33
- </h3>
34
- <Badge variant={config as 'success' | 'warning' | 'primary' | 'danger' | 'gray'}>{config}</Badge>
35
- <span className="text-xs text-gray-500">
36
- {formatDate(order.createdAt || new Date(), 'short')}
37
- </span>
47
+ <div className="relative overflow-hidden rounded-lg bg-slate-100">
48
+ {/* Delete Background Layer */}
49
+ <motion.div
50
+ style={{ opacity: deleteOpacity }}
51
+ className="absolute inset-0 bg-red-600 flex items-center justify-end px-8"
52
+ >
53
+ <div className="flex flex-col items-center gap-1 text-white">
54
+ <Trash2 className="w-6 h-6" />
55
+ <span className="text-[10px] font-bold uppercase tracking-wider">Delete</span>
38
56
  </div>
39
- <div className="text-right">
40
- <p className="text-lg font-bold text-slate-900">{formatPrice(order.grandTotal || 0)}</p>
41
- {itemCount > 0 && (
42
- <p className="text-xs text-gray-500">{itemCount} {itemCount === 1 ? 'item' : 'items'}</p>
43
- )}
57
+ </motion.div>
58
+
59
+ {/* Swipeable Foreground Card */}
60
+ <motion.div
61
+ drag="x"
62
+ dragDirectionLock
63
+ dragConstraints={{ left: -100, right: 0 }}
64
+ dragElastic={0.05}
65
+ onDragStart={() => setIsDragging(true)}
66
+ onDragEnd={(_, info) => {
67
+ // Reset dragging state after a short delay to prevent accidental click
68
+ setTimeout(() => setIsDragging(false), 50);
69
+
70
+ if (info.offset.x < -70 && onDelete && (order._id || order.id)) {
71
+ onDelete((order._id || order.id) as string);
72
+ }
73
+ }}
74
+ style={{ x: dragX }}
75
+ whileTap={{ cursor: 'grabbing' }}
76
+ className="relative z-10 bg-white border border-slate-200 p-4 shadow-xs hover:shadow-md transition-shadow cursor-pointer touch-pan-y"
77
+ onClick={handleCardClick}
78
+ >
79
+ {/* Header - Compact */}
80
+ <div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
81
+ <div className="flex items-center gap-3">
82
+ <h3 className="text-base font-bold text-slate-900 group-hover:text-primary transition-colors">
83
+ Order #{(order._id || order.id || '').slice(-8).toUpperCase()}
84
+ </h3>
85
+ <Badge variant={config as any}>{config}</Badge>
86
+ <span className="text-xs text-gray-500">
87
+ {formatDate(order.createdAt || new Date(), 'short')}
88
+ </span>
89
+ </div>
90
+ <div className="text-right">
91
+ <p className="text-lg font-bold text-slate-900">{formatPrice(order.grandTotal || 0)}</p>
92
+ {itemCount > 0 && (
93
+ <p className="text-xs text-gray-500">{itemCount} {itemCount === 1 ? 'item' : 'items'}</p>
94
+ )}
95
+ </div>
44
96
  </div>
45
- </div>
46
-
47
- {/* Items List - Compact */}
48
- <div className="space-y-2 mb-4">
49
- {order.items && order.items.length > 0 ? (
50
- order.items.slice(0, 3).map((item) => {
51
- const itemPrice = item.productVariantData?.finalPrice || 0;
52
- const itemTotal = itemPrice * item.quantity;
53
-
54
- return (
55
- <div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
56
- <div className="relative w-12 h-12 rounded-sm bg-gray-100 shrink-0 overflow-hidden">
57
- <Image
58
- src={item?.productVariantData?.media?.[0]?.file || '/placeholder-product.jpg'}
59
- alt={item?.productVariantData?.name || 'Product image'}
60
- fill
61
- className="object-cover"
62
- sizes="48px"
63
- />
64
- </div>
65
- <div className="flex-1 min-w-0">
66
- <p className="font-medium text-slate-900 truncate text-sm">
67
- {item.productVariantData?.name || 'Unknown Product'}
68
- </p>
69
- <p className="text-xs text-gray-500">Qty: {item.quantity}</p>
97
+
98
+ {/* Items List - Compact */}
99
+ <div className="space-y-2 mb-4">
100
+ {order.items && order.items.length > 0 ? (
101
+ order.items.slice(0, 3).map((item) => {
102
+ const itemPrice = item.productVariantData?.finalPrice || 0;
103
+ const itemTotal = itemPrice * item.quantity;
104
+
105
+ return (
106
+ <div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
107
+ <div className="relative w-12 h-12 rounded-sm bg-gray-100 shrink-0 overflow-hidden">
108
+ <Image
109
+ src={item?.productVariantData?.media?.[0]?.file || '/placeholder-product.jpg'}
110
+ alt={item?.productVariantData?.name || 'Product image'}
111
+ fill
112
+ className="object-cover"
113
+ sizes="48px"
114
+ />
115
+ </div>
116
+ <div className="flex-1 min-w-0">
117
+ <p className="font-medium text-slate-900 truncate text-sm">
118
+ {item.productVariantData?.name || 'Unknown Product'}
119
+ </p>
120
+ <p className="text-xs text-gray-500">Qty: {item.quantity}</p>
121
+ </div>
122
+ <p className="font-semibold text-slate-900 text-sm">{formatPrice(itemTotal)}</p>
70
123
  </div>
71
- <p className="font-semibold text-slate-900 text-sm">{formatPrice(itemTotal)}</p>
72
- </div>
73
- );
74
- })
75
- ) : (
76
- <p className="text-sm text-gray-500 text-center py-2">No items found</p>
77
- )}
78
- {order.items && order.items.length > 3 && (
79
- <p className="text-xs text-gray-500 text-center pt-1">
80
- +{order.items.length - 3} more {order.items.length - 3 === 1 ? 'item' : 'items'}
81
- </p>
82
- )}
83
- </div>
84
-
85
- {/* Price Breakdown - Only if needed */}
86
- {showPriceBreakdown && (
87
- <div className="mb-4 pb-4 border-b border-gray-200 space-y-1 text-xs">
88
- {order.shippingCost !== undefined && order.shippingCost > 0 && (
89
- <div className="flex justify-between text-gray-600">
90
- <span>Shipping</span>
91
- <span>{formatPrice(order.shippingCost)}</span>
92
- </div>
93
- )}
94
- {order.tax !== undefined && order.tax > 0 && (
95
- <div className="flex justify-between text-gray-600">
96
- <span>Tax</span>
97
- <span>{formatPrice(order.tax)}</span>
98
- </div>
124
+ );
125
+ })
126
+ ) : (
127
+ <p className="text-sm text-gray-500 text-center py-2">No items found</p>
99
128
  )}
100
- {order.discountedAmount !== undefined && order.discountedAmount > 0 && (
101
- <div className="flex justify-between text-green-600">
102
- <span>Discount</span>
103
- <span>-{formatPrice(order.discountedAmount)}</span>
104
- </div>
129
+ {order.items && order.items.length > 3 && (
130
+ <p className="text-xs text-gray-500 text-center pt-1">
131
+ +{order.items.length - 3} more {order.items.length - 3 === 1 ? 'item' : 'items'}
132
+ </p>
105
133
  )}
106
134
  </div>
107
- )}
108
-
109
- {/* Footer Actions */}
110
- {order.payment?.paymentStatus !== PaymentPaymentStatusEnum.Paid && order.payment?.paymentMethod === PaymentPaymentMethodEnum.Card && (
111
- <div className="flex justify-end">
112
- <button
113
- onClick={() => {
114
- window.open(order?.payment?.hostedInvoiceUrl || '', '_blank')
115
- }}
116
- className="inline-flex items-center gap-2 rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 text-sm font-medium transition-colors"
117
- >
118
- <CreditCard className="w-4 h-4" />
119
- Pay Now
120
- </button>
135
+
136
+ {/* Price Breakdown - Only if needed */}
137
+ {showPriceBreakdown && (
138
+ <div className="mb-4 pb-4 border-b border-gray-200 space-y-1 text-xs">
139
+ {order.shippingCost !== undefined && order.shippingCost > 0 && (
140
+ <div className="flex justify-between text-gray-600">
141
+ <span>Shipping</span>
142
+ <span>{formatPrice(order.shippingCost)}</span>
143
+ </div>
144
+ )}
145
+ {order.tax !== undefined && order.tax > 0 && (
146
+ <div className="flex justify-between text-gray-600">
147
+ <span>Tax</span>
148
+ <span>{formatPrice(order.tax)}</span>
149
+ </div>
150
+ )}
151
+ {order.discountedAmount !== undefined && order.discountedAmount > 0 && (
152
+ <div className="flex justify-between text-green-600">
153
+ <span>Discount</span>
154
+ <span>-{formatPrice(order.discountedAmount)}</span>
155
+ </div>
156
+ )}
157
+ </div>
158
+ )}
159
+
160
+ {/* Footer Actions */}
161
+ <div className="flex items-center justify-end gap-3 font-medium">
162
+ {order.payment?.paymentStatus !== PaymentPaymentStatusEnum.Paid &&
163
+ order.payment?.paymentMethod === PaymentPaymentMethodEnum.Card && (
164
+ <button
165
+ onClick={(e) => {
166
+ e.stopPropagation();
167
+ window.open(order?.payment?.hostedInvoiceUrl || '', '_blank');
168
+ }}
169
+ className="inline-flex items-center gap-2 rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 text-sm transition-colors"
170
+ onPointerDown={(e) => e.stopPropagation()}
171
+ >
172
+ <CreditCard className="w-4 h-4" />
173
+ Pay Now
174
+ </button>
175
+ )}
176
+
177
+ {isCompletedOrder && (
178
+ <button
179
+ onClick={handleReviewClick}
180
+ className="inline-flex items-center gap-2 rounded-lg border border-[#E67E50] bg-white hover:bg-[#E67E50]/5 text-[#E67E50] px-4 py-2 text-sm transition-colors"
181
+ onPointerDown={(e) => e.stopPropagation()}
182
+ >
183
+ <Star className="w-4 h-4" />
184
+ Write Review
185
+ </button>
186
+ )}
121
187
  </div>
122
- )}
123
- </motion.div>
188
+ </motion.div>
189
+ </div>
124
190
  );
125
191
  }
126
-
@@ -0,0 +1,30 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { useProductReviews } from '@/hooks/useReviews';
5
+ import { RatingDistribution } from './RatingDistribution';
6
+ import { ReviewsList } from './ReviewsList';
7
+
8
+ interface ProductReviewsSectionProps {
9
+ productId: string;
10
+ }
11
+
12
+ export function ProductReviewsSection({ productId }: ProductReviewsSectionProps) {
13
+ const { reviews, isLoading, error } = useProductReviews(productId);
14
+
15
+ if (error) {
16
+ return (
17
+ <div className="text-center py-12">
18
+ <p className="text-red-600 mb-2">Failed to load reviews</p>
19
+ <p className="text-sm text-gray-500">{error.message}</p>
20
+ </div>
21
+ );
22
+ }
23
+
24
+ return (
25
+ <div className="space-y-8">
26
+ <RatingDistribution reviews={reviews} />
27
+ <ReviewsList reviews={reviews} isLoading={isLoading} />
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Review } from '@/lib/Apis/models';
5
+ import { StarRating } from './StarRating';
6
+ import { Star } from 'lucide-react';
7
+
8
+ interface RatingDistributionProps {
9
+ reviews: Review[];
10
+ averageRating?: number;
11
+ }
12
+
13
+ export function RatingDistribution({ reviews, averageRating }: RatingDistributionProps) {
14
+ const totalReviews = reviews.length;
15
+
16
+ const ratingCounts = reviews.reduce(
17
+ (acc, review) => {
18
+ const rating = Math.floor(review.rating);
19
+ acc[rating] = (acc[rating] || 0) + 1;
20
+ return acc;
21
+ },
22
+ {} as Record<number, number>
23
+ );
24
+
25
+ const calculatedAverage =
26
+ averageRating ||
27
+ (totalReviews > 0
28
+ ? reviews.reduce((sum, r) => sum + r.rating, 0) / totalReviews
29
+ : 0);
30
+
31
+ const getRatingPercentage = (rating: number) => {
32
+ const count = ratingCounts[rating] || 0;
33
+ return totalReviews > 0 ? (count / totalReviews) * 100 : 0;
34
+ };
35
+
36
+ return (
37
+ <div className="bg-white rounded-lg border border-gray-200 p-6">
38
+ <div className="flex flex-col md:flex-row gap-8">
39
+ <div className="flex flex-col items-center justify-center border-r border-gray-200 pr-8">
40
+ <div className="text-5xl font-bold text-gray-900 mb-2">
41
+ {calculatedAverage.toFixed(1)}
42
+ </div>
43
+ <StarRating rating={calculatedAverage} size="md" showNumber={false} />
44
+ <p className="text-sm text-gray-600 mt-2">
45
+ Based on {totalReviews} {totalReviews === 1 ? 'review' : 'reviews'}
46
+ </p>
47
+ </div>
48
+
49
+ <div className="flex-1 space-y-3">
50
+ {[5, 4, 3, 2, 1].map((rating) => {
51
+ const percentage = getRatingPercentage(rating);
52
+ const count = ratingCounts[rating] || 0;
53
+
54
+ return (
55
+ <div key={rating} className="flex items-center gap-3">
56
+ <div className="flex items-center gap-1 w-16">
57
+ <span className="text-sm font-medium text-gray-700">{rating}</span>
58
+ <Star className="size-3 fill-[#E67E50] text-[#E67E50]" />
59
+ </div>
60
+
61
+ <div className="flex-1 h-2 bg-gray-200 rounded-full overflow-hidden">
62
+ <div
63
+ className="h-full bg-[#E67E50] transition-all duration-300"
64
+ style={{ width: `${percentage}%` }}
65
+ />
66
+ </div>
67
+
68
+ <span className="text-sm text-gray-600 w-12 text-right">
69
+ {count}
70
+ </span>
71
+ </div>
72
+ );
73
+ })}
74
+ </div>
75
+ </div>
76
+
77
+ {totalReviews === 0 && (
78
+ <div className="text-center py-8">
79
+ <Star className="size-12 text-gray-300 mx-auto mb-4" />
80
+ <p className="text-gray-600 font-medium mb-1">No reviews yet</p>
81
+ <p className="text-sm text-gray-500">Be the first to review this product!</p>
82
+ </div>
83
+ )}
84
+ </div>
85
+ );
86
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Review } from '@/lib/Apis/models';
5
+ import { StarRating } from './StarRating';
6
+ import { MessageCircle, User } from 'lucide-react';
7
+ import { formatDistanceToNow } from 'date-fns';
8
+
9
+ interface ReviewCardProps {
10
+ review: Review;
11
+ showProductInfo?: boolean;
12
+ }
13
+
14
+ export function ReviewCard({ review, showProductInfo = false }: ReviewCardProps) {
15
+ const reviewDate = new Date(review.createdAt);
16
+ const replyDate = review.replyDate ? new Date(review.replyDate) : null;
17
+
18
+ return (
19
+ <div className="border border-gray-200 rounded-lg p-4 bg-white">
20
+ <div className="flex items-start justify-between mb-3">
21
+ <div className="flex items-center gap-3">
22
+ <div className="w-10 h-10 bg-[#E67E50]/10 rounded-full flex items-center justify-center">
23
+ <User className="size-5 text-[#E67E50]" />
24
+ </div>
25
+ <div>
26
+ <p className="font-medium text-gray-900">Customer Review</p>
27
+ <p className="text-xs text-gray-500">
28
+ {formatDistanceToNow(reviewDate, { addSuffix: true })}
29
+ </p>
30
+ </div>
31
+ </div>
32
+ <StarRating rating={review.rating} size="sm" />
33
+ </div>
34
+
35
+ {review.reviewType && (
36
+ <span className="inline-block px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded mb-2">
37
+ {review.reviewType}
38
+ </span>
39
+ )}
40
+
41
+ <p className="text-gray-700 text-sm leading-relaxed mb-3">{review.review}</p>
42
+
43
+ {review.reply && (
44
+ <div className="mt-4 pl-4 border-l-2 border-[#E67E50] bg-gray-50 p-3 rounded">
45
+ <div className="flex items-center gap-2 mb-2">
46
+ <MessageCircle className="size-4 text-[#E67E50]" />
47
+ <p className="text-xs font-medium text-gray-900">Store Response</p>
48
+ {replyDate && (
49
+ <p className="text-xs text-gray-500">
50
+ {formatDistanceToNow(replyDate, { addSuffix: true })}
51
+ </p>
52
+ )}
53
+ </div>
54
+ <p className="text-sm text-gray-700">{review.reply}</p>
55
+ </div>
56
+ )}
57
+ </div>
58
+ );
59
+ }