hey-pharmacist-ecommerce 1.1.12 → 1.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.d.mts +2 -4
  2. package/dist/index.d.ts +2 -4
  3. package/dist/index.js +1123 -972
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1123 -971
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +3 -3
  8. package/src/components/AccountAddressesTab.tsx +209 -0
  9. package/src/components/AccountOrdersTab.tsx +151 -0
  10. package/src/components/AccountOverviewTab.tsx +209 -0
  11. package/src/components/AccountPaymentTab.tsx +116 -0
  12. package/src/components/AccountSavedItemsTab.tsx +76 -0
  13. package/src/components/AccountSettingsTab.tsx +116 -0
  14. package/src/components/AddressFormModal.tsx +23 -10
  15. package/src/components/CartItem.tsx +60 -56
  16. package/src/components/FilterChips.tsx +54 -80
  17. package/src/components/Header.tsx +69 -16
  18. package/src/components/Notification.tsx +148 -0
  19. package/src/components/OrderCard.tsx +89 -56
  20. package/src/components/ProductCard.tsx +215 -178
  21. package/src/components/QuickViewModal.tsx +314 -0
  22. package/src/components/TabNavigation.tsx +48 -0
  23. package/src/components/ui/Button.tsx +1 -1
  24. package/src/components/ui/ConfirmModal.tsx +84 -0
  25. package/src/hooks/useOrders.ts +1 -0
  26. package/src/hooks/usePaymentMethods.ts +58 -0
  27. package/src/index.ts +0 -1
  28. package/src/providers/CartProvider.tsx +22 -6
  29. package/src/providers/EcommerceProvider.tsx +8 -7
  30. package/src/providers/FavoritesProvider.tsx +10 -3
  31. package/src/providers/NotificationProvider.tsx +79 -0
  32. package/src/providers/WishlistProvider.tsx +34 -9
  33. package/src/screens/AddressesScreen.tsx +72 -61
  34. package/src/screens/CartScreen.tsx +48 -32
  35. package/src/screens/ChangePasswordScreen.tsx +155 -0
  36. package/src/screens/CheckoutScreen.tsx +162 -125
  37. package/src/screens/EditProfileScreen.tsx +165 -0
  38. package/src/screens/LoginScreen.tsx +59 -72
  39. package/src/screens/NewAddressScreen.tsx +16 -10
  40. package/src/screens/OrdersScreen.tsx +91 -148
  41. package/src/screens/ProductDetailScreen.tsx +334 -234
  42. package/src/screens/ProfileScreen.tsx +190 -200
  43. package/src/screens/RegisterScreen.tsx +51 -70
  44. package/src/screens/SearchResultsScreen.tsx +2 -1
  45. package/src/screens/ShopScreen.tsx +260 -384
  46. package/src/screens/WishlistScreen.tsx +226 -224
  47. package/src/styles/globals.css +9 -0
  48. package/src/screens/CategoriesScreen.tsx +0 -122
  49. package/src/screens/HomeScreen.tsx +0 -211
@@ -1,6 +1,6 @@
1
1
  import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { AnimatePresence, motion } from 'framer-motion';
3
- import { Check, Search } from 'lucide-react';
3
+ import { Check, ChevronDown, X } from 'lucide-react';
4
4
 
5
5
  interface FilterChipsProps {
6
6
  label: string;
@@ -22,11 +22,8 @@ export function FilterChips({
22
22
  variant = 'primary',
23
23
  }: FilterChipsProps) {
24
24
  const [isOverflowOpen, setIsOverflowOpen] = useState(false);
25
- const [filterSearchTerm, setFilterSearchTerm] = useState('');
26
25
  const overflowMenuRef = useRef<HTMLDivElement | null>(null);
27
26
 
28
- const color = variant === 'primary' ? 'primary' : 'secondary';
29
-
30
27
  const { visibleFilters, overflowFilters } = useMemo(() => {
31
28
  const basePrimary = filters.slice(0, maxVisible);
32
29
 
@@ -52,19 +49,6 @@ export function FilterChips({
52
49
  };
53
50
  }, [filters, maxVisible, selected]);
54
51
 
55
- const filteredOverflowFilters = useMemo(() => {
56
- if (!filterSearchTerm.trim()) return overflowFilters;
57
- return overflowFilters.filter((filter) =>
58
- filter.toLowerCase().includes(filterSearchTerm.toLowerCase())
59
- );
60
- }, [filterSearchTerm, overflowFilters]);
61
-
62
- useEffect(() => {
63
- if (!isOverflowOpen) {
64
- setFilterSearchTerm('');
65
- }
66
- }, [isOverflowOpen]);
67
-
68
52
  useEffect(() => {
69
53
  function handleClickOutside(event: MouseEvent) {
70
54
  if (overflowMenuRef.current && !overflowMenuRef.current.contains(event.target as Node)) {
@@ -81,71 +65,76 @@ export function FilterChips({
81
65
  };
82
66
  }, [isOverflowOpen]);
83
67
 
68
+ const isPrimary = variant === 'primary';
69
+
84
70
  return (
85
- <div className="flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
86
- <span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-600">
87
- <Icon className="h-4 w-4" />
88
- {label}
89
- </span>
90
- <div className="flex flex-col gap-2 md:flex-row md:items-center md:gap-3">
91
- <div className="flex flex-wrap gap-2">
92
- {visibleFilters.map((filter) => (
71
+ <div className="space-y-2">
72
+ <div className="flex items-center gap-2">
73
+ <Icon className="h-4 w-4 text-slate-500" />
74
+ <span className="text-sm font-medium text-slate-700">{label}</span>
75
+ {selected !== 'All' && (
76
+ <button
77
+ type="button"
78
+ onClick={() => onSelect('All')}
79
+ className="ml-auto text-xs text-primary-600 hover:text-primary-700 font-medium"
80
+ >
81
+ Clear
82
+ </button>
83
+ )}
84
+ </div>
85
+
86
+ <div className="flex flex-wrap items-center gap-2">
87
+ {visibleFilters.map((filter) => {
88
+ const isSelected = selected === filter;
89
+ return (
93
90
  <button
94
91
  key={filter}
95
92
  type="button"
96
93
  onClick={() => onSelect(filter)}
97
- className={`rounded-full border px-3 py-1 text-sm font-medium transition ${
98
- selected === filter
99
- ? `border-${color}-600 bg-${color}-600 text-white shadow-lg shadow-${color}-500/30`
100
- : `border-slate-200 bg-slate-50 text-slate-600 hover:border-${color}-300 hover:text-${color}-600`
94
+ className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all whitespace-nowrap ${
95
+ isSelected
96
+ ? isPrimary
97
+ ? 'border-primary-500 bg-primary-500 text-white shadow-sm'
98
+ : 'border-secondary-500 bg-secondary-500 text-white shadow-sm'
99
+ : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50'
101
100
  }`}
102
101
  >
103
102
  {filter}
103
+ {isSelected && <X className="h-3.5 w-3.5" />}
104
104
  </button>
105
- ))}
106
- </div>
105
+ );
106
+ })}
107
107
 
108
108
  {overflowFilters.length > 0 && (
109
109
  <div className="relative" ref={overflowMenuRef}>
110
110
  <button
111
111
  type="button"
112
112
  onClick={() => setIsOverflowOpen((prev) => !prev)}
113
- className={`flex items-center gap-2 rounded-full border px-3 py-1 text-sm font-medium transition ${
113
+ className={`inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all whitespace-nowrap ${
114
114
  overflowFilters.includes(selected)
115
- ? `border-${color}-600 bg-${color}-50 text-${color}-700 shadow-lg shadow-${color}-500/20`
116
- : 'border-slate-200 bg-white text-slate-600 hover:border-slate-300'
115
+ ? isPrimary
116
+ ? 'border-primary-500 bg-primary-50 text-primary-700'
117
+ : 'border-secondary-500 bg-secondary-50 text-secondary-700'
118
+ : 'border-slate-200 bg-white text-slate-700 hover:border-slate-300 hover:bg-slate-50'
117
119
  }`}
118
120
  >
119
121
  <span>{overflowFilters.includes(selected) ? selected : 'More'}</span>
120
- <span className={`inline-flex h-5 min-w-[1.5rem] items-center justify-center rounded-full bg-${color}-100 px-1 text-xs font-semibold text-${color}-600`}>
121
- {overflowFilters.length}
122
- </span>
122
+ <ChevronDown className={`h-3.5 w-3.5 transition-transform ${isOverflowOpen ? 'rotate-180' : ''}`} />
123
123
  </button>
124
124
 
125
125
  <AnimatePresence>
126
126
  {isOverflowOpen && (
127
127
  <motion.div
128
- initial={{ opacity: 0, y: 8 }}
129
- animate={{ opacity: 1, y: 0 }}
130
- exit={{ opacity: 0, y: 8 }}
128
+ initial={{ opacity: 0, y: 8, scale: 0.95 }}
129
+ animate={{ opacity: 1, y: 0, scale: 1 }}
130
+ exit={{ opacity: 0, y: 8, scale: 0.95 }}
131
131
  transition={{ duration: 0.15 }}
132
- className="absolute right-0 z-50 mt-2 w-64 rounded-2xl border border-slate-100 bg-white shadow-xl shadow-primary-50"
132
+ className="absolute left-0 top-full z-50 mt-2 w-56 rounded-lg border border-slate-200 bg-white shadow-lg"
133
133
  >
134
- <div className="border-b border-slate-100 px-4 py-3">
135
- <div className="relative">
136
- <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400" />
137
- <input
138
- type="text"
139
- placeholder={`Search ${label.toLowerCase()}`}
140
- value={filterSearchTerm}
141
- onChange={(event) => setFilterSearchTerm(event.target.value)}
142
- className="w-full rounded-full border border-slate-200 bg-slate-50 py-2 pl-9 pr-3 text-sm text-slate-600 outline-none transition focus:border-primary-300 focus:bg-white focus:ring-2 focus:ring-primary-200"
143
- />
144
- </div>
145
- </div>
146
- <div className="max-h-60 overflow-y-auto px-2 py-2">
147
- {filteredOverflowFilters.length > 0 ? (
148
- filteredOverflowFilters.map((filter) => (
134
+ <div className="max-h-64 overflow-y-auto p-1.5">
135
+ {overflowFilters.map((filter) => {
136
+ const isSelected = selected === filter;
137
+ return (
149
138
  <button
150
139
  key={filter}
151
140
  type="button"
@@ -153,34 +142,19 @@ export function FilterChips({
153
142
  onSelect(filter);
154
143
  setIsOverflowOpen(false);
155
144
  }}
156
- className={`flex w-full items-center justify-between rounded-xl px-3 py-2 text-sm font-medium transition ${
157
- selected === filter
158
- ? `bg-${color}-600 text-white shadow-lg shadow-${color}-500/30`
159
- : 'text-slate-600 hover:bg-slate-100'
145
+ className={`flex w-full items-center justify-between rounded-md px-3 py-2 text-sm font-medium transition ${
146
+ isSelected
147
+ ? isPrimary
148
+ ? 'bg-primary-500 text-white'
149
+ : 'bg-secondary-500 text-white'
150
+ : 'text-slate-700 hover:bg-slate-100'
160
151
  }`}
161
152
  >
162
153
  <span>{filter}</span>
163
- {selected === filter && <Check className="h-4 w-4" />}
154
+ {isSelected && <Check className="h-4 w-4" />}
164
155
  </button>
165
- ))
166
- ) : (
167
- <p className="px-3 py-4 text-sm text-slate-500">No items found.</p>
168
- )}
169
- </div>
170
- <div className="flex items-center justify-between gap-2 border-t border-slate-100 px-4 py-3">
171
- <span className="text-xs font-semibold uppercase tracking-wide text-slate-400">
172
- Quick actions
173
- </span>
174
- <button
175
- type="button"
176
- onClick={() => {
177
- onSelect('All');
178
- setIsOverflowOpen(false);
179
- }}
180
- className="text-xs font-semibold uppercase tracking-wide text-primary-600 hover:text-primary-700"
181
- >
182
- Reset to All
183
- </button>
156
+ );
157
+ })}
184
158
  </div>
185
159
  </motion.div>
186
160
  )}
@@ -2,36 +2,50 @@
2
2
 
3
3
  import React, { useState } from 'react';
4
4
  import { motion, AnimatePresence } from 'framer-motion';
5
- import { ShoppingCart, User, Menu, X, Search, Heart } from 'lucide-react';
5
+ import { ShoppingCart, User, Menu, X, Search, Heart, ChevronDown, Settings, LogOut } from 'lucide-react';
6
6
  import { useAuth } from '@/providers/AuthProvider';
7
7
  import { useCart } from '@/providers/CartProvider';
8
8
  import { useTheme } from '@/providers/ThemeProvider';
9
9
  import { useWishlist } from '@/providers/WishlistProvider';
10
10
  import { useBasePath } from '@/providers/BasePathProvider';
11
+ import { getInitials } from '@/lib/utils/format';
12
+ import { useRouter } from 'next/navigation';
11
13
  import Link from 'next/link';
12
14
  import Image from 'next/image';
13
15
 
14
16
  export function Header() {
15
17
  const { config } = useTheme();
16
- const { user, isAuthenticated } = useAuth();
18
+ const { user, isAuthenticated, logout } = useAuth();
17
19
  const { cart } = useCart() || { cart: { itemCount: 0 } };
18
20
  const { getWishlistCount } = useWishlist();
19
21
  const wishlistCount = getWishlistCount?.() || 0;
20
22
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
21
23
  const [isSearchOpen, setIsSearchOpen] = useState(false);
24
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
25
+ const [isLoggingOut, setIsLoggingOut] = useState(false);
22
26
  const [searchQuery, setSearchQuery] = useState('');
23
27
  const { buildPath } = useBasePath();
28
+ const router = useRouter();
29
+
30
+ const handleLogout = async () => {
31
+ setIsLoggingOut(true);
32
+ try {
33
+ await logout();
34
+ router.push(buildPath('/'));
35
+ } catch (error) {
36
+ console.error('Logout failed:', error);
37
+ } finally {
38
+ setIsLoggingOut(false);
39
+ setIsDropdownOpen(false);
40
+ }
41
+ };
24
42
 
25
43
  const navLinks = [
26
44
  { href: buildPath('/shop'), label: 'Shop' },
27
- { href: buildPath('/categories'), label: 'Categories' },
28
- { href: buildPath('/orders'), label: 'Orders' },
29
- { href: buildPath('/about'), label: 'About' },
30
- { href: buildPath('/contact'), label: 'Contact' },
31
45
  ];
32
46
 
33
47
  return (
34
- <header className="sticky top-0 z-40 bg-white/80 backdrop-blur-xl border-b border-gray-200 shadow-sm">
48
+ <header className="sticky top-0 z-10 bg-white/80 backdrop-blur-xl border-b border-gray-200 shadow-sm">
35
49
  <div className="container mx-auto px-4">
36
50
  <div className="flex items-center justify-between h-20">
37
51
  {/* Logo */}
@@ -44,9 +58,6 @@ export function Header() {
44
58
  className="object-contain"
45
59
  />
46
60
  </div>
47
- <span className="text-2xl font-bold text-gray-900 hidden sm:block">
48
- {config.storeName}
49
- </span>
50
61
  </Link>
51
62
 
52
63
  {/* Desktop Navigation */}
@@ -134,12 +145,54 @@ export function Header() {
134
145
 
135
146
  {/* User Menu */}
136
147
  {isAuthenticated ? (
137
- <Link
138
- href={buildPath('/account')}
139
- className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
140
- >
141
- <User className="w-6 h-6 text-gray-700" />
142
- </Link>
148
+ <div className="relative">
149
+ <button
150
+ onClick={() => setIsDropdownOpen(!isDropdownOpen)}
151
+ className="flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-2 hover:bg-slate-50 transition-colors"
152
+ >
153
+ <div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary-100 text-xs font-semibold text-primary-700">
154
+ {getInitials(user?.firstname || '', user?.lastname || '')}
155
+ </div>
156
+ <ChevronDown className={`h-4 w-4 text-slate-400 transition-transform ${isDropdownOpen ? 'rotate-180' : ''}`} />
157
+ </button>
158
+
159
+ {/* Dropdown Menu */}
160
+ {isDropdownOpen && (
161
+ <>
162
+ <div
163
+ className="fixed inset-0 z-10"
164
+ onClick={() => setIsDropdownOpen(false)}
165
+ />
166
+ <div className="absolute right-0 top-full mt-2 w-56 rounded-lg border border-slate-200 bg-white shadow-lg z-20">
167
+ <div className="p-2">
168
+ <div className="px-3 py-2 border-b border-slate-200 mb-1">
169
+ <p className="text-sm font-medium text-secondary truncate">
170
+ {user?.firstname} {user?.lastname}
171
+ </p>
172
+ <p className="text-xs text-slate-500 truncate">{user?.email}</p>
173
+ </div>
174
+ <Link
175
+ href={buildPath('/account')}
176
+ onClick={() => setIsDropdownOpen(false)}
177
+ className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm text-slate-700 hover:bg-slate-100 transition-colors"
178
+ >
179
+ <User className="h-4 w-4" />
180
+ My Account
181
+ </Link>
182
+ <div className="my-1 border-t border-slate-200" />
183
+ <button
184
+ onClick={handleLogout}
185
+ disabled={isLoggingOut}
186
+ className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors disabled:opacity-50"
187
+ >
188
+ <LogOut className="h-4 w-4" />
189
+ {isLoggingOut ? 'Signing out...' : 'Sign Out'}
190
+ </button>
191
+ </div>
192
+ </div>
193
+ </>
194
+ )}
195
+ </div>
143
196
  ) : (
144
197
  <Link
145
198
  href={buildPath('/login')}
@@ -0,0 +1,148 @@
1
+ 'use client';
2
+
3
+ import React, { useEffect, useState } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { CheckCircle2, XCircle, AlertCircle, Info, X } from 'lucide-react';
6
+
7
+ export type NotificationType = 'success' | 'error' | 'warning' | 'info';
8
+
9
+ export interface NotificationData {
10
+ id: string;
11
+ type: NotificationType;
12
+ message: string;
13
+ description?: string;
14
+ duration?: number;
15
+ }
16
+
17
+ interface NotificationProps {
18
+ notification: NotificationData;
19
+ onDismiss: (id: string) => void;
20
+ }
21
+
22
+ const notificationConfig = {
23
+ success: {
24
+ icon: CheckCircle2,
25
+ gradient: 'from-emerald-500 to-green-600',
26
+ iconColor: 'text-emerald-600',
27
+ bgColor: 'bg-emerald-50',
28
+ borderColor: 'border-emerald-200',
29
+ },
30
+ error: {
31
+ icon: XCircle,
32
+ gradient: 'from-red-500 to-rose-600',
33
+ iconColor: 'text-red-600',
34
+ bgColor: 'bg-red-50',
35
+ borderColor: 'border-red-200',
36
+ },
37
+ warning: {
38
+ icon: AlertCircle,
39
+ gradient: 'from-orange-500 to-amber-600',
40
+ iconColor: 'text-orange-600',
41
+ bgColor: 'bg-orange-50',
42
+ borderColor: 'border-orange-200',
43
+ },
44
+ info: {
45
+ icon: Info,
46
+ gradient: 'from-blue-500 to-indigo-600',
47
+ iconColor: 'text-blue-600',
48
+ bgColor: 'bg-blue-50',
49
+ borderColor: 'border-blue-200',
50
+ },
51
+ };
52
+
53
+ export function Notification({ notification, onDismiss }: NotificationProps) {
54
+ const [progress, setProgress] = useState(100);
55
+ const config = notificationConfig[notification.type];
56
+ const Icon = config.icon;
57
+ const duration = notification.duration || 4000;
58
+
59
+ useEffect(() => {
60
+ const startTime = Date.now();
61
+ const timer = setInterval(() => {
62
+ const elapsed = Date.now() - startTime;
63
+ const remaining = Math.max(0, 100 - (elapsed / duration) * 100);
64
+ setProgress(remaining);
65
+
66
+ if (remaining === 0) {
67
+ clearInterval(timer);
68
+ onDismiss(notification.id);
69
+ }
70
+ }, 16);
71
+
72
+ return () => clearInterval(timer);
73
+ }, [notification.id, duration, onDismiss]);
74
+
75
+ return (
76
+ <motion.div
77
+ initial={{ opacity: 0, y: -20, scale: 0.95 }}
78
+ animate={{ opacity: 1, y: 0, scale: 1 }}
79
+ exit={{ opacity: 0, x: 100, scale: 0.95 }}
80
+ transition={{ type: 'spring', stiffness: 500, damping: 30 }}
81
+ className={`relative bg-white rounded-2xl border-2 ${config.borderColor} shadow-xl overflow-hidden min-w-[320px] max-w-[420px]`}
82
+ >
83
+ {/* Gradient accent bar */}
84
+ <div className={`h-1 bg-gradient-to-r ${config.gradient}`} />
85
+
86
+ <div className="p-4 flex items-start gap-3">
87
+ {/* Icon */}
88
+ <motion.div
89
+ initial={{ scale: 0, rotate: -180 }}
90
+ animate={{ scale: 1, rotate: 0 }}
91
+ transition={{ delay: 0.1, type: 'spring', stiffness: 500 }}
92
+ className={`size-10 rounded-full ${config.bgColor} flex items-center justify-center shrink-0`}
93
+ >
94
+ <Icon className={`size-5 ${config.iconColor}`} />
95
+ </motion.div>
96
+
97
+ {/* Content */}
98
+ <div className="flex-1 min-w-0">
99
+ <p className="font-['Poppins',sans-serif] font-semibold text-[14px] text-[#2B4B7C] mb-1">
100
+ {notification.message}
101
+ </p>
102
+ {notification.description && (
103
+ <p className="font-['Poppins',sans-serif] text-[12px] text-[#676c80] leading-relaxed">
104
+ {notification.description}
105
+ </p>
106
+ )}
107
+ </div>
108
+
109
+ {/* Close button */}
110
+ <button
111
+ onClick={() => onDismiss(notification.id)}
112
+ className="p-1.5 hover:bg-gray-100 rounded-full transition-colors shrink-0"
113
+ aria-label="Dismiss notification"
114
+ >
115
+ <X className="size-4 text-[#676c80]" />
116
+ </button>
117
+ </div>
118
+
119
+ {/* Progress bar */}
120
+ <div className="h-1 bg-gray-100">
121
+ <motion.div
122
+ className={`h-full bg-gradient-to-r ${config.gradient}`}
123
+ style={{ width: `${progress}%` }}
124
+ transition={{ duration: 0.016, ease: 'linear' }}
125
+ />
126
+ </div>
127
+ </motion.div>
128
+ );
129
+ }
130
+
131
+ interface NotificationContainerProps {
132
+ notifications: NotificationData[];
133
+ onDismiss: (id: string) => void;
134
+ }
135
+
136
+ export function NotificationContainer({ notifications, onDismiss }: NotificationContainerProps) {
137
+ return (
138
+ <div className="fixed top-4 right-4 z-[9999] flex flex-col gap-3 pointer-events-none">
139
+ <AnimatePresence mode="popLayout">
140
+ {notifications.map((notification) => (
141
+ <div key={notification.id} className="pointer-events-auto">
142
+ <Notification notification={notification} onDismiss={onDismiss} />
143
+ </div>
144
+ ))}
145
+ </AnimatePresence>
146
+ </div>
147
+ );
148
+ }
@@ -2,91 +2,124 @@
2
2
 
3
3
  import React from 'react';
4
4
  import { motion } from 'framer-motion';
5
- import { Package, Calendar, CreditCard, ExternalLink } from 'lucide-react';
5
+ import { CreditCard } 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
- import Link from 'next/link';
10
9
  import Image from 'next/image';
11
- import { useBasePath } from '@/providers/BasePathProvider';
12
10
 
13
11
  interface OrderCardProps {
14
12
  order: PopulatedOrder;
15
13
  }
16
14
 
17
15
  export function OrderCard({ order }: OrderCardProps) {
18
- const { buildPath } = useBasePath();
19
16
  const config = order.orderStatus;
17
+ const itemCount = order.items?.length || 0;
18
+ const showPriceBreakdown = (order.shippingCost && order.shippingCost > 0) ||
19
+ (order.tax && order.tax > 0) ||
20
+ (order.discountedAmount && order.discountedAmount > 0);
20
21
 
21
22
  return (
22
23
  <motion.div
23
24
  initial={{ opacity: 0, y: 20 }}
24
25
  animate={{ opacity: 1, y: 0 }}
25
- whileHover={{ y: -4 }}
26
- className="bg-white rounded-2xl p-6 shadow-sm hover:shadow-xl transition-all duration-300 border border-gray-100"
26
+ className="rounded-lg border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow"
27
27
  >
28
- {/* Header */}
29
- <div className="flex justify-between items-start mb-4">
30
- <div>
31
- <h3 className="text-lg font-bold text-gray-900 flex items-center gap-2">
32
- <Package className="w-5 h-5 text-primary-600" />
33
- Order #{order?._id?.slice(0, 6) || ''}
28
+ {/* Header - Compact */}
29
+ <div className="flex items-center justify-between mb-4 pb-4 border-b border-gray-200">
30
+ <div className="flex items-center gap-3">
31
+ <h3 className="text-base font-bold text-slate-900">
32
+ Order #{order?._id?.slice(0, 8) || ''}
34
33
  </h3>
35
- <p className="text-sm text-gray-500 mt-1 flex items-center gap-2">
36
- <Calendar className="w-4 h-4" />
37
- {formatDate(order.createdAt || new Date(), 'long')}
38
- </p>
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>
38
+ </div>
39
+ <div className="text-right">
40
+ <p className="text-lg font-bold text-slate-900">{formatPrice(order.grandTotal || 0)}</p>
41
+ {itemCount > 0 && (
42
+ <p className="text-xs text-gray-500">{itemCount} {itemCount === 1 ? 'item' : 'items'}</p>
43
+ )}
39
44
  </div>
40
- <Badge variant={config as 'success' | 'warning' | 'primary' | 'danger' | 'gray'}>{config}</Badge>
41
45
  </div>
42
46
 
43
- {/* Items Preview */}
47
+ {/* Items List - Compact */}
44
48
  <div className="space-y-2 mb-4">
45
- {order.items?.slice(0, 2).map((item) => (
46
- <div key={item.productVariantId} className="flex items-center gap-3 text-sm">
47
- <Image src={item?.productVariantData?.productMedia?.[0]?.file || '/placeholder-product.jpg'} alt={item?.productVariantData?.name || 'Product image'} width={48} height={48} className="w-12 h-12 rounded-lg bg-gray-100 flex-shrink-0" />
48
- <div className="flex-1 min-w-0">
49
- <p className="font-medium text-gray-900 truncate">{item.productVariantData.name}</p>
50
- <p className="text-gray-500">Qty: {item.quantity}</p>
51
- </div>
52
- <p className="font-semibold text-gray-900">{formatPrice(item.productVariantData.finalPrice)}</p>
53
- </div>
54
- ))}
55
- {order.items?.length && order.items?.length > 2 && (
56
- <p className="text-sm text-gray-500 pl-15">
57
- +{order.items.length - 2} more item{order.items.length - 2 > 1 ? 's' : ''}
49
+ {order.items && order.items.length > 0 ? (
50
+ order.items.slice(0, 3).map((item) => {
51
+ const itemPrice = item.productVariantData?.finalPrice || 0;
52
+ const itemTotal = itemPrice * item.quantity;
53
+
54
+ return (
55
+ <div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
56
+ <div className="relative w-12 h-12 rounded bg-gray-100 flex-shrink-0 overflow-hidden">
57
+ <Image
58
+ src={item?.productVariantData?.productMedia?.[0]?.file || '/placeholder-product.jpg'}
59
+ alt={item?.productVariantData?.name || 'Product image'}
60
+ fill
61
+ className="object-cover"
62
+ sizes="48px"
63
+ />
64
+ </div>
65
+ <div className="flex-1 min-w-0">
66
+ <p className="font-medium text-slate-900 truncate text-sm">
67
+ {item.productVariantData?.name || 'Unknown Product'}
68
+ </p>
69
+ <p className="text-xs text-gray-500">Qty: {item.quantity}</p>
70
+ </div>
71
+ <p className="font-semibold text-slate-900 text-sm">{formatPrice(itemTotal)}</p>
72
+ </div>
73
+ );
74
+ })
75
+ ) : (
76
+ <p className="text-sm text-gray-500 text-center py-2">No items found</p>
77
+ )}
78
+ {order.items && order.items.length > 3 && (
79
+ <p className="text-xs text-gray-500 text-center pt-1">
80
+ +{order.items.length - 3} more {order.items.length - 3 === 1 ? 'item' : 'items'}
58
81
  </p>
59
82
  )}
60
83
  </div>
61
84
 
62
- {/* Footer */}
63
- <div className="flex justify-between items-center pt-4 border-t border-gray-200">
64
- <div>
65
- <p className="text-sm text-gray-500">Total Amount</p>
66
- <p className="text-2xl font-bold text-gray-900">{formatPrice(order.grandTotal || 0)}</p>
67
- </div>
68
-
69
- <div className="flex gap-2">
70
- {order.payment.paymentStatus !== PaymentPaymentStatusEnum.Paid && order.payment.paymentMethod === PaymentPaymentMethodEnum.Card && (
71
- <a
72
- href={order?.payment?.paymentIntent?.hostedInvoiceUrl || ''}
73
- target="_blank"
74
- rel="noopener noreferrer"
75
- className="inline-flex items-center gap-2 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
76
- >
77
- <CreditCard className="w-4 h-4" />
78
- Pay Now
79
- </a>
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>
80
93
  )}
81
- {/* <Link
82
- href={buildPath(`/orders/${order.id}`)}
83
- className="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
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>
99
+ )}
100
+ {order.discountedAmount !== undefined && order.discountedAmount > 0 && (
101
+ <div className="flex justify-between text-green-600">
102
+ <span>Discount</span>
103
+ <span>-{formatPrice(order.discountedAmount)}</span>
104
+ </div>
105
+ )}
106
+ </div>
107
+ )}
108
+
109
+ {/* Footer Actions */}
110
+ {order.payment?.paymentStatus !== PaymentPaymentStatusEnum.Paid && order.payment?.paymentMethod === PaymentPaymentMethodEnum.Card && (
111
+ <div className="flex justify-end">
112
+ <button
113
+ onClick={() => {
114
+ window.open(order?.payment?.hostedInvoiceUrl || '', '_blank')
115
+ }}
116
+ className="inline-flex items-center gap-2 rounded-full border-2 border-primary-500 bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 text-sm font-medium transition-colors"
84
117
  >
85
- View Details
86
- <ExternalLink className="w-4 h-4" />
87
- </Link> */}
118
+ <CreditCard className="w-4 h-4" />
119
+ Pay Now
120
+ </button>
88
121
  </div>
89
- </div>
122
+ )}
90
123
  </motion.div>
91
124
  );
92
125
  }