hey-pharmacist-ecommerce 1.1.13 → 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 +1039 -857
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1039 -856
- 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/Header.tsx +69 -16
- package/src/components/Notification.tsx +148 -0
- 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/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/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
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { CreditCard, Plus, Trash2 } from 'lucide-react';
|
|
5
|
+
import { usePaymentMethods } from '@/hooks/usePaymentMethods';
|
|
6
|
+
import { EmptyState } from './EmptyState';
|
|
7
|
+
import { Button } from './ui/Button';
|
|
8
|
+
import { Badge } from './ui/Badge';
|
|
9
|
+
|
|
10
|
+
export function AccountPaymentTab() {
|
|
11
|
+
const { paymentMethods, isLoading, error, deletePaymentMethod } = usePaymentMethods();
|
|
12
|
+
const [deletingId, setDeletingId] = useState<string | null>(null);
|
|
13
|
+
|
|
14
|
+
const handleDelete = async (paymentMethodId: string) => {
|
|
15
|
+
if (!confirm('Are you sure you want to remove this payment method?')) return;
|
|
16
|
+
|
|
17
|
+
setDeletingId(paymentMethodId);
|
|
18
|
+
try {
|
|
19
|
+
await deletePaymentMethod(paymentMethodId);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Failed to delete payment method:', error);
|
|
22
|
+
alert('Failed to delete payment method. Please try again.');
|
|
23
|
+
} finally {
|
|
24
|
+
setDeletingId(null);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (isLoading) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="p-6">
|
|
31
|
+
<div className="space-y-4">
|
|
32
|
+
{Array.from({ length: 2 }).map((_, index) => (
|
|
33
|
+
<div
|
|
34
|
+
key={index}
|
|
35
|
+
className="h-24 animate-pulse rounded-lg border border-slate-100 bg-slate-50"
|
|
36
|
+
/>
|
|
37
|
+
))}
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
return (
|
|
45
|
+
<div className="p-6">
|
|
46
|
+
<div className="rounded-lg border border-red-100 bg-red-50 p-6 text-sm text-red-700">
|
|
47
|
+
{error.message}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="p-6">
|
|
55
|
+
<div className="mb-6 flex items-center justify-between">
|
|
56
|
+
<div>
|
|
57
|
+
<h3 className="text-lg font-semibold text-slate-900">Payment Methods</h3>
|
|
58
|
+
<p className="text-sm text-slate-600">Manage your saved payment methods</p>
|
|
59
|
+
</div>
|
|
60
|
+
<Button variant="primary" size="sm">
|
|
61
|
+
<Plus className="h-4 w-4" />
|
|
62
|
+
Add Card
|
|
63
|
+
</Button>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{paymentMethods.length === 0 ? (
|
|
67
|
+
<EmptyState
|
|
68
|
+
icon={CreditCard}
|
|
69
|
+
title="No payment methods"
|
|
70
|
+
description="Add a payment method to make checkout faster and easier."
|
|
71
|
+
actionLabel="Add your first card"
|
|
72
|
+
onAction={() => {
|
|
73
|
+
// TODO: Implement add payment method modal
|
|
74
|
+
alert('Add payment method functionality coming soon');
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
) : (
|
|
78
|
+
<div className="space-y-4">
|
|
79
|
+
{paymentMethods.map((method) => (
|
|
80
|
+
<div
|
|
81
|
+
key={method.id}
|
|
82
|
+
className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 hover:shadow-md transition-shadow"
|
|
83
|
+
>
|
|
84
|
+
<div className="flex items-center gap-4">
|
|
85
|
+
<div className="flex h-12 w-12 items-center justify-center rounded-lg bg-primary-50">
|
|
86
|
+
<CreditCard className="h-6 w-6 text-primary-600" />
|
|
87
|
+
</div>
|
|
88
|
+
{/* <div>
|
|
89
|
+
<div className="flex items-center gap-2">
|
|
90
|
+
<p className="font-medium text-slate-900">
|
|
91
|
+
{method.card?.brand || 'Card'} •••• {method.card?.last4 || '****'}
|
|
92
|
+
</p>
|
|
93
|
+
{method.isDefault && (
|
|
94
|
+
<Badge variant="success">Default</Badge>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<p className="text-sm text-slate-500">
|
|
98
|
+
Expires {method.card?.expMonth}/{method.card?.expYear}
|
|
99
|
+
</p>
|
|
100
|
+
</div> */}
|
|
101
|
+
</div>
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => handleDelete(method.id)}
|
|
104
|
+
disabled={deletingId === method.id}
|
|
105
|
+
className="inline-flex items-center gap-2 rounded-full border border-red-200 px-3 py-1 text-sm font-semibold text-red-600 transition hover:border-red-300 hover:text-red-700 disabled:opacity-50"
|
|
106
|
+
>
|
|
107
|
+
<Trash2 className="h-4 w-4" />
|
|
108
|
+
{deletingId === method.id ? 'Deleting...' : 'Delete'}
|
|
109
|
+
</button>
|
|
110
|
+
</div>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { Heart, ShoppingCart } from 'lucide-react';
|
|
5
|
+
import { useWishlist } from '@/providers/WishlistProvider';
|
|
6
|
+
import { useWishlistProducts } from '@/hooks/useWishlistProducts';
|
|
7
|
+
import { ProductCard } from './ProductCard';
|
|
8
|
+
import { EmptyState } from './EmptyState';
|
|
9
|
+
|
|
10
|
+
export function AccountSavedItemsTab() {
|
|
11
|
+
const { products: wishlistProductIds } = useWishlist();
|
|
12
|
+
|
|
13
|
+
const { products: wishlistProducts, isLoading, error } = useWishlistProducts(
|
|
14
|
+
wishlistProductIds as string[]
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const products = wishlistProducts || [];
|
|
18
|
+
console.log(products);
|
|
19
|
+
|
|
20
|
+
if (isLoading) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="p-6">
|
|
23
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
24
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
25
|
+
<div
|
|
26
|
+
key={index}
|
|
27
|
+
className="h-80 animate-pulse rounded-lg border border-slate-100 bg-slate-50"
|
|
28
|
+
/>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (error) {
|
|
36
|
+
return (
|
|
37
|
+
<div className="p-6">
|
|
38
|
+
<div className="rounded-lg border border-red-100 bg-red-50 p-6 text-sm text-red-700">
|
|
39
|
+
{error.message}
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (products.length === 0) {
|
|
46
|
+
return (
|
|
47
|
+
<div className="p-6">
|
|
48
|
+
<EmptyState
|
|
49
|
+
icon={Heart}
|
|
50
|
+
title="No saved items"
|
|
51
|
+
description="Items you save will appear here for easy access later."
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="pb-24 p-6">
|
|
59
|
+
<div className="p-6 bg-white rounded-xl">
|
|
60
|
+
<div className="mb-4">
|
|
61
|
+
<h3 className="text-lg font-semibold text-secondary">
|
|
62
|
+
Saved for Later
|
|
63
|
+
</h3>
|
|
64
|
+
<p className="text-sm text-secondary">
|
|
65
|
+
{products.length} {products.length === 1 ? 'item' : 'items'} saved
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
69
|
+
{products.map((product) => (
|
|
70
|
+
<ProductCard key={product._id} product={product} />
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
import { Bell, Lock, Trash2 } from 'lucide-react';
|
|
5
|
+
import { Button } from './ui/Button';
|
|
6
|
+
import { useRouter } from 'next/navigation';
|
|
7
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
8
|
+
|
|
9
|
+
export function AccountSettingsTab() {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
const { buildPath } = useBasePath();
|
|
12
|
+
const [emailNotifications, setEmailNotifications] = useState(true);
|
|
13
|
+
const [orderUpdates, setOrderUpdates] = useState(true);
|
|
14
|
+
const [promotionalEmails, setPromotionalEmails] = useState(false);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="p-6 space-y-6">
|
|
18
|
+
{/* Account Preferences */}
|
|
19
|
+
<div className="rounded-2xl border border-slate-200 bg-white p-6">
|
|
20
|
+
<div className="flex items-center gap-2 mb-4">
|
|
21
|
+
<Bell className="h-5 w-5 text-secondary" />
|
|
22
|
+
<h3 className="text-lg font-semibold text-secondary">Account Preferences</h3>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="space-y-4">
|
|
25
|
+
<label className="flex items-center justify-between gap-4 rounded-lg border border-slate-200 bg-slate-50 px-4 py-3">
|
|
26
|
+
<div>
|
|
27
|
+
<p className="text-sm font-medium text-secondary">Email Notifications</p>
|
|
28
|
+
<p className="text-xs text-muted">Receive updates about your orders</p>
|
|
29
|
+
</div>
|
|
30
|
+
<input
|
|
31
|
+
type="checkbox"
|
|
32
|
+
checked={emailNotifications}
|
|
33
|
+
onChange={(e) => setEmailNotifications(e.target.checked)}
|
|
34
|
+
className="h-4 w-4 rounded border-primary-300 text-secondary focus:ring-primary-500"
|
|
35
|
+
/>
|
|
36
|
+
</label>
|
|
37
|
+
<label className="flex items-center justify-between gap-4 rounded-lg border border-slate-200 bg-slate-50 px-4 py-3">
|
|
38
|
+
<div>
|
|
39
|
+
<p className="text-sm font-medium text-secondary">Order Updates</p>
|
|
40
|
+
<p className="text-xs text-muted">Get notified about shipping status</p>
|
|
41
|
+
</div>
|
|
42
|
+
<input
|
|
43
|
+
type="checkbox"
|
|
44
|
+
checked={orderUpdates}
|
|
45
|
+
onChange={(e) => setOrderUpdates(e.target.checked)}
|
|
46
|
+
className="h-4 w-4 rounded border-primary-300 text-secondary focus:ring-primary-500"
|
|
47
|
+
/>
|
|
48
|
+
</label>
|
|
49
|
+
<label className="flex items-center justify-between gap-4 rounded-lg border border-slate-200 bg-slate-50 px-4 py-3">
|
|
50
|
+
<div>
|
|
51
|
+
<p className="text-sm font-medium text-secondary">Promotional Emails</p>
|
|
52
|
+
<p className="text-xs text-muted">Receive special offers and discounts</p>
|
|
53
|
+
</div>
|
|
54
|
+
<input
|
|
55
|
+
type="checkbox"
|
|
56
|
+
checked={promotionalEmails}
|
|
57
|
+
onChange={(e) => setPromotionalEmails(e.target.checked)}
|
|
58
|
+
className="h-4 w-4 rounded border-primary-300 text-secondary focus:ring-primary-500"
|
|
59
|
+
/>
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Security */}
|
|
65
|
+
<div className="rounded-2xl border border-slate-200 bg-white p-6">
|
|
66
|
+
<div className="flex items-center gap-2 mb-4">
|
|
67
|
+
<Lock className="h-5 w-5 text-secondary" />
|
|
68
|
+
<h3 className="text-lg font-semibold text-secondary">Security</h3>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="space-y-3">
|
|
71
|
+
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-4 py-3">
|
|
72
|
+
<div>
|
|
73
|
+
<p className="text-sm font-medium text-secondary">Change Password</p>
|
|
74
|
+
<p className="text-xs text-muted">Update your account password</p>
|
|
75
|
+
</div>
|
|
76
|
+
<Button
|
|
77
|
+
variant="outline"
|
|
78
|
+
size="sm"
|
|
79
|
+
onClick={() => router.push(buildPath('/account/change-password'))}
|
|
80
|
+
>
|
|
81
|
+
Change
|
|
82
|
+
</Button>
|
|
83
|
+
</div>
|
|
84
|
+
{/* <div className="flex items-center justify-between rounded-lg border border-slate-200 bg-slate-50 px-4 py-3">
|
|
85
|
+
<div>
|
|
86
|
+
<p className="text-sm font-medium text-secondary">Two-Factor Authentication</p>
|
|
87
|
+
<p className="text-xs text-muted">Add an extra layer of security</p>
|
|
88
|
+
</div>
|
|
89
|
+
<Button variant="outline" size="sm">
|
|
90
|
+
Enable
|
|
91
|
+
</Button>
|
|
92
|
+
</div> */}
|
|
93
|
+
<div className="flex items-center justify-between rounded-lg border border-red-100 bg-red-50 px-4 py-3">
|
|
94
|
+
<div>
|
|
95
|
+
<p className="text-sm font-medium text-red-900">Delete Account</p>
|
|
96
|
+
<p className="text-xs text-red-600">Permanently delete your account and data</p>
|
|
97
|
+
</div>
|
|
98
|
+
<Button
|
|
99
|
+
variant="outline"
|
|
100
|
+
size="sm"
|
|
101
|
+
className="border-red-200 text-red-600 hover:bg-red-50"
|
|
102
|
+
onClick={() => {
|
|
103
|
+
if (confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
|
|
104
|
+
alert('Account deletion functionality coming soon');
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Trash2 className="h-4 w-4" />
|
|
109
|
+
Delete
|
|
110
|
+
</Button>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -8,7 +8,7 @@ import { addressSchema, type AddressFormData } from '@/lib/validations/address';
|
|
|
8
8
|
import { AddressesApi } from '@/lib/Apis/apis/addresses-api';
|
|
9
9
|
import { Address } from '@/lib/Apis';
|
|
10
10
|
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
11
|
-
import {
|
|
11
|
+
import { useNotification } from '@/providers/NotificationProvider';
|
|
12
12
|
|
|
13
13
|
interface AddressFormModalProps {
|
|
14
14
|
isOpen: boolean;
|
|
@@ -20,6 +20,7 @@ interface AddressFormModalProps {
|
|
|
20
20
|
|
|
21
21
|
export function AddressFormModal({ isOpen, onClose, onAddressAdded, onAddressUpdated, initialAddress }: AddressFormModalProps) {
|
|
22
22
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
23
|
+
const notification = useNotification();
|
|
23
24
|
|
|
24
25
|
const {
|
|
25
26
|
register,
|
|
@@ -69,7 +70,10 @@ export function AddressFormModal({ isOpen, onClose, onAddressAdded, onAddressUpd
|
|
|
69
70
|
country: data.country,
|
|
70
71
|
phone: data.phone,
|
|
71
72
|
}, initialAddress.id);
|
|
72
|
-
|
|
73
|
+
notification.success(
|
|
74
|
+
'Address updated',
|
|
75
|
+
'Your address has been updated successfully.'
|
|
76
|
+
);
|
|
73
77
|
reset();
|
|
74
78
|
onClose();
|
|
75
79
|
if (onAddressUpdated) onAddressUpdated(response.data);
|
|
@@ -85,16 +89,25 @@ export function AddressFormModal({ isOpen, onClose, onAddressAdded, onAddressUpd
|
|
|
85
89
|
phone: data.phone,
|
|
86
90
|
});
|
|
87
91
|
if (response.status === 201) {
|
|
88
|
-
|
|
92
|
+
notification.success(
|
|
93
|
+
'Address added',
|
|
94
|
+
'Your new address has been saved to your account.'
|
|
95
|
+
);
|
|
89
96
|
reset();
|
|
90
97
|
onClose();
|
|
91
98
|
if (onAddressAdded) onAddressAdded(response.data);
|
|
92
99
|
} else {
|
|
93
|
-
|
|
100
|
+
notification.error(
|
|
101
|
+
'Could not add address',
|
|
102
|
+
'We could not save this address. Please try again.'
|
|
103
|
+
);
|
|
94
104
|
}
|
|
95
105
|
}
|
|
96
106
|
} catch (error) {
|
|
97
|
-
|
|
107
|
+
notification.error(
|
|
108
|
+
'Could not save address',
|
|
109
|
+
'Something went wrong while saving your address. Please try again.'
|
|
110
|
+
);
|
|
98
111
|
} finally {
|
|
99
112
|
setIsSubmitting(false);
|
|
100
113
|
}
|
|
@@ -162,16 +175,16 @@ export function AddressFormModal({ isOpen, onClose, onAddressAdded, onAddressUpd
|
|
|
162
175
|
/>
|
|
163
176
|
</div>
|
|
164
177
|
<div className="flex justify-end gap-4">
|
|
165
|
-
<
|
|
178
|
+
<button
|
|
166
179
|
type="button"
|
|
167
|
-
variant="outline"
|
|
168
180
|
onClick={onClose}
|
|
181
|
+
className='flex flex-row items-center gap-2 px-6 py-2 border border-slate-200 rounded-xl text-slate-700 hover:opacity-80 transition-colors text-secondary text-sm'
|
|
169
182
|
>
|
|
170
183
|
Cancel
|
|
171
|
-
</
|
|
172
|
-
<
|
|
184
|
+
</button>
|
|
185
|
+
<button type="submit" disabled={isSubmitting} className='flex flex-row items-center gap-2 px-6 py-2 border border-slate-200 rounded-xl text-slate-700 hover:opacity-80 transition-colors bg-secondary text-white text-sm'>
|
|
173
186
|
{isSubmitting ? 'Adding Address...' : 'Add Address'}
|
|
174
|
-
</
|
|
187
|
+
</button>
|
|
175
188
|
</div>
|
|
176
189
|
</form>
|
|
177
190
|
</Modal>
|
|
@@ -51,77 +51,81 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
51
51
|
initial={{ opacity: 0, y: 20 }}
|
|
52
52
|
animate={{ opacity: 1, y: 0 }}
|
|
53
53
|
exit={{ opacity: 0, x: -100 }}
|
|
54
|
-
className="
|
|
54
|
+
className="bg-white border-2 border-gray-100 rounded-[24px] p-6 hover:border-[#5B9BD5]/30 transition-all duration-300"
|
|
55
55
|
>
|
|
56
|
-
|
|
57
|
-
<button
|
|
58
|
-
onClick={handleRemove}
|
|
59
|
-
className="absolute top-4 right-4 p-1 text-gray-400 hover:text-red-600 transition-colors"
|
|
60
|
-
aria-label="Remove item"
|
|
61
|
-
>
|
|
62
|
-
<Trash2 className="w-5 h-5" />
|
|
63
|
-
</button>
|
|
56
|
+
|
|
64
57
|
|
|
65
58
|
<div className="flex gap-4 pr-8">
|
|
66
59
|
{/* Product Image */}
|
|
67
|
-
<div className="
|
|
60
|
+
<div className="w-28 h-28 rounded-[16px] overflow-hidden bg-gray-50 shrink-0">
|
|
68
61
|
<Image
|
|
69
62
|
src={item.productVariantData.productMedia[0]?.file || '/placeholder-product.jpg'}
|
|
70
63
|
alt={item.productVariantData.name}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
className="w-full h-full object-cover"
|
|
65
|
+
height={112}
|
|
66
|
+
width={112}
|
|
74
67
|
/>
|
|
75
68
|
</div>
|
|
76
69
|
|
|
77
70
|
{/* Product Info */}
|
|
78
71
|
<div className="flex-1 min-w-0">
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<
|
|
86
|
-
{
|
|
87
|
-
</p>
|
|
88
|
-
)}
|
|
89
|
-
|
|
90
|
-
{/* Quantity Controls */}
|
|
91
|
-
<div className="flex items-center gap-2 mt-3">
|
|
92
|
-
<button
|
|
93
|
-
onClick={() => handleUpdateQuantity(item.quantity - 1)}
|
|
94
|
-
disabled={isUpdating || item.quantity <= 1}
|
|
95
|
-
className="p-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors rounded"
|
|
96
|
-
>
|
|
97
|
-
<Minus className="w-4 h-4 text-gray-600" />
|
|
98
|
-
</button>
|
|
99
|
-
<span className="px-3 font-medium min-w-[2rem] text-center text-gray-900">
|
|
100
|
-
{isUpdating ? (
|
|
101
|
-
<span className="inline-block h-4 w-4 align-middle animate-spin rounded-full border-2 border-gray-300 border-t-gray-600" />
|
|
102
|
-
) : (
|
|
103
|
-
item.quantity
|
|
104
|
-
)}
|
|
72
|
+
<div className="flex items-start justify-between gap-4 mb-3">
|
|
73
|
+
<div className="flex-1 min-w-0">
|
|
74
|
+
<h3 className="font-['Poppins',sans-serif] font-semibold text-[#2B4B7C] mb-2">
|
|
75
|
+
{item.productVariantData.name}
|
|
76
|
+
</h3>
|
|
77
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
78
|
+
<span className="font-['Poppins',sans-serif] text-[12px] text-[#676c80]">
|
|
79
|
+
Variant: <span className="font-medium text-[#2B4B7C]">{item.productVariantData.name}</span>
|
|
105
80
|
</span>
|
|
106
|
-
<button
|
|
107
|
-
onClick={() => handleUpdateQuantity(item.quantity + 1)}
|
|
108
|
-
disabled={isUpdating || item.quantity >= (item.productVariantData.inventoryCount || 999)}
|
|
109
|
-
className="p-1 hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors rounded"
|
|
110
|
-
>
|
|
111
|
-
<Plus className="w-4 h-4 text-gray-600" />
|
|
112
|
-
</button>
|
|
113
81
|
</div>
|
|
114
|
-
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Delete Icon - Top Right */}
|
|
85
|
+
<button
|
|
86
|
+
onClick={handleRemove}
|
|
87
|
+
className="p-2 hover:bg-red-50 rounded-full transition-colors group"
|
|
88
|
+
aria-label="Remove item"
|
|
89
|
+
>
|
|
90
|
+
<Trash2 className="size-5 text-[#676c80] group-hover:text-red-500 transition-colors" />
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Quantity and Price */}
|
|
95
|
+
<div className="flex items-center justify-between gap-4">
|
|
96
|
+
{/* Quantity Controls */}
|
|
97
|
+
<div className="flex items-center gap-3 bg-gray-50 rounded-full px-4 py-2">
|
|
98
|
+
<button
|
|
99
|
+
onClick={() => handleUpdateQuantity(item.quantity - 1)}
|
|
100
|
+
disabled={isUpdating || item.quantity <= 1}
|
|
101
|
+
className="p-1 hover:bg-white rounded-full transition-colors"
|
|
102
|
+
>
|
|
103
|
+
<Minus className="size-4 text-[#2B4B7C]" />
|
|
104
|
+
</button>
|
|
105
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[14px] text-[#2B4B7C] min-w-[20px] text-center">
|
|
106
|
+
{item.quantity}
|
|
107
|
+
</span>
|
|
108
|
+
<button
|
|
109
|
+
onClick={() => handleUpdateQuantity(item.quantity + 1)}
|
|
110
|
+
disabled={isUpdating || item.quantity >= (item.productVariantData.inventoryCount || 999)}
|
|
111
|
+
className="p-1 hover:bg-white rounded-full transition-colors"
|
|
112
|
+
>
|
|
113
|
+
<Plus className="size-4 text-[#2B4B7C]" />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Price */}
|
|
118
|
+
<div className="text-right">
|
|
119
|
+
<p className="font-['Poppins',sans-serif] font-bold text-[18px] text-[#E67E50]">
|
|
120
|
+
{formatPrice(itemTotal)}
|
|
121
|
+
</p>
|
|
122
|
+
<p className="font-['Poppins',sans-serif] text-[11px] text-[#676c80]">
|
|
123
|
+
{formatPrice(unitPrice)} each
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
115
128
|
|
|
116
|
-
{/* Pricing - Right Side */}
|
|
117
|
-
<div className="text-right flex-shrink-0">
|
|
118
|
-
<p className="text-lg font-bold text-orange-600">
|
|
119
|
-
{formatPrice(itemTotal)}
|
|
120
|
-
</p>
|
|
121
|
-
<p className="text-sm text-gray-500 mt-1">
|
|
122
|
-
{formatPrice(unitPrice)} each
|
|
123
|
-
</p>
|
|
124
|
-
</div>
|
|
125
129
|
</div>
|
|
126
130
|
</motion.div>
|
|
127
131
|
);
|
|
@@ -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')}
|