hey-pharmacist-ecommerce 1.1.35 → 1.1.37
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.js +934 -730
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +587 -383
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/AccountOrdersTab.tsx +3 -1
- package/src/components/CartItem.tsx +6 -2
- package/src/components/NotificationBell.tsx +74 -21
- package/src/components/NotificationModal.tsx +223 -0
- package/src/components/OrderCard.tsx +3 -1
- package/src/components/ProductCard.tsx +6 -1
- package/src/components/QuickViewModal.tsx +1 -1
- package/src/lib/constants/assets.ts +1 -0
- package/src/providers/EcommerceProvider.tsx +0 -2
- package/src/screens/CheckoutScreen.tsx +6 -4
- package/src/screens/OrderDetailScreen.tsx +7 -3
- package/src/screens/ProductDetailScreen.tsx +8 -6
- package/src/screens/ShopScreen.tsx +6 -4
- package/src/screens/WishlistScreen.tsx +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hey-pharmacist-ecommerce",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.37",
|
|
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",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React from 'react';
|
|
4
6
|
import { Package, Truck } from 'lucide-react';
|
|
5
7
|
import { useCurrentOrders } from '@/hooks/useOrders';
|
|
@@ -102,7 +104,7 @@ export function AccountOrdersTab() {
|
|
|
102
104
|
<div key={item.productVariantId || index} className="flex items-center gap-3">
|
|
103
105
|
<div className="relative w-12 h-12 rounded-lg bg-slate-100 shrink-0 overflow-hidden">
|
|
104
106
|
<Image
|
|
105
|
-
src={item?.productVariantData?.media?.[0]?.file ||
|
|
107
|
+
src={item?.productVariantData?.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
106
108
|
alt={item?.productVariantData?.name || 'Product image'}
|
|
107
109
|
fill
|
|
108
110
|
className="object-cover"
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React, { useState } from 'react';
|
|
4
6
|
import { motion } from 'framer-motion';
|
|
5
7
|
import { Minus, Plus, Trash2 } from 'lucide-react';
|
|
8
|
+
|
|
6
9
|
import { formatPrice } from '@/lib/utils/format';
|
|
7
10
|
import { useCart } from '@/providers/CartProvider';
|
|
8
11
|
import Image from 'next/image';
|
|
@@ -52,10 +55,11 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
<div className="flex gap-4 pr-8">
|
|
55
|
-
|
|
58
|
+
|
|
59
|
+
|
|
56
60
|
<div className="w-28 h-28 rounded-[16px] overflow-hidden bg-gray-50 shrink-0">
|
|
57
61
|
<Image
|
|
58
|
-
src={item.productVariantData.media[0]?.file ||
|
|
62
|
+
src={item.productVariantData.media[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
59
63
|
alt={item.productVariantData.name}
|
|
60
64
|
className="w-full h-full object-cover"
|
|
61
65
|
height={112}
|
|
@@ -5,29 +5,82 @@ import { Bell } from 'lucide-react';
|
|
|
5
5
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
6
6
|
import { useNotificationCenter } from '@/providers/NotificationCenterProvider';
|
|
7
7
|
|
|
8
|
+
import { NotificationModal } from './NotificationModal';
|
|
9
|
+
|
|
8
10
|
export function NotificationBell() {
|
|
9
|
-
const { unreadCount, openDrawer } = useNotificationCenter();
|
|
11
|
+
const { unreadCount, isDrawerOpen, openDrawer, closeDrawer } = useNotificationCenter();
|
|
12
|
+
|
|
13
|
+
const handleToggle = (e: React.MouseEvent) => {
|
|
14
|
+
e.stopPropagation();
|
|
15
|
+
if (isDrawerOpen) {
|
|
16
|
+
closeDrawer();
|
|
17
|
+
} else {
|
|
18
|
+
openDrawer();
|
|
19
|
+
}
|
|
20
|
+
};
|
|
10
21
|
|
|
11
22
|
return (
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
<div className="relative">
|
|
24
|
+
<motion.button
|
|
25
|
+
id="notification-bell-button"
|
|
26
|
+
onClick={handleToggle}
|
|
27
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
28
|
+
className={`relative p-2.5 rounded-xl transition-all duration-300 group flex items-center justify-center ${isDrawerOpen
|
|
29
|
+
? 'bg-primary-50 text-primary-600'
|
|
30
|
+
: 'hover:bg-gradient-to-br hover:from-primary-50 hover:to-primary-100/50 text-gray-700'
|
|
31
|
+
}`}
|
|
32
|
+
aria-label="Notifications"
|
|
33
|
+
whileHover={isDrawerOpen ? {} : { scale: 1.05 }}
|
|
34
|
+
whileTap={{ scale: 0.95 }}
|
|
35
|
+
>
|
|
36
|
+
{/* Bell Icon */}
|
|
37
|
+
<Bell
|
|
38
|
+
className={`w-6 h-6 transition-colors duration-300 ${isDrawerOpen ? 'text-primary-600 bg-gray-100 rounded-lg p-2 w-10 h-10 transition-all duration-300' : 'group-hover:text-primary-600'
|
|
39
|
+
}`}
|
|
40
|
+
strokeWidth={2}
|
|
41
|
+
/>
|
|
42
|
+
|
|
43
|
+
{/* Unread Badge */}
|
|
44
|
+
<AnimatePresence>
|
|
45
|
+
{unreadCount > 0 && (
|
|
46
|
+
<>
|
|
47
|
+
{/* Pulse ring animation */}
|
|
48
|
+
<motion.span
|
|
49
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
50
|
+
animate={{
|
|
51
|
+
scale: [1, 1.4, 1.4],
|
|
52
|
+
opacity: [0.6, 0, 0]
|
|
53
|
+
}}
|
|
54
|
+
exit={{ scale: 0, opacity: 0 }}
|
|
55
|
+
transition={{
|
|
56
|
+
duration: 2,
|
|
57
|
+
repeat: Infinity,
|
|
58
|
+
repeatDelay: 0.5
|
|
59
|
+
}}
|
|
60
|
+
className="absolute -top-0.5 -right-0.5 w-6 h-6 bg-red-500 rounded-full"
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{/* Badge */}
|
|
64
|
+
<motion.span
|
|
65
|
+
initial={{ scale: 0, rotate: -180 }}
|
|
66
|
+
animate={{ scale: 1, rotate: 0 }}
|
|
67
|
+
exit={{ scale: 0, rotate: 180 }}
|
|
68
|
+
transition={{
|
|
69
|
+
type: 'spring',
|
|
70
|
+
damping: 15,
|
|
71
|
+
stiffness: 300
|
|
72
|
+
}}
|
|
73
|
+
className="absolute -top-1 -right-1 min-w-[20px] h-5 bg-gradient-to-br from-red-500 to-red-600 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1.5 shadow-lg shadow-red-500/40 border-2 border-white"
|
|
74
|
+
>
|
|
75
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
76
|
+
</motion.span>
|
|
77
|
+
</>
|
|
78
|
+
)}
|
|
79
|
+
</AnimatePresence>
|
|
80
|
+
</motion.button>
|
|
81
|
+
|
|
82
|
+
{/* Notification Dropdown */}
|
|
83
|
+
<NotificationModal />
|
|
84
|
+
</div>
|
|
32
85
|
);
|
|
33
86
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useCallback,
|
|
7
|
+
useMemo,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
10
|
+
import { X, Settings, CheckCheck, BellOff } from 'lucide-react';
|
|
11
|
+
import { useNotificationCenter } from '@/providers/NotificationCenterProvider';
|
|
12
|
+
import { NotificationCard } from './NotificationCard';
|
|
13
|
+
import { useRouter } from 'next/navigation';
|
|
14
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
15
|
+
|
|
16
|
+
const modalVariants = {
|
|
17
|
+
hidden: { opacity: 0, y: -8, scale: 0.96 },
|
|
18
|
+
visible: { opacity: 1, y: 0, scale: 1 },
|
|
19
|
+
exit: { opacity: 0, y: -8, scale: 0.96 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function NotificationModal() {
|
|
23
|
+
const {
|
|
24
|
+
isDrawerOpen,
|
|
25
|
+
closeDrawer,
|
|
26
|
+
notifications,
|
|
27
|
+
unreadCount,
|
|
28
|
+
isLoading,
|
|
29
|
+
markAsRead,
|
|
30
|
+
markAllAsRead,
|
|
31
|
+
deleteNotification,
|
|
32
|
+
loadMore,
|
|
33
|
+
hasMore,
|
|
34
|
+
} = useNotificationCenter();
|
|
35
|
+
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const { buildPath } = useBasePath();
|
|
38
|
+
|
|
39
|
+
const scrollRef = useRef<HTMLDivElement>(null);
|
|
40
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
41
|
+
const lastScrollTrigger = useRef(0);
|
|
42
|
+
|
|
43
|
+
/* ---------------------------- Handlers ---------------------------- */
|
|
44
|
+
|
|
45
|
+
const handleScroll = useCallback(() => {
|
|
46
|
+
if (!scrollRef.current || isLoading || !hasMore) return;
|
|
47
|
+
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
if (now - lastScrollTrigger.current < 300) return;
|
|
50
|
+
|
|
51
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
|
|
52
|
+
if ((scrollTop + clientHeight) / scrollHeight > 0.8) {
|
|
53
|
+
lastScrollTrigger.current = now;
|
|
54
|
+
loadMore();
|
|
55
|
+
}
|
|
56
|
+
}, [isLoading, hasMore, loadMore]);
|
|
57
|
+
|
|
58
|
+
const handleSettingsClick = useCallback(() => {
|
|
59
|
+
closeDrawer();
|
|
60
|
+
router.push(buildPath('/account/notifications'));
|
|
61
|
+
}, [closeDrawer, router, buildPath]);
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (!isDrawerOpen) return;
|
|
66
|
+
|
|
67
|
+
modalRef.current?.focus();
|
|
68
|
+
|
|
69
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
70
|
+
if (e.key === 'Escape') closeDrawer();
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
document.addEventListener('keydown', onKeyDown);
|
|
74
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
75
|
+
}, [isDrawerOpen, closeDrawer]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!isDrawerOpen) return;
|
|
79
|
+
|
|
80
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
81
|
+
const bellButton = document.getElementById('notification-bell-button');
|
|
82
|
+
if (
|
|
83
|
+
modalRef.current &&
|
|
84
|
+
!modalRef.current.contains(e.target as Node) &&
|
|
85
|
+
!bellButton?.contains(e.target as Node)
|
|
86
|
+
) {
|
|
87
|
+
closeDrawer();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
92
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
93
|
+
}, [isDrawerOpen, closeDrawer]);
|
|
94
|
+
|
|
95
|
+
const hasNotifications = notifications.length > 0;
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<AnimatePresence>
|
|
100
|
+
{isDrawerOpen && (
|
|
101
|
+
<>
|
|
102
|
+
{/* Backdrop */}
|
|
103
|
+
{/* <motion.div
|
|
104
|
+
initial={{ opacity: 0 }}
|
|
105
|
+
animate={{ opacity: 0.4 }}
|
|
106
|
+
exit={{ opacity: 0 }}
|
|
107
|
+
className="fixed inset-0 bg-black z-40"
|
|
108
|
+
/> */}
|
|
109
|
+
|
|
110
|
+
<motion.div
|
|
111
|
+
ref={modalRef}
|
|
112
|
+
role="dialog"
|
|
113
|
+
aria-modal="true"
|
|
114
|
+
aria-labelledby="notification-title"
|
|
115
|
+
tabIndex={-1}
|
|
116
|
+
variants={modalVariants}
|
|
117
|
+
initial="hidden"
|
|
118
|
+
animate="visible"
|
|
119
|
+
exit="exit"
|
|
120
|
+
transition={{ type: 'spring', stiffness: 350, damping: 28 }}
|
|
121
|
+
className="absolute top-full lg:right-0 mt-1 lg:w-screen lg:max-w-sm bg-white rounded-2xl shadow-[0_20px_50px_-12px_rgba(0,0,0,0.15)] z-[100] flex flex-col overflow-hidden border border-gray-100 origin-top-right rounded-lg border border-slate-200 bg-white shadow-lg"
|
|
122
|
+
style={{ maxHeight: 'calc(100vh - 120px)' }}
|
|
123
|
+
>
|
|
124
|
+
{/* Elegant arrow pointer connecting to the bell */}
|
|
125
|
+
<div className="absolute -top-1 right-5 w-3 h-3 bg-white border-l border-t border-gray-100 transform rotate-45 z-10" />
|
|
126
|
+
{/* Header */}
|
|
127
|
+
<div className="flex items-center justify-between px-4 py-3 border-b bg-gradient-to-r from-primary-50 to-white">
|
|
128
|
+
<div className="flex items-center gap-2">
|
|
129
|
+
<h2
|
|
130
|
+
id="notification-title"
|
|
131
|
+
className="text-lg font-bold text-gray-900"
|
|
132
|
+
>
|
|
133
|
+
Notifications
|
|
134
|
+
</h2>
|
|
135
|
+
<span
|
|
136
|
+
aria-live="polite"
|
|
137
|
+
className="bg-red-500 text-white text-xs font-bold px-2 py-0.5 rounded-full"
|
|
138
|
+
>
|
|
139
|
+
{unreadCount}
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div className="flex items-center gap-1">
|
|
144
|
+
{unreadCount > 0 && (
|
|
145
|
+
<button
|
|
146
|
+
onClick={markAllAsRead}
|
|
147
|
+
title="Mark all as read"
|
|
148
|
+
className="p-2 rounded-lg hover:bg-white transition"
|
|
149
|
+
>
|
|
150
|
+
<CheckCheck className="w-4 h-4 text-gray-600" />
|
|
151
|
+
</button>
|
|
152
|
+
)}
|
|
153
|
+
|
|
154
|
+
<button
|
|
155
|
+
onClick={handleSettingsClick}
|
|
156
|
+
title="Notification settings"
|
|
157
|
+
className="p-2 rounded-lg hover:bg-white transition"
|
|
158
|
+
>
|
|
159
|
+
<Settings className="w-4 h-4 text-gray-600" />
|
|
160
|
+
</button>
|
|
161
|
+
|
|
162
|
+
<button
|
|
163
|
+
onClick={closeDrawer}
|
|
164
|
+
aria-label="Close notifications"
|
|
165
|
+
className="p-2 rounded-lg hover:bg-red-50 transition"
|
|
166
|
+
>
|
|
167
|
+
<X className="w-4 h-4 text-gray-600" />
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* List */}
|
|
173
|
+
<div
|
|
174
|
+
ref={scrollRef}
|
|
175
|
+
onScroll={handleScroll}
|
|
176
|
+
className="flex-1 overflow-y-auto p-3 space-y-2 bg-gray-50 scrollbar-thin scrollbar-thumb-gray-300"
|
|
177
|
+
>
|
|
178
|
+
{!hasNotifications && !isLoading ? (
|
|
179
|
+
<div className="flex flex-col items-center justify-center py-14 text-center">
|
|
180
|
+
<BellOff className="w-10 h-10 text-gray-400 mb-3" />
|
|
181
|
+
<p className="text-sm font-medium text-gray-700">
|
|
182
|
+
No notifications yet
|
|
183
|
+
</p>
|
|
184
|
+
<p className="text-xs text-gray-500 mt-1">
|
|
185
|
+
You’re all caught up 🎉
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
{notifications.map((notification) => (
|
|
191
|
+
<NotificationCard
|
|
192
|
+
key={notification._id}
|
|
193
|
+
notification={notification}
|
|
194
|
+
onMarkAsRead={markAsRead}
|
|
195
|
+
onDelete={deleteNotification}
|
|
196
|
+
/>
|
|
197
|
+
))}
|
|
198
|
+
|
|
199
|
+
{isLoading && (
|
|
200
|
+
<div className="space-y-2 py-2">
|
|
201
|
+
{[...Array(3)].map((_, i) => (
|
|
202
|
+
<div
|
|
203
|
+
key={i}
|
|
204
|
+
className="h-16 rounded-xl bg-gray-200 animate-pulse"
|
|
205
|
+
/>
|
|
206
|
+
))}
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{!hasMore && hasNotifications && (
|
|
211
|
+
<div className="text-center py-3 text-xs text-gray-500">
|
|
212
|
+
You’re all caught up 🎉
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</>
|
|
216
|
+
)}
|
|
217
|
+
</div>
|
|
218
|
+
</motion.div>
|
|
219
|
+
</>
|
|
220
|
+
)}
|
|
221
|
+
</AnimatePresence>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React, { useRef, useState } from 'react';
|
|
4
6
|
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
|
5
7
|
import { CreditCard, Star, Trash2 } from 'lucide-react';
|
|
@@ -106,7 +108,7 @@ export function OrderCard({ order, onDelete }: OrderCardProps) {
|
|
|
106
108
|
<div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
|
|
107
109
|
<div className="relative w-12 h-12 rounded-sm bg-gray-100 shrink-0 overflow-hidden">
|
|
108
110
|
<Image
|
|
109
|
-
src={item?.productVariantData?.media?.[0]?.file ||
|
|
111
|
+
src={item?.productVariantData?.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
110
112
|
alt={item?.productVariantData?.name || 'Product image'}
|
|
111
113
|
fill
|
|
112
114
|
className="object-cover"
|
|
@@ -13,6 +13,7 @@ import { useBasePath } from '@/providers/BasePathProvider';
|
|
|
13
13
|
import { QuickViewModal } from './QuickViewModal';
|
|
14
14
|
import { useNotification } from '@/providers/NotificationProvider';
|
|
15
15
|
import { useAuth } from '@/providers/AuthProvider';
|
|
16
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
16
17
|
|
|
17
18
|
interface ProductCardProps {
|
|
18
19
|
product: Product;
|
|
@@ -157,8 +158,12 @@ export function ProductCard({
|
|
|
157
158
|
return selectedVariant ? selectedVariant.inventoryCount : product.variants?.[0]?.inventoryCount;
|
|
158
159
|
}, [selectedVariant, product.variants]);
|
|
159
160
|
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// ... (inside component)
|
|
164
|
+
|
|
160
165
|
const imageSource = useMemo(() => {
|
|
161
|
-
const src = selectedVariantImage || selectedVariant?.media?.[0]?.file || product.media?.[0]?.file ||
|
|
166
|
+
const src = selectedVariantImage || selectedVariant?.media?.[0]?.file || product.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC;
|
|
162
167
|
return {
|
|
163
168
|
src,
|
|
164
169
|
alt: product.name || 'Product image'
|
|
@@ -210,7 +210,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
210
210
|
}}
|
|
211
211
|
className={`size-10 rounded-full border-2 transition-all ${selectedVariantIndex === index
|
|
212
212
|
? 'border-primary scale-110'
|
|
213
|
-
: 'border-gray-200 hover:border-primary
|
|
213
|
+
: 'border-gray-200 hover:border-primary-50'
|
|
214
214
|
}`}
|
|
215
215
|
style={{ backgroundColor: variant.colorHex }}
|
|
216
216
|
title={variant.color}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PLACEHOLDER_IMAGE_SRC = `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZjNmNGY2Ii8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIyNCIgZmlsbD0iIzljYTNhZiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSI+Tm8gSW1hZ2U8L3RleHQ+CiAgPHJlY3QgeD0iMTUwIiB5PSIxMjAiIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBzdHJva2U9IiM5Y2EzYWYiIHN0cm9rZS13aWR0aD0iNCIgZmlsbD0ibm9uZSIgcng9IjgiLz4KICA8cGF0aCBkPSJNMTUwIDE4MCBMMTcwIDE2MCBMMjAwIDE5MCBMMjIwIDE3MCBMMjUwIDIwMCBMMjUwIDIyMCBMMTUwIDIyMCBaIiBmaWxsPSIjOWNhM2FmIiBvcGFjaXR5PSIwLjUiLz4KPC9zdmc+`;
|
|
@@ -43,7 +43,6 @@ export function EcommerceProvider({ config, children, withToaster = true, basePa
|
|
|
43
43
|
<DiscountProvider>
|
|
44
44
|
<WishlistProvider>
|
|
45
45
|
{children}
|
|
46
|
-
<NotificationDrawer />
|
|
47
46
|
</WishlistProvider>
|
|
48
47
|
</DiscountProvider>
|
|
49
48
|
</CartProvider>
|
|
@@ -55,4 +54,3 @@ export function EcommerceProvider({ config, children, withToaster = true, basePa
|
|
|
55
54
|
</QueryClientProvider>
|
|
56
55
|
);
|
|
57
56
|
}
|
|
58
|
-
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React, { useState, useEffect } from 'react';
|
|
4
6
|
import { motion } from 'framer-motion';
|
|
5
7
|
import { useForm } from 'react-hook-form';
|
|
@@ -868,7 +870,7 @@ export function CheckoutScreen() {
|
|
|
868
870
|
{/* Provider Logo */}
|
|
869
871
|
<div className="shrink-0">
|
|
870
872
|
<Image
|
|
871
|
-
src={rate.providerImage75 ||
|
|
873
|
+
src={rate.providerImage75 || PLACEHOLDER_IMAGE_SRC}
|
|
872
874
|
alt={rate.provider}
|
|
873
875
|
className="w-12 h-12 rounded-lg object-contain bg-white border border-gray-200 p-1"
|
|
874
876
|
onError={(e: any) => {
|
|
@@ -987,7 +989,7 @@ export function CheckoutScreen() {
|
|
|
987
989
|
{cart?.cartBody?.items?.map((item: any) => (
|
|
988
990
|
<div key={item.productVariantId || item.id} className="flex gap-3">
|
|
989
991
|
<div className="w-16 h-16 rounded-xl overflow-hidden bg-white shrink-0">
|
|
990
|
-
<Image src={item.productVariantData?.media?.[0]?.file ||
|
|
992
|
+
<Image src={item.productVariantData?.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC} alt={item.productVariantData.name} className="w-full h-full object-cover" height={200} width={200} />
|
|
991
993
|
</div>
|
|
992
994
|
<div className="flex-1 min-w-0">
|
|
993
995
|
<p className="font-['Poppins',sans-serif] font-medium text-[12px] text-[#2B4B7C] mb-1">
|
|
@@ -1008,8 +1010,8 @@ export function CheckoutScreen() {
|
|
|
1008
1010
|
|
|
1009
1011
|
{/* Coupon Code Section */}
|
|
1010
1012
|
<div className="mb-6">
|
|
1011
|
-
<CouponCodeInput
|
|
1012
|
-
userId={user?.id}
|
|
1013
|
+
<CouponCodeInput
|
|
1014
|
+
userId={user?.id}
|
|
1013
1015
|
className="mb-4"
|
|
1014
1016
|
/>
|
|
1015
1017
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React from 'react';
|
|
4
6
|
import { motion } from 'framer-motion';
|
|
5
7
|
import {
|
|
@@ -36,7 +38,7 @@ export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
|
|
|
36
38
|
if (isLoading) {
|
|
37
39
|
return (
|
|
38
40
|
<div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
|
|
39
|
-
<div className="w-16 h-16 border-4 border-primary
|
|
41
|
+
<div className="w-16 h-16 border-4 border-primary-20 border-t-primary rounded-full animate-spin mb-4" />
|
|
40
42
|
<p className="text-muted font-medium animate-pulse">Retrieving order details...</p>
|
|
41
43
|
</div>
|
|
42
44
|
);
|
|
@@ -143,9 +145,11 @@ export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
|
|
|
143
145
|
<div className="divide-y divide-slate-100">
|
|
144
146
|
{items.map((item, idx) => (
|
|
145
147
|
<div key={item._id || idx} className="p-6 flex gap-6 group hover:bg-slate-50/50 transition-colors">
|
|
148
|
+
|
|
149
|
+
|
|
146
150
|
<div className="relative w-20 h-20 bg-slate-100 rounded-2xl overflow-hidden shrink-0 border border-slate-100 group-hover:scale-105 transition-transform duration-300">
|
|
147
151
|
<Image
|
|
148
|
-
src={item.productVariantData?.media?.[0]?.file ||
|
|
152
|
+
src={item.productVariantData?.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
149
153
|
alt={item.productVariantData?.name || 'Item'}
|
|
150
154
|
fill
|
|
151
155
|
className="object-cover"
|
|
@@ -225,7 +229,7 @@ export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
|
|
|
225
229
|
<motion.div
|
|
226
230
|
initial={{ opacity: 0, x: 20 }}
|
|
227
231
|
animate={{ opacity: 1, x: 0 }}
|
|
228
|
-
className="bg-secondary p-8 rounded-[2rem] text-white shadow-xl shadow-secondary
|
|
232
|
+
className="bg-secondary p-8 rounded-[2rem] text-white shadow-xl shadow-secondary-20 sticky top-8"
|
|
229
233
|
>
|
|
230
234
|
<h2 className="text-xl font-bold mb-6 flex items-center gap-2">
|
|
231
235
|
Summary View
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React, { useEffect, useState, useMemo } from 'react';
|
|
4
6
|
import { motion } from 'framer-motion';
|
|
5
7
|
import {
|
|
@@ -435,8 +437,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
435
437
|
type="button"
|
|
436
438
|
onClick={() => setActiveImageIndex(index)}
|
|
437
439
|
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-all ${activeImageIndex === index
|
|
438
|
-
? 'border-primary
|
|
439
|
-
: 'border-slate-200 hover:border-primary
|
|
440
|
+
? 'border-primary-50 ring-2 ring-primary-80 ring-offset-2 shadow-md'
|
|
441
|
+
: 'border-slate-200 hover:border-primary-50'
|
|
440
442
|
}`}
|
|
441
443
|
>
|
|
442
444
|
<Image
|
|
@@ -555,15 +557,15 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
555
557
|
const variantImage = variant.media?.[0]?.file
|
|
556
558
|
|| product.media?.[0]?.file
|
|
557
559
|
|| product.images?.[0]
|
|
558
|
-
||
|
|
560
|
+
|| PLACEHOLDER_IMAGE_SRC;
|
|
559
561
|
return (
|
|
560
562
|
<button
|
|
561
563
|
key={variant._id}
|
|
562
564
|
type="button"
|
|
563
565
|
onClick={() => handleVariantSelect(variant)}
|
|
564
566
|
className={`flex items-start gap-3 px-4 py-2.5 rounded-xl border-2 transition-all ${isSelected
|
|
565
|
-
? 'border-primary bg-primary
|
|
566
|
-
: 'border-gray-200 hover:border-primary
|
|
567
|
+
? 'border-primary bg-primary-5'
|
|
568
|
+
: 'border-gray-200 hover:border-primary-50'
|
|
567
569
|
}`}
|
|
568
570
|
>
|
|
569
571
|
<div className={`relative h-12 w-12 shrink-0 overflow-hidden rounded-full border-2 ${isSelected ? 'border-primary' : 'border-slate-200'}`}>
|
|
@@ -658,7 +660,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
658
660
|
)
|
|
659
661
|
}
|
|
660
662
|
</button>
|
|
661
|
-
<button className="sm:w-auto px-6 py-4 rounded-full border-2 border-primary hover:bg-primary
|
|
663
|
+
<button className="sm:w-auto px-6 py-4 rounded-full border-2 border-primary hover:bg-primary-5 transition-all flex items-center justify-center"
|
|
662
664
|
onClick={handleToggleFavorite}>
|
|
663
665
|
<Heart className={`h-4 w-4 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-primary'}`} />
|
|
664
666
|
</button>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
useCallback,
|
|
5
7
|
useEffect,
|
|
@@ -843,7 +845,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
843
845
|
type="button"
|
|
844
846
|
onClick={applyCustomPrice}
|
|
845
847
|
disabled={!isCustomPriceDirty}
|
|
846
|
-
className="w-full rounded-lg border border-primary bg-primary
|
|
848
|
+
className="w-full rounded-lg border border-primary bg-primary-10 px-4 py-2 text-sm font-medium text-primary transition hover:bg-primary-20 disabled:cursor-not-allowed disabled:border-slate-200 disabled:text-slate-400"
|
|
847
849
|
>
|
|
848
850
|
Apply
|
|
849
851
|
</button>
|
|
@@ -941,7 +943,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
941
943
|
<div className="relative">
|
|
942
944
|
<div className={`size-12 rounded-full mb-3 mx-auto flex items-center justify-center transition-all ${!categoryFilter
|
|
943
945
|
? 'bg-white/20'
|
|
944
|
-
: 'bg-linear-to-br from-primary
|
|
946
|
+
: 'bg-linear-to-br from-primary-10 to-secondary-10 group-hover:scale-110'
|
|
945
947
|
}`}>
|
|
946
948
|
<Package className={`size-6 ${!categoryFilter ? 'text-white' : 'text-primary'
|
|
947
949
|
}`} />
|
|
@@ -976,7 +978,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
976
978
|
<div className="relative">
|
|
977
979
|
<div className={`size-12 rounded-full mb-3 mx-auto flex items-center justify-center transition-all ${isSelected
|
|
978
980
|
? 'bg-white/20'
|
|
979
|
-
: 'bg-linear-to-br from-primary
|
|
981
|
+
: 'bg-linear-to-br from-primary-10 to-secondary-10 group-hover:scale-110'
|
|
980
982
|
}`}>
|
|
981
983
|
<Icon className={`size-6 ${isSelected ? 'text-white' : 'text-primary'
|
|
982
984
|
}`} />
|
|
@@ -1126,7 +1128,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
|
|
|
1126
1128
|
>
|
|
1127
1129
|
<div className="relative h-48 w-full overflow-hidden rounded-2xl bg-gray-100 md:h-40 md:w-40">
|
|
1128
1130
|
<Image
|
|
1129
|
-
src={product.media?.[0]?.file ||
|
|
1131
|
+
src={product.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
1130
1132
|
alt={product.name}
|
|
1131
1133
|
fill
|
|
1132
1134
|
className="object-cover transition duration-500 group-hover:scale-105"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PLACEHOLDER_IMAGE_SRC } from '@/lib/constants/assets';
|
|
4
|
+
|
|
3
5
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
4
6
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
7
|
import {
|
|
@@ -347,7 +349,7 @@ export default function WishlistScreen() {
|
|
|
347
349
|
<div className="relative h-28 w-full overflow-hidden rounded-2xl bg-white sm:w-40">
|
|
348
350
|
<Image
|
|
349
351
|
fill
|
|
350
|
-
src={product.media?.[0]?.file ||
|
|
352
|
+
src={product.media?.[0]?.file || PLACEHOLDER_IMAGE_SRC}
|
|
351
353
|
alt={product.name || 'Wishlist item'}
|
|
352
354
|
className="h-full w-full object-cover"
|
|
353
355
|
/>
|
|
@@ -389,7 +391,7 @@ export default function WishlistScreen() {
|
|
|
389
391
|
<Button
|
|
390
392
|
size="sm"
|
|
391
393
|
onClick={() => router.push(buildPath(`/products/${product._id}`))}
|
|
392
|
-
className='bg-primary
|
|
394
|
+
className='bg-primary-90 text-white hover:bg-primary-70'
|
|
393
395
|
>
|
|
394
396
|
View details
|
|
395
397
|
</Button>
|