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,188 @@
|
|
|
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
|
+
|
|
30
|
+
|
|
31
|
+
// Handle scroll for infinite loading
|
|
32
|
+
const handleScroll = () => {
|
|
33
|
+
if (!scrollContainerRef.current || isLoading || !hasMore) return;
|
|
34
|
+
|
|
35
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
|
|
36
|
+
const scrollPercentage = (scrollTop + clientHeight) / scrollHeight;
|
|
37
|
+
|
|
38
|
+
if (scrollPercentage > 0.8) {
|
|
39
|
+
loadMore();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Close drawer on escape key
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
46
|
+
if (e.key === 'Escape' && isDrawerOpen) {
|
|
47
|
+
closeDrawer();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
document.addEventListener('keydown', handleEscape);
|
|
52
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
53
|
+
}, [isDrawerOpen, closeDrawer]);
|
|
54
|
+
|
|
55
|
+
// Prevent body scroll when drawer is open
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (isDrawerOpen) {
|
|
58
|
+
document.body.style.overflow = 'hidden';
|
|
59
|
+
} else {
|
|
60
|
+
document.body.style.overflow = '';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return () => {
|
|
64
|
+
document.body.style.overflow = '';
|
|
65
|
+
};
|
|
66
|
+
}, [isDrawerOpen]);
|
|
67
|
+
|
|
68
|
+
const handleSettingsClick = () => {
|
|
69
|
+
closeDrawer();
|
|
70
|
+
router.push(buildPath('/account/notifications'));
|
|
71
|
+
};
|
|
72
|
+
console.log(notifications)
|
|
73
|
+
return (
|
|
74
|
+
<AnimatePresence>
|
|
75
|
+
{isDrawerOpen && (
|
|
76
|
+
<>
|
|
77
|
+
{/* Backdrop */}
|
|
78
|
+
<motion.div
|
|
79
|
+
initial={{ opacity: 0 }}
|
|
80
|
+
animate={{ opacity: 1 }}
|
|
81
|
+
exit={{ opacity: 0 }}
|
|
82
|
+
className="fixed inset-0 bg-black/50 z-40"
|
|
83
|
+
onClick={closeDrawer}
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
{/* Drawer */}
|
|
87
|
+
<motion.div
|
|
88
|
+
initial={{ x: '100%' }}
|
|
89
|
+
animate={{ x: 0 }}
|
|
90
|
+
exit={{ x: '100%' }}
|
|
91
|
+
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
92
|
+
className="fixed right-0 top-0 bottom-0 w-full sm:w-[480px] bg-white shadow-2xl z-50 flex flex-col"
|
|
93
|
+
>
|
|
94
|
+
{/* Header */}
|
|
95
|
+
<div className="flex items-center justify-between p-4 border-b border-gray-200 bg-white">
|
|
96
|
+
<div className="flex items-center gap-3">
|
|
97
|
+
<h2 className="text-xl font-bold text-gray-900">Notifications</h2>
|
|
98
|
+
{unreadCount > 0 && (
|
|
99
|
+
<span className="bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">
|
|
100
|
+
{unreadCount}
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div className="flex items-center gap-2">
|
|
106
|
+
{/* Mark all as read */}
|
|
107
|
+
{unreadCount > 0 && (
|
|
108
|
+
<button
|
|
109
|
+
onClick={markAllAsRead}
|
|
110
|
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
111
|
+
title="Mark all as read"
|
|
112
|
+
>
|
|
113
|
+
<CheckCheck className="w-5 h-5 text-gray-600" />
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{/* Settings */}
|
|
118
|
+
<button
|
|
119
|
+
onClick={handleSettingsClick}
|
|
120
|
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
121
|
+
title="Notification settings"
|
|
122
|
+
>
|
|
123
|
+
<Settings className="w-5 h-5 text-gray-600" />
|
|
124
|
+
</button>
|
|
125
|
+
|
|
126
|
+
{/* Close */}
|
|
127
|
+
<button
|
|
128
|
+
onClick={closeDrawer}
|
|
129
|
+
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
130
|
+
aria-label="Close"
|
|
131
|
+
>
|
|
132
|
+
<X className="w-5 h-5 text-gray-600" />
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
{/* Notification List */}
|
|
138
|
+
<div
|
|
139
|
+
ref={scrollContainerRef}
|
|
140
|
+
onScroll={handleScroll}
|
|
141
|
+
className="flex-1 overflow-y-auto p-4 space-y-3"
|
|
142
|
+
>
|
|
143
|
+
{notifications.length === 0 && !isLoading ? (
|
|
144
|
+
// Empty state
|
|
145
|
+
<div className="flex flex-col items-center justify-center h-full text-center py-12">
|
|
146
|
+
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
|
|
147
|
+
<BellOff className="w-10 h-10 text-gray-400" />
|
|
148
|
+
</div>
|
|
149
|
+
<h3 className="text-lg font-semibold text-gray-900 mb-2">
|
|
150
|
+
No notifications yet
|
|
151
|
+
</h3>
|
|
152
|
+
<p className="text-sm text-gray-500 max-w-xs">
|
|
153
|
+
When you receive notifications, they'll appear here
|
|
154
|
+
</p>
|
|
155
|
+
</div>
|
|
156
|
+
) : (
|
|
157
|
+
<>
|
|
158
|
+
{notifications.map((notification) => (
|
|
159
|
+
<NotificationCard
|
|
160
|
+
key={notification._id}
|
|
161
|
+
notification={notification}
|
|
162
|
+
onMarkAsRead={markAsRead}
|
|
163
|
+
onDelete={deleteNotification}
|
|
164
|
+
/>
|
|
165
|
+
))}
|
|
166
|
+
|
|
167
|
+
{/* Loading indicator */}
|
|
168
|
+
{isLoading && (
|
|
169
|
+
<div className="flex justify-center py-4">
|
|
170
|
+
<div className="w-6 h-6 border-2 border-primary-600 border-t-transparent rounded-full animate-spin" />
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* End of list indicator */}
|
|
175
|
+
{!hasMore && notifications.length > 0 && (
|
|
176
|
+
<div className="text-center py-4 text-sm text-gray-500">
|
|
177
|
+
You're all caught up! 🎉
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</>
|
|
181
|
+
)}
|
|
182
|
+
</div>
|
|
183
|
+
</motion.div>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
</AnimatePresence>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -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
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
<
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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.
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
</
|
|
188
|
+
</motion.div>
|
|
189
|
+
</div>
|
|
124
190
|
);
|
|
125
191
|
}
|
|
126
|
-
|
|
@@ -267,7 +267,7 @@ export function ProductCard({
|
|
|
267
267
|
)}
|
|
268
268
|
</div>
|
|
269
269
|
{/* Rating */}
|
|
270
|
-
<div className="flex items-center gap-1.5 ">
|
|
270
|
+
<div className="flex items-center gap-1.5 my-2">
|
|
271
271
|
<div className="flex items-center gap-0.5">
|
|
272
272
|
{[...Array(5)].map((_, i) => (
|
|
273
273
|
<Star
|
|
@@ -279,7 +279,7 @@ export function ProductCard({
|
|
|
279
279
|
/>
|
|
280
280
|
))}
|
|
281
281
|
</div>
|
|
282
|
-
<span className="font-['Poppins',sans-serif] text-[10px] text-[#676c80]
|
|
282
|
+
<span className="font-['Poppins',sans-serif] text-[10px] text-[#676c80] ">
|
|
283
283
|
({product.summary?.reviewCount || 0})
|
|
284
284
|
</span>
|
|
285
285
|
</div>
|
|
@@ -370,7 +370,7 @@ export function ProductCard({
|
|
|
370
370
|
}
|
|
371
371
|
}}
|
|
372
372
|
disabled={isAddingToCart || (variantImages.length > 0 && !selectedVariantId) || displayInventoryCount === 0}
|
|
373
|
-
className="w-full font-['Poppins',sans-serif] font-medium text-[11px] px-3 py-2 rounded-full bg-[#5B9BD5] text-white hover:bg-[#4a8ac4] hover:shadow-lg transition-all duration-300 flex items-center justify-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
373
|
+
className="w-full font-['Poppins',sans-serif] font-medium text-[11px] px-3 py-2 rounded-full bg-[#5B9BD5] text-white hover:bg-[#4a8ac4] hover:shadow-lg transition-all duration-300 flex items-center justify-center gap-1.5 disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
|
|
374
374
|
>
|
|
375
375
|
<ShoppingCart className="h-4 w-4" />
|
|
376
376
|
{displayInventoryCount === 0 ? 'Out of Stock' : 'Add to Cart'}
|
|
@@ -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
|
+
}
|