hey-pharmacist-ecommerce 1.1.28 → 1.1.29
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 +344 -640
- package/dist/index.d.ts +344 -640
- package/dist/index.js +1807 -838
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1807 -840
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/components/AccountOrdersTab.tsx +1 -1
- package/src/components/AccountSettingsTab.tsx +88 -6
- package/src/components/CartItem.tsx +1 -1
- package/src/components/Header.tsx +8 -2
- package/src/components/OrderCard.tsx +4 -4
- package/src/components/ProductCard.tsx +59 -42
- package/src/components/QuickViewModal.tsx +13 -13
- package/src/hooks/useAddresses.ts +4 -1
- package/src/hooks/usePaymentMethods.ts +26 -31
- package/src/hooks/useProducts.ts +63 -64
- package/src/hooks/useWishlistProducts.ts +4 -5
- package/src/index.ts +2 -0
- package/src/lib/Apis/api.ts +0 -1
- package/src/lib/Apis/apis/auth-api.ts +18 -29
- package/src/lib/Apis/apis/products-api.ts +845 -405
- package/src/lib/Apis/models/category-populated.ts +0 -12
- package/src/lib/Apis/models/category-sub-category-populated.ts +2 -2
- package/src/lib/Apis/models/category.ts +0 -18
- package/src/lib/Apis/models/{table-cell-dto.ts → change-password-dto.ts} +6 -6
- package/src/lib/Apis/models/create-product-dto.ts +30 -23
- package/src/lib/Apis/models/create-sub-category-dto.ts +6 -0
- package/src/lib/Apis/models/create-variant-dto.ts +29 -29
- package/src/lib/Apis/models/index.ts +5 -7
- package/src/lib/Apis/models/paginated-products-dto.ts +6 -6
- package/src/lib/Apis/models/product-summary.ts +69 -0
- package/src/lib/Apis/models/product-variant.ts +34 -65
- package/src/lib/Apis/models/product.ts +138 -0
- package/src/lib/Apis/models/products-insights-dto.ts +12 -0
- package/src/lib/Apis/models/single-product-media.ts +0 -12
- package/src/lib/Apis/models/sub-category.ts +6 -12
- package/src/lib/Apis/models/update-product-dto.ts +30 -19
- package/src/lib/Apis/models/update-sub-category-dto.ts +6 -0
- package/src/lib/Apis/models/{update-product-variant-dto.ts → update-variant-dto.ts} +51 -45
- package/src/lib/Apis/models/{shallow-parent-category-dto.ts → variant-id-inventory-body.ts} +5 -11
- package/src/lib/api-adapter/config.ts +53 -0
- package/src/lib/validations/address.ts +1 -1
- package/src/providers/FavoritesProvider.tsx +5 -5
- package/src/providers/WishlistProvider.tsx +4 -4
- package/src/screens/CartScreen.tsx +1 -1
- package/src/screens/ChangePasswordScreen.tsx +2 -6
- package/src/screens/CheckoutScreen.tsx +40 -11
- package/src/screens/ForgotPasswordScreen.tsx +153 -0
- package/src/screens/ProductDetailScreen.tsx +51 -60
- package/src/screens/RegisterScreen.tsx +31 -31
- package/src/screens/ResetPasswordScreen.tsx +202 -0
- package/src/screens/SearchResultsScreen.tsx +264 -26
- package/src/screens/ShopScreen.tsx +42 -45
- package/src/screens/WishlistScreen.tsx +35 -31
- package/src/lib/Apis/apis/product-variants-api.ts +0 -552
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +0 -154
- package/src/lib/Apis/models/extended-product-dto.ts +0 -206
- package/src/lib/Apis/models/frequently-bought-product-dto.ts +0 -71
- package/src/lib/Apis/models/table-dto.ts +0 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hey-pharmacist-ecommerce",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.29",
|
|
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",
|
|
@@ -102,7 +102,7 @@ export function AccountOrdersTab() {
|
|
|
102
102
|
<div key={item.productVariantId || index} className="flex items-center gap-3">
|
|
103
103
|
<div className="relative w-12 h-12 rounded-lg bg-slate-100 shrink-0 overflow-hidden">
|
|
104
104
|
<Image
|
|
105
|
-
src={item?.productVariantData?.
|
|
105
|
+
src={item?.productVariantData?.media?.[0]?.file || '/placeholder-product.jpg'}
|
|
106
106
|
alt={item?.productVariantData?.name || 'Product image'}
|
|
107
107
|
fill
|
|
108
108
|
className="object-cover"
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState } from 'react';
|
|
4
|
-
import { Bell, Lock, Trash2 } from 'lucide-react';
|
|
4
|
+
import { Bell, Lock, Trash2, AlertTriangle } from 'lucide-react';
|
|
5
5
|
import { Button } from './ui/Button';
|
|
6
|
+
import { Modal } from './ui/Modal';
|
|
6
7
|
import { useRouter } from 'next/navigation';
|
|
7
8
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
9
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
10
|
+
import { UsersApi } from '@/lib/Apis/apis/users-api';
|
|
11
|
+
import { AXIOS_CONFIG } from '@/lib/Apis/sharedConfig';
|
|
8
12
|
|
|
9
13
|
export function AccountSettingsTab() {
|
|
10
14
|
const router = useRouter();
|
|
11
15
|
const { buildPath } = useBasePath();
|
|
16
|
+
const { logout } = useAuth();
|
|
12
17
|
const [emailNotifications, setEmailNotifications] = useState(true);
|
|
13
18
|
const [orderUpdates, setOrderUpdates] = useState(true);
|
|
14
19
|
const [promotionalEmails, setPromotionalEmails] = useState(false);
|
|
20
|
+
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
|
21
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
22
|
+
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleDeleteAccount = async () => {
|
|
25
|
+
setIsDeleting(true);
|
|
26
|
+
setDeleteError(null);
|
|
27
|
+
try {
|
|
28
|
+
const usersApi = new UsersApi(AXIOS_CONFIG);
|
|
29
|
+
await usersApi.deleteMyProfile();
|
|
30
|
+
// Logout and redirect to home
|
|
31
|
+
logout();
|
|
32
|
+
router.push(buildPath('/'));
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
setDeleteError(
|
|
35
|
+
error?.response?.data?.message || 'Failed to delete account. Please try again.'
|
|
36
|
+
);
|
|
37
|
+
setIsDeleting(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
15
40
|
|
|
16
41
|
return (
|
|
17
42
|
<div className="p-6 space-y-6">
|
|
@@ -99,11 +124,7 @@ export function AccountSettingsTab() {
|
|
|
99
124
|
variant="outline-solid"
|
|
100
125
|
size="sm"
|
|
101
126
|
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
|
-
}}
|
|
127
|
+
onClick={() => setShowDeleteModal(true)}
|
|
107
128
|
>
|
|
108
129
|
<Trash2 className="h-4 w-4" />
|
|
109
130
|
Delete
|
|
@@ -111,6 +132,67 @@ export function AccountSettingsTab() {
|
|
|
111
132
|
</div>
|
|
112
133
|
</div>
|
|
113
134
|
</div>
|
|
135
|
+
|
|
136
|
+
{/* Delete Account Confirmation Modal */}
|
|
137
|
+
<Modal
|
|
138
|
+
isOpen={showDeleteModal}
|
|
139
|
+
onClose={() => {
|
|
140
|
+
setShowDeleteModal(false);
|
|
141
|
+
setDeleteError(null);
|
|
142
|
+
}}
|
|
143
|
+
title="Delete Account"
|
|
144
|
+
>
|
|
145
|
+
<div className="space-y-4">
|
|
146
|
+
<div className="flex items-start gap-3 rounded-lg border border-red-200 bg-red-50 p-4">
|
|
147
|
+
<AlertTriangle className="h-5 w-5 text-red-600 mt-0.5 flex-shrink-0" />
|
|
148
|
+
<div>
|
|
149
|
+
<p className="text-sm font-semibold text-red-900">Warning: This action is permanent</p>
|
|
150
|
+
<p className="text-sm text-red-700 mt-1">
|
|
151
|
+
Deleting your account will permanently remove all your data, including:
|
|
152
|
+
</p>
|
|
153
|
+
<ul className="text-sm text-red-700 mt-2 ml-4 list-disc space-y-1">
|
|
154
|
+
<li>Personal information and profile</li>
|
|
155
|
+
<li>Order history and tracking</li>
|
|
156
|
+
<li>Saved addresses and payment methods</li>
|
|
157
|
+
<li>Wishlist items</li>
|
|
158
|
+
</ul>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{deleteError && (
|
|
163
|
+
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
|
164
|
+
<span className="font-semibold">Error: </span>
|
|
165
|
+
{deleteError}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<p className="text-sm text-slate-600">
|
|
170
|
+
Are you absolutely sure you want to delete your account? This action cannot be undone.
|
|
171
|
+
</p>
|
|
172
|
+
|
|
173
|
+
<div className="flex gap-3 pt-2">
|
|
174
|
+
<Button
|
|
175
|
+
variant="outline-solid"
|
|
176
|
+
onClick={() => {
|
|
177
|
+
setShowDeleteModal(false);
|
|
178
|
+
setDeleteError(null);
|
|
179
|
+
}}
|
|
180
|
+
disabled={isDeleting}
|
|
181
|
+
className="flex-1"
|
|
182
|
+
>
|
|
183
|
+
Cancel
|
|
184
|
+
</Button>
|
|
185
|
+
<Button
|
|
186
|
+
onClick={handleDeleteAccount}
|
|
187
|
+
isLoading={isDeleting}
|
|
188
|
+
disabled={isDeleting}
|
|
189
|
+
className="flex-1 bg-red-600 hover:bg-red-700 text-white"
|
|
190
|
+
>
|
|
191
|
+
{isDeleting ? 'Deleting...' : 'Delete Account'}
|
|
192
|
+
</Button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</Modal>
|
|
114
196
|
</div>
|
|
115
197
|
);
|
|
116
198
|
}
|
|
@@ -55,7 +55,7 @@ export function CartItem({ item }: CartItemProps) {
|
|
|
55
55
|
{/* Product Image */}
|
|
56
56
|
<div className="w-28 h-28 rounded-[16px] overflow-hidden bg-gray-50 shrink-0">
|
|
57
57
|
<Image
|
|
58
|
-
src={item.productVariantData.
|
|
58
|
+
src={item.productVariantData.media[0]?.file || '/placeholder-product.jpg'}
|
|
59
59
|
alt={item.productVariantData.name}
|
|
60
60
|
className="w-full h-full object-cover"
|
|
61
61
|
height={112}
|
|
@@ -101,13 +101,19 @@ export function Header() {
|
|
|
101
101
|
value={searchQuery}
|
|
102
102
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
103
103
|
onKeyDown={(e) => {
|
|
104
|
-
if (e.key === 'Enter'
|
|
105
|
-
|
|
104
|
+
if (e.key === 'Enter') {
|
|
105
|
+
const sanitized = searchQuery.trim().replace(/\s+/g, ' ');
|
|
106
|
+
if (sanitized) {
|
|
107
|
+
router.push(buildPath(`/search?q=${encodeURIComponent(sanitized)}`));
|
|
108
|
+
setIsSearchOpen(false);
|
|
109
|
+
setSearchQuery('');
|
|
110
|
+
}
|
|
106
111
|
}
|
|
107
112
|
}}
|
|
108
113
|
placeholder="Search products..."
|
|
109
114
|
className="w-full outline-hidden text-gray-700"
|
|
110
115
|
autoFocus
|
|
116
|
+
autoComplete="off"
|
|
111
117
|
/>
|
|
112
118
|
{searchQuery && (
|
|
113
119
|
<button
|
|
@@ -15,9 +15,9 @@ interface OrderCardProps {
|
|
|
15
15
|
export function OrderCard({ order }: OrderCardProps) {
|
|
16
16
|
const config = order.orderStatus;
|
|
17
17
|
const itemCount = order.items?.length || 0;
|
|
18
|
-
const showPriceBreakdown = (order.shippingCost && order.shippingCost > 0) ||
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const showPriceBreakdown = (order.shippingCost && order.shippingCost > 0) ||
|
|
19
|
+
(order.tax && order.tax > 0) ||
|
|
20
|
+
(order.discountedAmount && order.discountedAmount > 0);
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
23
|
<motion.div
|
|
@@ -55,7 +55,7 @@ export function OrderCard({ order }: OrderCardProps) {
|
|
|
55
55
|
<div key={item.productVariantId || item._id} className="flex items-center gap-2 text-sm">
|
|
56
56
|
<div className="relative w-12 h-12 rounded-sm bg-gray-100 shrink-0 overflow-hidden">
|
|
57
57
|
<Image
|
|
58
|
-
src={item?.productVariantData?.
|
|
58
|
+
src={item?.productVariantData?.media?.[0]?.file || '/placeholder-product.jpg'}
|
|
59
59
|
alt={item?.productVariantData?.name || 'Product image'}
|
|
60
60
|
fill
|
|
61
61
|
className="object-cover"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
-
import { motion
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
5
|
import { Star, ShoppingCart, Eye } from 'lucide-react';
|
|
6
|
-
import {
|
|
6
|
+
import { Product, } from '@/lib/Apis';
|
|
7
7
|
import { formatPrice } from '@/lib/utils/format';
|
|
8
8
|
import { useWishlist } from '@/providers/WishlistProvider';
|
|
9
9
|
import { useCart } from '@/providers/CartProvider';
|
|
@@ -12,11 +12,12 @@ import { useRouter } from 'next/navigation';
|
|
|
12
12
|
import { useBasePath } from '@/providers/BasePathProvider';
|
|
13
13
|
import { QuickViewModal } from './QuickViewModal';
|
|
14
14
|
import { useNotification } from '@/providers/NotificationProvider';
|
|
15
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
15
16
|
|
|
16
17
|
interface ProductCardProps {
|
|
17
|
-
product:
|
|
18
|
-
onClickProduct?: (product:
|
|
19
|
-
onFavorite?: (product:
|
|
18
|
+
product: Product;
|
|
19
|
+
onClickProduct?: (product: Product) => void;
|
|
20
|
+
onFavorite?: (product: Product) => void;
|
|
20
21
|
isFavorited?: boolean;
|
|
21
22
|
showFavoriteButton?: boolean;
|
|
22
23
|
}
|
|
@@ -30,6 +31,7 @@ export function ProductCard({
|
|
|
30
31
|
className
|
|
31
32
|
}: ProductCardProps & { className?: string }) {
|
|
32
33
|
const router = useRouter();
|
|
34
|
+
const { isAuthenticated } = useAuth();
|
|
33
35
|
const { buildPath } = useBasePath();
|
|
34
36
|
const [isFavorite, setIsFavorite] = useState(isFavorited);
|
|
35
37
|
const { addToWishlist, removeFromWishlist, isInWishlist } = useWishlist();
|
|
@@ -65,7 +67,7 @@ export function ProductCard({
|
|
|
65
67
|
`${product.name} was removed from your wishlist.`
|
|
66
68
|
);
|
|
67
69
|
} else {
|
|
68
|
-
await addToWishlist(product as unknown as
|
|
70
|
+
await addToWishlist(product as unknown as Product);
|
|
69
71
|
setIsFavorite(true);
|
|
70
72
|
notification.success(
|
|
71
73
|
'Added to wishlist',
|
|
@@ -95,14 +97,14 @@ export function ProductCard({
|
|
|
95
97
|
setIsImageLoaded(false);
|
|
96
98
|
|
|
97
99
|
// Auto-select first variant if available
|
|
98
|
-
if (product.
|
|
99
|
-
const firstVariant = product.
|
|
100
|
-
if (firstVariant.
|
|
101
|
-
setSelectedVariantImage(firstVariant.
|
|
100
|
+
if (product.variants && product.variants.length > 0) {
|
|
101
|
+
const firstVariant = product.variants[0];
|
|
102
|
+
if (firstVariant.media && firstVariant.media.length > 0) {
|
|
103
|
+
setSelectedVariantImage(firstVariant.media[0].file);
|
|
102
104
|
setSelectedVariantId(firstVariant.id || firstVariant._id || null);
|
|
103
105
|
}
|
|
104
106
|
}
|
|
105
|
-
}, [product._id, product.
|
|
107
|
+
}, [product._id, product.variants]);
|
|
106
108
|
|
|
107
109
|
// Handle keyboard navigation
|
|
108
110
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
@@ -115,58 +117,53 @@ export function ProductCard({
|
|
|
115
117
|
|
|
116
118
|
// Get variant images (first image from each variant that has images)
|
|
117
119
|
const variantImages = useMemo(() => {
|
|
118
|
-
if (!product.
|
|
120
|
+
if (!product.variants || product.variants.length === 0) {
|
|
119
121
|
return [];
|
|
120
122
|
}
|
|
121
|
-
return product.
|
|
122
|
-
.filter((variant) => variant.
|
|
123
|
+
return product.variants
|
|
124
|
+
.filter((variant) => variant.media && variant.media.length > 0)
|
|
123
125
|
.map((variant) => ({
|
|
124
126
|
variantId: variant.id || variant._id,
|
|
125
127
|
variantName: variant.name,
|
|
126
|
-
image: variant.
|
|
128
|
+
image: variant.media[0].file,
|
|
127
129
|
}));
|
|
128
|
-
}, [product.
|
|
130
|
+
}, [product.variants]);
|
|
129
131
|
|
|
130
132
|
// Get selected variant
|
|
131
133
|
const selectedVariant = useMemo(() => {
|
|
132
|
-
if (!selectedVariantId || !product.
|
|
133
|
-
return product.
|
|
134
|
+
if (!selectedVariantId || !product.variants) return null;
|
|
135
|
+
return product.variants.find(
|
|
134
136
|
(variant) => (variant.id || variant._id) === selectedVariantId
|
|
135
137
|
);
|
|
136
|
-
}, [selectedVariantId, product.
|
|
137
|
-
|
|
138
|
-
// Get display name (variant name if selected, otherwise product name)
|
|
139
|
-
const displayName = useMemo(() => {
|
|
140
|
-
return selectedVariant?.name || product.name;
|
|
141
|
-
}, [selectedVariant, product.name]);
|
|
138
|
+
}, [selectedVariantId, product.variants]);
|
|
142
139
|
|
|
143
140
|
const displayFinalPrice = useMemo(() => {
|
|
144
|
-
return selectedVariant?.finalPrice ?? product.finalPrice;
|
|
145
|
-
}, [selectedVariant, product.
|
|
141
|
+
return selectedVariant?.finalPrice ?? product.variants?.[0]?.finalPrice ?? 0;
|
|
142
|
+
}, [selectedVariant, product.variants]);
|
|
146
143
|
|
|
147
144
|
const displayPriceBeforeDiscount = useMemo(() => {
|
|
148
|
-
return selectedVariant?.retailPrice ?? product.
|
|
149
|
-
}, [selectedVariant, product.
|
|
145
|
+
return selectedVariant?.retailPrice ?? product.variants?.[0]?.retailPrice ?? 0;
|
|
146
|
+
}, [selectedVariant, product.variants]);
|
|
150
147
|
|
|
151
148
|
const displayIsDiscounted = useMemo(() => {
|
|
152
|
-
return selectedVariant ? selectedVariant.isDiscounted : product.isDiscounted;
|
|
153
|
-
}, [selectedVariant, product.
|
|
149
|
+
return selectedVariant ? selectedVariant.isDiscounted : product.variants?.[0]?.isDiscounted;
|
|
150
|
+
}, [selectedVariant, product.variants]);
|
|
154
151
|
|
|
155
152
|
const displayDiscountAmount = useMemo(() => {
|
|
156
|
-
return selectedVariant ? selectedVariant.discountAmount : product.discountAmount;
|
|
157
|
-
}, [selectedVariant, product.
|
|
153
|
+
return selectedVariant ? selectedVariant.discountAmount : product.variants?.[0]?.discountAmount;
|
|
154
|
+
}, [selectedVariant, product.variants]);
|
|
158
155
|
|
|
159
156
|
const displayInventoryCount = useMemo(() => {
|
|
160
|
-
return selectedVariant ? selectedVariant.inventoryCount : product.inventoryCount;
|
|
161
|
-
}, [selectedVariant, product.
|
|
157
|
+
return selectedVariant ? selectedVariant.inventoryCount : product.variants?.[0]?.inventoryCount;
|
|
158
|
+
}, [selectedVariant, product.variants]);
|
|
162
159
|
|
|
163
160
|
const imageSource = useMemo(() => {
|
|
164
|
-
const src = selectedVariantImage || product.
|
|
161
|
+
const src = selectedVariantImage || selectedVariant?.media?.[0]?.file || product.media?.[0]?.file || '/placeholder-product.jpg';
|
|
165
162
|
return {
|
|
166
163
|
src,
|
|
167
164
|
alt: product.name || 'Product image'
|
|
168
165
|
};
|
|
169
|
-
}, [product.
|
|
166
|
+
}, [product.media, product.name, selectedVariantImage]);
|
|
170
167
|
|
|
171
168
|
// Reset image loaded state when image source changes
|
|
172
169
|
useEffect(() => {
|
|
@@ -261,24 +258,29 @@ export function ProductCard({
|
|
|
261
258
|
|
|
262
259
|
<div className="h-[40px] mb-3">
|
|
263
260
|
<h3 className="text-sm font-['Poppins',sans-serif] font-semibold text-[#2B4B7C] line-clamp-2">
|
|
264
|
-
{
|
|
261
|
+
{product.name}
|
|
265
262
|
</h3>
|
|
263
|
+
{selectedVariant && (
|
|
264
|
+
<p className="text-xs font-['Poppins',sans-serif] text-[#676c80]">
|
|
265
|
+
{selectedVariant.name}
|
|
266
|
+
</p>
|
|
267
|
+
)}
|
|
266
268
|
</div>
|
|
267
269
|
{/* Rating */}
|
|
268
|
-
<div className="flex items-center gap-1.5
|
|
270
|
+
<div className="flex items-center gap-1.5 ">
|
|
269
271
|
<div className="flex items-center gap-0.5">
|
|
270
272
|
{[...Array(5)].map((_, i) => (
|
|
271
273
|
<Star
|
|
272
274
|
key={i}
|
|
273
|
-
className={`size-4 ${i < Math.floor(product.
|
|
275
|
+
className={`size-4 ${i < Math.floor(product.summary?.averageRating || 0)
|
|
274
276
|
? 'text-[#E67E50] fill-[#E67E50]'
|
|
275
277
|
: 'text-gray-300'
|
|
276
278
|
}`}
|
|
277
279
|
/>
|
|
278
280
|
))}
|
|
279
281
|
</div>
|
|
280
|
-
<span className="font-['Poppins',sans-serif] text-[10px] text-[#676c80]">
|
|
281
|
-
({product.
|
|
282
|
+
<span className="font-['Poppins',sans-serif] text-[10px] text-[#676c80] mt-4">
|
|
283
|
+
({product.summary?.reviewCount || 0})
|
|
282
284
|
</span>
|
|
283
285
|
</div>
|
|
284
286
|
|
|
@@ -289,7 +291,7 @@ export function ProductCard({
|
|
|
289
291
|
</span>
|
|
290
292
|
{displayIsDiscounted && (
|
|
291
293
|
<span className="font-['Poppins',sans-serif] text-sm text-[#676c80] line-through">
|
|
292
|
-
${
|
|
294
|
+
${displayPriceBeforeDiscount.toFixed(2)}
|
|
293
295
|
</span>
|
|
294
296
|
)}
|
|
295
297
|
</div>
|
|
@@ -342,6 +344,21 @@ export function ProductCard({
|
|
|
342
344
|
);
|
|
343
345
|
return;
|
|
344
346
|
}
|
|
347
|
+
if (displayInventoryCount === 0) {
|
|
348
|
+
notification.error(
|
|
349
|
+
'Out of Stock',
|
|
350
|
+
'This item is currently out of stock.'
|
|
351
|
+
);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (!isAuthenticated) {
|
|
355
|
+
notification.error(
|
|
356
|
+
'Sign-in required',
|
|
357
|
+
'Please sign in to add items to your cart.'
|
|
358
|
+
);
|
|
359
|
+
router.push(buildPath(`/login?redirect=${encodeURIComponent(window.location.pathname + window.location.search)}`));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
345
362
|
try {
|
|
346
363
|
await addToCart(
|
|
347
364
|
product._id || product.id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
2
|
import { X, ShoppingCart, Check, Star, Package, ExternalLink, Minus, Plus } from 'lucide-react';
|
|
3
|
-
import {
|
|
3
|
+
import { Product } from '@/lib/Apis';
|
|
4
4
|
import { useCart } from '@/providers/CartProvider';
|
|
5
5
|
import Image from 'next/image';
|
|
6
6
|
import { ProductVariantInventoryStatusEnum } from '@/lib/Apis';
|
|
@@ -8,7 +8,7 @@ import { useNotification } from '@/providers/NotificationProvider';
|
|
|
8
8
|
import Link from 'next/link';
|
|
9
9
|
|
|
10
10
|
interface QuickViewModalProps {
|
|
11
|
-
product:
|
|
11
|
+
product: Product;
|
|
12
12
|
onClose: () => void;
|
|
13
13
|
onNavigateToProduct?: (productId: string) => void;
|
|
14
14
|
}
|
|
@@ -28,8 +28,8 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
28
28
|
setQuantity(newQuantity);
|
|
29
29
|
}
|
|
30
30
|
};
|
|
31
|
-
const selectedVariant = product.
|
|
32
|
-
const selectedSize = selectedVariant.
|
|
31
|
+
const selectedVariant = product.variants[selectedVariantIndex];
|
|
32
|
+
const selectedSize = selectedVariant.media[selectedSizeIndex];
|
|
33
33
|
const displayPrice = selectedVariant.finalPrice;
|
|
34
34
|
const displayOriginalPrice = selectedVariant.retailPrice;
|
|
35
35
|
const isDiscounted = selectedVariant.isDiscounted;
|
|
@@ -75,7 +75,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
75
75
|
<div className="flex items-start justify-between mb-6">
|
|
76
76
|
<div>
|
|
77
77
|
<p className="font-['Poppins',sans-serif] text-[11px] text-primary uppercase tracking-wide font-medium mb-2">
|
|
78
|
-
{product.brand}
|
|
78
|
+
{product.brand}
|
|
79
79
|
</p>
|
|
80
80
|
<h2 className="font-['Poppins',sans-serif] font-semibold text-secondary tracking-[-1px]">
|
|
81
81
|
{displayName}
|
|
@@ -87,7 +87,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
87
87
|
{[...Array(5)].map((_, i) => (
|
|
88
88
|
<Star
|
|
89
89
|
key={i}
|
|
90
|
-
className={`size-4 ${i < Math.floor(product.
|
|
90
|
+
className={`size-4 ${i < Math.floor(product.summary?.averageRating || 0)
|
|
91
91
|
? 'text-accent fill-accent'
|
|
92
92
|
: 'text-gray-300'
|
|
93
93
|
}`}
|
|
@@ -95,7 +95,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
95
95
|
))}
|
|
96
96
|
</div>
|
|
97
97
|
<span className="font-['Poppins',sans-serif] text-[13px] text-muted">
|
|
98
|
-
{product.
|
|
98
|
+
{product.summary?.averageRating || 0} ({product.summary?.reviewCount || 0} reviews)
|
|
99
99
|
</span>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
@@ -112,7 +112,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
112
112
|
<div className="space-y-4">
|
|
113
113
|
<div className="relative aspect-3/4 rounded-[24px] overflow-hidden bg-gray-50">
|
|
114
114
|
<img
|
|
115
|
-
src={selectedVariant.
|
|
115
|
+
src={selectedVariant.media[selectedImageIndex]?.file || selectedVariant.media[0]?.file}
|
|
116
116
|
alt={product.name}
|
|
117
117
|
className="w-full h-full object-contain"
|
|
118
118
|
/>
|
|
@@ -136,9 +136,9 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
136
136
|
</div>
|
|
137
137
|
|
|
138
138
|
{/* Thumbnail Images */}
|
|
139
|
-
{selectedVariant.
|
|
139
|
+
{selectedVariant.media.length > 1 && (
|
|
140
140
|
<div className="grid grid-cols-4 gap-3">
|
|
141
|
-
{selectedVariant.
|
|
141
|
+
{selectedVariant.media.map((image: any, index: any) => (
|
|
142
142
|
<div
|
|
143
143
|
key={index}
|
|
144
144
|
className={`aspect-square rounded-xl overflow-hidden cursor-pointer transition-opacity ${selectedImageIndex === index ? 'ring-2 ring-primary' : 'bg-gray-50 hover:opacity-75'}`}
|
|
@@ -197,10 +197,10 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
197
197
|
{/* Color Selection */}
|
|
198
198
|
<div className="mb-6">
|
|
199
199
|
<h3 className="font-['Poppins',sans-serif] font-semibold text-[13px] text-secondary mb-3">
|
|
200
|
-
Selected Variant: <span className="font-normal text-muted">{product.
|
|
200
|
+
Selected Variant: <span className="font-normal text-muted">{product.variants[selectedVariantIndex].name}</span>
|
|
201
201
|
</h3>
|
|
202
202
|
<div className="flex flex-wrap gap-3">
|
|
203
|
-
{product.
|
|
203
|
+
{product.variants.map((variant: any, index: any) => (
|
|
204
204
|
<button
|
|
205
205
|
key={variant.id}
|
|
206
206
|
onClick={() => {
|
|
@@ -216,7 +216,7 @@ export function QuickViewModal({ product, onClose, onNavigateToProduct }: QuickV
|
|
|
216
216
|
title={variant.color}
|
|
217
217
|
>
|
|
218
218
|
<Image
|
|
219
|
-
src={variant.
|
|
219
|
+
src={variant.media?.[0]?.file || ''}
|
|
220
220
|
alt={variant.color || `Variant ${index + 1}`}
|
|
221
221
|
className="object-cover"
|
|
222
222
|
height={32}
|
|
@@ -34,7 +34,10 @@ export function useAddresses(): UseAddressesReturn {
|
|
|
34
34
|
const sortedAddresses = useMemo(() => {
|
|
35
35
|
return [...addresses].sort((a, b) => {
|
|
36
36
|
if (a.isDefault === b.isDefault) {
|
|
37
|
-
|
|
37
|
+
// updatedAt is already a string from the API, so we can compare directly
|
|
38
|
+
const dateA = a.updatedAt || '';
|
|
39
|
+
const dateB = b.updatedAt || '';
|
|
40
|
+
return dateB.toString().localeCompare(dateA.toString());
|
|
38
41
|
}
|
|
39
42
|
return a.isDefault ? -1 : 1;
|
|
40
43
|
});
|
|
@@ -1,47 +1,42 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
// NOTE: PaymentMethodsApi and PaymentMethod were removed in the API refactoring.
|
|
4
|
+
// This hook is stubbed out until the API is restored.
|
|
5
|
+
// The AccountPaymentTab component will display an empty state.
|
|
6
|
+
|
|
7
|
+
interface PaymentMethod {
|
|
8
|
+
id: string;
|
|
9
|
+
type: string;
|
|
10
|
+
last4?: string;
|
|
11
|
+
brand?: string;
|
|
12
|
+
expiryMonth?: number;
|
|
13
|
+
expiryYear?: number;
|
|
14
|
+
isDefault?: boolean;
|
|
15
|
+
}
|
|
5
16
|
|
|
6
17
|
export function usePaymentMethods() {
|
|
7
18
|
const [paymentMethods, setPaymentMethods] = useState<PaymentMethod[]>([]);
|
|
8
|
-
const [isLoading, setIsLoading] = useState(
|
|
19
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
9
20
|
const [error, setError] = useState<Error | null>(null);
|
|
10
21
|
|
|
11
22
|
const fetchPaymentMethods = useCallback(async () => {
|
|
12
|
-
|
|
23
|
+
// Stubbed: API not available
|
|
24
|
+
setIsLoading(false);
|
|
25
|
+
setPaymentMethods([]);
|
|
13
26
|
setError(null);
|
|
14
|
-
try {
|
|
15
|
-
const response = await new PaymentMethodsApi(getApiConfiguration()).getPaymentMethods();
|
|
16
|
-
setPaymentMethods(response.data.paymentMethods || []);
|
|
17
|
-
} catch (err) {
|
|
18
|
-
setError(err as Error);
|
|
19
|
-
} finally {
|
|
20
|
-
setIsLoading(false);
|
|
21
|
-
}
|
|
22
27
|
}, []);
|
|
23
28
|
|
|
24
29
|
const deletePaymentMethod = useCallback(async (paymentMethodId: string) => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
await fetchPaymentMethods();
|
|
30
|
-
} catch (err) {
|
|
31
|
-
throw err;
|
|
32
|
-
}
|
|
33
|
-
}, [fetchPaymentMethods]);
|
|
30
|
+
// Stubbed: API not available
|
|
31
|
+
console.warn('PaymentMethodsApi not available - deletePaymentMethod stubbed');
|
|
32
|
+
throw new Error('Payment methods API is not available');
|
|
33
|
+
}, []);
|
|
34
34
|
|
|
35
35
|
const setDefaultPaymentMethod = useCallback(async (paymentMethodId: string) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
await fetchPaymentMethods();
|
|
41
|
-
} catch (err) {
|
|
42
|
-
throw err;
|
|
43
|
-
}
|
|
44
|
-
}, [fetchPaymentMethods]);
|
|
36
|
+
// Stubbed: API not available
|
|
37
|
+
console.warn('PaymentMethodsApi not available - setDefaultPaymentMethod stubbed');
|
|
38
|
+
throw new Error('Payment methods API is not available');
|
|
39
|
+
}, []);
|
|
45
40
|
|
|
46
41
|
useEffect(() => {
|
|
47
42
|
fetchPaymentMethods();
|