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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hey-pharmacist-ecommerce",
3
- "version": "1.1.35",
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 || '/placeholder-product.png'}
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
- {/* Product Image */}
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 || '/placeholder-product.png'}
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
- <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
+ }
@@ -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 || '/placeholder-product.png'}
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 || '/placeholder-product.png';
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/50'
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 || '/placeholder-product.png'}
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 || '/placeholder-product.png'} alt={item.productVariantData.name} className="w-full h-full object-cover" height={200} width={200} />
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/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" />
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 || '/placeholder-product.png'}
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/20 sticky top-8"
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/50 ring-2 ring-primary/80 ring-offset-2 shadow-md'
439
- : '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'
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
- || '/placeholder-product.png';
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/5'
566
- : 'border-gray-200 hover:border-primary/50'
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/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"
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/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"
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/10 to-secondary/10 group-hover:scale-110'
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/10 to-secondary/10 group-hover:scale-110'
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 || '/placeholder-product.png'}
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 || '/placeholder-product.png'}
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/90 text-white hover:bg-primary/70'
394
+ className='bg-primary-90 text-white hover:bg-primary-70'
393
395
  >
394
396
  View details
395
397
  </Button>