hey-pharmacist-ecommerce 1.1.36 → 1.1.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hey-pharmacist-ecommerce",
3
- "version": "1.1.36",
3
+ "version": "1.1.38",
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",
@@ -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
- <button
13
- onClick={openDrawer}
14
- className="relative p-2 hover:bg-gray-100 rounded-lg transition-colors"
15
- aria-label="Notifications"
16
- >
17
- <Bell className="w-6 h-6 text-gray-700" />
18
-
19
- <AnimatePresence>
20
- {unreadCount > 0 && (
21
- <motion.span
22
- initial={{ scale: 0 }}
23
- animate={{ scale: 1 }}
24
- exit={{ scale: 0 }}
25
- className="absolute -top-1 -right-1 bg-red-500 text-white text-xs font-bold rounded-full w-5 h-5 flex items-center justify-center"
26
- >
27
- {unreadCount > 99 ? '99+' : unreadCount}
28
- </motion.span>
29
- )}
30
- </AnimatePresence>
31
- </button>
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
+ }
@@ -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/50'
213
+ : 'border-gray-200 hover:border-primary-50'
214
214
  }`}
215
215
  style={{ backgroundColor: variant.colorHex }}
216
216
  title={variant.color}
@@ -15,7 +15,6 @@ export interface EcommerceConfig {
15
15
  to: string; // hex color
16
16
  };
17
17
  apiBaseUrl: string;
18
- stripePublicKey: string;
19
18
  }
20
19
 
21
20
 
@@ -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
-
@@ -93,13 +93,13 @@ export function LoginScreen() {
93
93
  }}>
94
94
  {status && (
95
95
  <div
96
- className={`flex items-start gap-2 rounded-2xl border px-4 py-3 text-sm ${
96
+ className={`flex flex-row items-start gap-2 rounded-2xl border px-4 py-3 text-sm ${
97
97
  status.type === 'success'
98
98
  ? 'border-green-200 bg-green-50 text-green-800'
99
99
  : 'border-red-200 bg-red-50 text-red-700'
100
100
  }`}
101
101
  >
102
- <span className="mt-[2px] text-base">{status.type === 'success' ? '✔' : '!'}</span>
102
+ <span className=" text-base">{status.type === 'success' ? '✔' : '!'}</span>
103
103
  <span>{status.message}</span>
104
104
  </div>
105
105
  )}
@@ -38,7 +38,7 @@ export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
38
38
  if (isLoading) {
39
39
  return (
40
40
  <div className="min-h-screen bg-slate-50 flex flex-col items-center justify-center p-4">
41
- <div className="w-16 h-16 border-4 border-primary/20 border-t-primary rounded-full animate-spin mb-4" />
41
+ <div className="w-16 h-16 border-4 border-primary-20 border-t-primary rounded-full animate-spin mb-4" />
42
42
  <p className="text-muted font-medium animate-pulse">Retrieving order details...</p>
43
43
  </div>
44
44
  );
@@ -229,7 +229,7 @@ export function OrderDetailScreen({ id }: OrderDetailScreenProps) {
229
229
  <motion.div
230
230
  initial={{ opacity: 0, x: 20 }}
231
231
  animate={{ opacity: 1, x: 0 }}
232
- className="bg-secondary p-8 rounded-[2rem] text-white shadow-xl shadow-secondary/20 sticky top-8"
232
+ className="bg-secondary p-8 rounded-[2rem] text-white shadow-xl shadow-secondary-20 sticky top-8"
233
233
  >
234
234
  <h2 className="text-xl font-bold mb-6 flex items-center gap-2">
235
235
  Summary View
@@ -437,8 +437,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
437
437
  type="button"
438
438
  onClick={() => setActiveImageIndex(index)}
439
439
  className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-all ${activeImageIndex === index
440
- ? 'border-primary/50 ring-2 ring-primary/80 ring-offset-2 shadow-md'
441
- : 'border-slate-200 hover:border-primary/50'
440
+ ? 'border-primary-50 ring-2 ring-primary-80 ring-offset-2 shadow-md'
441
+ : 'border-slate-200 hover:border-primary-50'
442
442
  }`}
443
443
  >
444
444
  <Image
@@ -564,8 +564,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
564
564
  type="button"
565
565
  onClick={() => handleVariantSelect(variant)}
566
566
  className={`flex items-start gap-3 px-4 py-2.5 rounded-xl border-2 transition-all ${isSelected
567
- ? 'border-primary bg-primary/5'
568
- : 'border-gray-200 hover:border-primary/50'
567
+ ? 'border-primary bg-primary-5'
568
+ : 'border-gray-200 hover:border-primary-50'
569
569
  }`}
570
570
  >
571
571
  <div className={`relative h-12 w-12 shrink-0 overflow-hidden rounded-full border-2 ${isSelected ? 'border-primary' : 'border-slate-200'}`}>
@@ -660,7 +660,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
660
660
  )
661
661
  }
662
662
  </button>
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"
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"
664
664
  onClick={handleToggleFavorite}>
665
665
  <Heart className={`h-4 w-4 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-primary'}`} />
666
666
  </button>
@@ -845,7 +845,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
845
845
  type="button"
846
846
  onClick={applyCustomPrice}
847
847
  disabled={!isCustomPriceDirty}
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"
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"
849
849
  >
850
850
  Apply
851
851
  </button>
@@ -943,7 +943,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
943
943
  <div className="relative">
944
944
  <div className={`size-12 rounded-full mb-3 mx-auto flex items-center justify-center transition-all ${!categoryFilter
945
945
  ? 'bg-white/20'
946
- : 'bg-linear-to-br from-primary/10 to-secondary/10 group-hover:scale-110'
946
+ : 'bg-linear-to-br from-primary-10 to-secondary-10 group-hover:scale-110'
947
947
  }`}>
948
948
  <Package className={`size-6 ${!categoryFilter ? 'text-white' : 'text-primary'
949
949
  }`} />
@@ -978,7 +978,7 @@ export function ShopScreen({ initialFilters = {}, categoryName }: ShopScreenProp
978
978
  <div className="relative">
979
979
  <div className={`size-12 rounded-full mb-3 mx-auto flex items-center justify-center transition-all ${isSelected
980
980
  ? 'bg-white/20'
981
- : 'bg-linear-to-br from-primary/10 to-secondary/10 group-hover:scale-110'
981
+ : 'bg-linear-to-br from-primary-10 to-secondary-10 group-hover:scale-110'
982
982
  }`}>
983
983
  <Icon className={`size-6 ${isSelected ? 'text-white' : 'text-primary'
984
984
  }`} />
@@ -391,7 +391,7 @@ export default function WishlistScreen() {
391
391
  <Button
392
392
  size="sm"
393
393
  onClick={() => router.push(buildPath(`/products/${product._id}`))}
394
- className='bg-primary/90 text-white hover:bg-primary/70'
394
+ className='bg-primary-90 text-white hover:bg-primary-70'
395
395
  >
396
396
  View details
397
397
  </Button>