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.
- package/dist/index.d.mts +2 -4
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1123 -972
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1123 -971
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AccountAddressesTab.tsx +209 -0
- package/src/components/AccountOrdersTab.tsx +151 -0
- package/src/components/AccountOverviewTab.tsx +209 -0
- package/src/components/AccountPaymentTab.tsx +116 -0
- package/src/components/AccountSavedItemsTab.tsx +76 -0
- package/src/components/AccountSettingsTab.tsx +116 -0
- package/src/components/AddressFormModal.tsx +23 -10
- package/src/components/CartItem.tsx +60 -56
- package/src/components/FilterChips.tsx +54 -80
- package/src/components/Header.tsx +69 -16
- package/src/components/Notification.tsx +148 -0
- package/src/components/OrderCard.tsx +89 -56
- package/src/components/ProductCard.tsx +215 -178
- package/src/components/QuickViewModal.tsx +314 -0
- package/src/components/TabNavigation.tsx +48 -0
- package/src/components/ui/Button.tsx +1 -1
- package/src/components/ui/ConfirmModal.tsx +84 -0
- package/src/hooks/useOrders.ts +1 -0
- package/src/hooks/usePaymentMethods.ts +58 -0
- package/src/index.ts +0 -1
- package/src/providers/CartProvider.tsx +22 -6
- package/src/providers/EcommerceProvider.tsx +8 -7
- package/src/providers/FavoritesProvider.tsx +10 -3
- package/src/providers/NotificationProvider.tsx +79 -0
- package/src/providers/WishlistProvider.tsx +34 -9
- package/src/screens/AddressesScreen.tsx +72 -61
- package/src/screens/CartScreen.tsx +48 -32
- package/src/screens/ChangePasswordScreen.tsx +155 -0
- package/src/screens/CheckoutScreen.tsx +162 -125
- package/src/screens/EditProfileScreen.tsx +165 -0
- package/src/screens/LoginScreen.tsx +59 -72
- package/src/screens/NewAddressScreen.tsx +16 -10
- package/src/screens/OrdersScreen.tsx +91 -148
- package/src/screens/ProductDetailScreen.tsx +334 -234
- package/src/screens/ProfileScreen.tsx +190 -200
- package/src/screens/RegisterScreen.tsx +51 -70
- package/src/screens/SearchResultsScreen.tsx +2 -1
- package/src/screens/ShopScreen.tsx +260 -384
- package/src/screens/WishlistScreen.tsx +226 -224
- package/src/styles/globals.css +9 -0
- package/src/screens/CategoriesScreen.tsx +0 -122
- 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,
|
|
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="
|
|
86
|
-
<
|
|
87
|
-
<Icon className="h-4 w-4" />
|
|
88
|
-
{label}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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-
|
|
98
|
-
|
|
99
|
-
?
|
|
100
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
?
|
|
116
|
-
|
|
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
|
-
<
|
|
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
|
|
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="
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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-
|
|
157
|
-
|
|
158
|
-
?
|
|
159
|
-
|
|
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
|
-
{
|
|
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-
|
|
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
|
-
<
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
30
|
-
<div>
|
|
31
|
-
<h3 className="text-
|
|
32
|
-
|
|
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
|
-
<
|
|
36
|
-
|
|
37
|
-
{formatDate(order.createdAt || new Date(), '
|
|
38
|
-
</
|
|
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
|
|
47
|
+
{/* Items List - Compact */}
|
|
44
48
|
<div className="space-y-2 mb-4">
|
|
45
|
-
{order.items
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
{/*
|
|
63
|
-
|
|
64
|
-
<div>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
</
|
|
118
|
+
<CreditCard className="w-4 h-4" />
|
|
119
|
+
Pay Now
|
|
120
|
+
</button>
|
|
88
121
|
</div>
|
|
89
|
-
|
|
122
|
+
)}
|
|
90
123
|
</motion.div>
|
|
91
124
|
);
|
|
92
125
|
}
|