hey-pharmacist-ecommerce 1.0.5 → 1.0.7
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/README.md +157 -17
- package/dist/index.d.mts +3636 -316
- package/dist/index.d.ts +3636 -316
- package/dist/index.js +6802 -3866
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6756 -3818
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -15
- package/src/components/AddressFormModal.tsx +171 -0
- package/src/components/CartItem.tsx +17 -12
- package/src/components/FilterChips.tsx +195 -0
- package/src/components/Header.tsx +121 -71
- package/src/components/OrderCard.tsx +18 -25
- package/src/components/ProductCard.tsx +209 -72
- package/src/components/ui/Button.tsx +13 -5
- package/src/components/ui/Card.tsx +46 -0
- package/src/hooks/useAddresses.ts +83 -0
- package/src/hooks/useOrders.ts +37 -19
- package/src/hooks/useProducts.ts +55 -63
- package/src/hooks/useWishlistProducts.ts +75 -0
- package/src/index.ts +3 -19
- package/src/lib/Apis/api.ts +1 -0
- package/src/lib/Apis/apis/cart-api.ts +3 -3
- package/src/lib/Apis/apis/inventory-api.ts +0 -108
- package/src/lib/Apis/apis/stores-api.ts +70 -0
- package/src/lib/Apis/apis/wishlist-api.ts +447 -0
- package/src/lib/Apis/models/cart-item-populated.ts +0 -1
- package/src/lib/Apis/models/create-single-variant-product-dto.ts +3 -10
- package/src/lib/Apis/models/create-variant-dto.ts +26 -33
- package/src/lib/Apis/models/extended-product-dto.ts +20 -24
- package/src/lib/Apis/models/index.ts +2 -1
- package/src/lib/Apis/models/order-time-line-dto.ts +49 -0
- package/src/lib/Apis/models/order.ts +3 -8
- package/src/lib/Apis/models/populated-order.ts +3 -8
- package/src/lib/Apis/models/product-variant.ts +29 -0
- package/src/lib/Apis/models/update-product-variant-dto.ts +16 -23
- package/src/lib/Apis/models/wishlist.ts +51 -0
- package/src/lib/Apis/wrapper.ts +18 -7
- package/src/lib/api-adapter/index.ts +0 -12
- package/src/lib/types/index.ts +16 -61
- package/src/lib/utils/colors.ts +7 -4
- package/src/lib/utils/format.ts +1 -1
- package/src/lib/validations/address.ts +14 -0
- package/src/providers/AuthProvider.tsx +61 -31
- package/src/providers/CartProvider.tsx +18 -28
- package/src/providers/EcommerceProvider.tsx +7 -0
- package/src/providers/FavoritesProvider.tsx +86 -0
- package/src/providers/ThemeProvider.tsx +16 -1
- package/src/providers/WishlistProvider.tsx +174 -0
- package/src/screens/AddressesScreen.tsx +484 -0
- package/src/screens/CartScreen.tsx +120 -84
- package/src/screens/CategoriesScreen.tsx +120 -0
- package/src/screens/CheckoutScreen.tsx +919 -241
- package/src/screens/CurrentOrdersScreen.tsx +125 -61
- package/src/screens/HomeScreen.tsx +209 -0
- package/src/screens/LoginScreen.tsx +133 -88
- package/src/screens/NewAddressScreen.tsx +187 -0
- package/src/screens/OrdersScreen.tsx +162 -50
- package/src/screens/ProductDetailScreen.tsx +641 -190
- package/src/screens/ProfileScreen.tsx +192 -116
- package/src/screens/RegisterScreen.tsx +193 -144
- package/src/screens/SearchResultsScreen.tsx +165 -0
- package/src/screens/ShopScreen.tsx +1110 -146
- package/src/screens/WishlistScreen.tsx +428 -0
- package/src/lib/Apis/models/inventory-paginated-response.ts +0 -75
- package/src/lib/api/auth.ts +0 -81
- package/src/lib/api/cart.ts +0 -42
- package/src/lib/api/orders.ts +0 -53
- package/src/lib/api/products.ts +0 -51
- package/src/lib/api-adapter/auth-adapter.ts +0 -196
- package/src/lib/api-adapter/cart-adapter.ts +0 -193
- package/src/lib/api-adapter/mappers.ts +0 -152
- package/src/lib/api-adapter/orders-adapter.ts +0 -195
- package/src/lib/api-adapter/products-adapter.ts +0 -194
|
@@ -1,122 +1,259 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useState } from 'react';
|
|
4
|
-
import { motion } from 'framer-motion';
|
|
5
|
-
import {
|
|
6
|
-
import { Product } from '@/lib/
|
|
3
|
+
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
4
|
+
import { motion, AnimatePresence } from 'framer-motion';
|
|
5
|
+
import { Heart } from 'lucide-react';
|
|
6
|
+
import { ExtendedProductDTO, Product } from '@/lib/Apis';
|
|
7
7
|
import { formatPrice } from '@/lib/utils/format';
|
|
8
|
-
import {
|
|
8
|
+
import { useWishlist } from '@/providers/WishlistProvider';
|
|
9
9
|
import Image from 'next/image';
|
|
10
|
+
import { toast } from 'sonner';
|
|
11
|
+
import { useRouter } from 'next/navigation';
|
|
12
|
+
|
|
10
13
|
|
|
11
14
|
interface ProductCardProps {
|
|
12
|
-
product:
|
|
13
|
-
onClickProduct?: (product:
|
|
15
|
+
product: ExtendedProductDTO;
|
|
16
|
+
onClickProduct?: (product: ExtendedProductDTO) => void;
|
|
17
|
+
onFavorite?: (product: ExtendedProductDTO) => void;
|
|
18
|
+
isFavorited?: boolean;
|
|
19
|
+
showFavoriteButton?: boolean;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
export function ProductCard({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
export function ProductCard({
|
|
23
|
+
product,
|
|
24
|
+
onClickProduct,
|
|
25
|
+
onFavorite,
|
|
26
|
+
isFavorited = false,
|
|
27
|
+
showFavoriteButton = true,
|
|
28
|
+
className
|
|
29
|
+
}: ProductCardProps & { className?: string }) {
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const [isFavorite, setIsFavorite] = useState(isFavorited);
|
|
32
|
+
const { addToWishlist, removeFromWishlist, isInWishlist } = useWishlist();
|
|
33
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
34
|
+
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
|
35
|
+
|
|
36
|
+
// Handle image load state
|
|
37
|
+
const handleImageLoad = useCallback(() => {
|
|
38
|
+
setIsImageLoaded(true);
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
// Handle card click
|
|
42
|
+
const handleCardClick = useCallback((e: React.MouseEvent) => {
|
|
43
|
+
if (onClickProduct) {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
onClickProduct(product);
|
|
46
|
+
}
|
|
47
|
+
}, [onClickProduct, product]);
|
|
48
|
+
|
|
20
49
|
|
|
21
|
-
const
|
|
50
|
+
const handleFavorite = async (e: React.MouseEvent) => {
|
|
22
51
|
e.stopPropagation();
|
|
23
|
-
setIsAdding(true);
|
|
24
52
|
try {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
53
|
+
if (isInWishlist(product?._id || '')) {
|
|
54
|
+
await removeFromWishlist(product?._id || '');
|
|
55
|
+
setIsFavorite(false);
|
|
56
|
+
toast.success('Removed from wishlist');
|
|
57
|
+
} else {
|
|
58
|
+
await addToWishlist(product as unknown as Product);
|
|
59
|
+
setIsFavorite(true);
|
|
60
|
+
toast.success('Added to wishlist');
|
|
61
|
+
}
|
|
62
|
+
onFavorite?.(product);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Error updating wishlist:', error);
|
|
65
|
+
toast.error('Failed to update wishlist');
|
|
28
66
|
}
|
|
29
67
|
};
|
|
30
68
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
69
|
+
// Update favorite state when props or wishlist changes
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setIsFavorite(isInWishlist(product?._id || '') || isFavorited);
|
|
72
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
73
|
+
}, [isFavorited, isInWishlist, product?._id]);
|
|
74
|
+
|
|
75
|
+
// Handle keyboard navigation
|
|
76
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
77
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
handleCardClick(e as any);
|
|
80
|
+
}
|
|
34
81
|
};
|
|
35
82
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
83
|
+
|
|
84
|
+
const imageSource = useMemo(() => {
|
|
85
|
+
return {
|
|
86
|
+
src: product.productMedia?.[0]?.file || '/placeholder-product.jpg',
|
|
87
|
+
alt: product.name || 'Product image'
|
|
88
|
+
};
|
|
89
|
+
}, [product.productMedia, product.name]);
|
|
90
|
+
|
|
91
|
+
|
|
39
92
|
|
|
40
93
|
return (
|
|
41
|
-
<motion.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
94
|
+
<motion.article
|
|
95
|
+
className=
|
|
96
|
+
"relative group bg-white rounded-xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-300 border border-gray-100 hover:border-gray-200 flex h-[420px] flex-col"
|
|
97
|
+
|
|
98
|
+
whileHover={{ y: -4 }}
|
|
99
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
100
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
101
|
+
aria-labelledby={`product-${product.id}-title`}
|
|
102
|
+
role="article"
|
|
103
|
+
tabIndex={0}
|
|
104
|
+
onClick={handleCardClick}
|
|
105
|
+
onKeyDown={handleKeyDown}
|
|
45
106
|
>
|
|
46
107
|
{/* Image Container */}
|
|
47
|
-
<div className="relative
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
108
|
+
<div className="relative h-48 w-full overflow-hidden bg-gray-50">
|
|
109
|
+
<AnimatePresence>
|
|
110
|
+
{!isImageLoaded && (
|
|
111
|
+
<motion.div
|
|
112
|
+
className="absolute inset-0 bg-gray-200 animate-pulse"
|
|
113
|
+
initial={{ opacity: 1 }}
|
|
114
|
+
exit={{ opacity: 0 }}
|
|
115
|
+
transition={{ duration: 0.2 }}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</AnimatePresence>
|
|
119
|
+
|
|
120
|
+
{product.productMedia?.[0]?.file && (
|
|
121
|
+
<Image
|
|
122
|
+
src={product.productMedia?.[0]?.file || '/placeholder-product.jpg'}
|
|
123
|
+
alt={product.name || 'Product image'}
|
|
124
|
+
fill
|
|
125
|
+
className={`h-full w-full object-cover object-center transition-opacity duration-300 ${product.inventoryCount === 0 ? 'opacity-60' : ''
|
|
126
|
+
} ${isImageLoaded ? 'opacity-100' : 'opacity-0'}`}
|
|
127
|
+
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
|
|
128
|
+
priority={false}
|
|
129
|
+
onLoad={handleImageLoad}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
|
|
55
133
|
{/* Badges */}
|
|
56
|
-
<div className="absolute top-
|
|
57
|
-
{
|
|
58
|
-
<span
|
|
59
|
-
|
|
60
|
-
|
|
134
|
+
<div className="absolute top-3 left-3 flex flex-col gap-2 z-10">
|
|
135
|
+
{product.isDiscounted && (
|
|
136
|
+
<motion.span
|
|
137
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
138
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
139
|
+
className="inline-flex items-center justify-center px-2.5 py-1 rounded-full text-xs font-bold text-white bg-green-600 shadow-md"
|
|
140
|
+
>
|
|
141
|
+
-{product.discountAmount}%
|
|
142
|
+
</motion.span>
|
|
61
143
|
)}
|
|
62
|
-
|
|
63
|
-
|
|
144
|
+
|
|
145
|
+
{product.inventoryCount === 0 && (
|
|
146
|
+
<motion.span
|
|
147
|
+
initial={{ scale: 0.9, opacity: 0 }}
|
|
148
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
149
|
+
className="inline-flex items-center justify-center px-2.5 py-1 rounded-full text-xs font-bold text-white bg-red-600"
|
|
150
|
+
>
|
|
64
151
|
Out of Stock
|
|
65
|
-
</span>
|
|
152
|
+
</motion.span>
|
|
66
153
|
)}
|
|
67
154
|
</div>
|
|
68
155
|
|
|
69
156
|
{/* Favorite Button */}
|
|
70
|
-
|
|
71
|
-
onClick={handleFavorite}
|
|
72
|
-
className="absolute top-4 right-4 p-2 bg-white/90 backdrop-blur-sm rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110"
|
|
73
|
-
>
|
|
74
|
-
<Heart
|
|
75
|
-
className={`w-5 h-5 ${isFavorited ? 'fill-red-500 text-red-500' : 'text-gray-700'}`}
|
|
76
|
-
/>
|
|
77
|
-
</button>
|
|
78
|
-
|
|
79
|
-
{/* Quick Add Button - Shows on hover */}
|
|
80
|
-
{product.inStock && (
|
|
157
|
+
{showFavoriteButton && (
|
|
81
158
|
<motion.button
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
className=
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={handleFavorite}
|
|
161
|
+
className={
|
|
162
|
+
`absolute top-2 right-2 p-2 rounded-full z-10 transition-colors bg-white/90 backdrop-blur-sm shadow-md hover:shadow-lg ${isFavorite ? 'text-red-500' : 'text-primary-600 hover:text-red-500'} ${isHovered || isFavorite ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`
|
|
163
|
+
}
|
|
164
|
+
whileHover={{ scale: 1.1 }}
|
|
85
165
|
whileTap={{ scale: 0.95 }}
|
|
166
|
+
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
86
167
|
>
|
|
87
|
-
<
|
|
88
|
-
{isAdding ? 'Adding...' : 'Quick Add'}
|
|
168
|
+
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-current' : ''}`} />
|
|
89
169
|
</motion.button>
|
|
90
170
|
)}
|
|
171
|
+
|
|
172
|
+
</div>
|
|
173
|
+
<div className="absolute top-4 left-4 flex flex-col gap-2">
|
|
174
|
+
{product.inventoryCount === 0 && (
|
|
175
|
+
<span className="px-3 py-1 rounded-full text-sm font-bold bg-red-100 text-red-800">
|
|
176
|
+
Out of Stock
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
91
179
|
</div>
|
|
92
180
|
|
|
93
|
-
{/*
|
|
181
|
+
{/* Favorite Button */}
|
|
182
|
+
{showFavoriteButton && (
|
|
183
|
+
<button
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={handleFavorite}
|
|
186
|
+
className={`absolute top-2 right-2 p-2 rounded-full transition-colors ${isFavorite ? 'text-red-500' : 'text-gray-400 hover:text-red-500'
|
|
187
|
+
}`}
|
|
188
|
+
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
189
|
+
>
|
|
190
|
+
<Heart className={`w-5 h-5 ${isFavorite ? 'fill-current' : ''}`} />
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
|
|
94
194
|
<div className="p-4">
|
|
95
195
|
{/* Category */}
|
|
96
|
-
{product.
|
|
196
|
+
{product.parentCategories && product.parentCategories?.length > 0 && (
|
|
97
197
|
<p className="text-xs text-gray-500 uppercase tracking-wider mb-2">
|
|
98
|
-
{product.category}
|
|
198
|
+
{product.parentCategories?.map((category) => category?.name).join(', ') || 'No categories'}
|
|
99
199
|
</p>
|
|
100
200
|
)}
|
|
101
201
|
|
|
102
|
-
{/* Product Name */}
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
202
|
+
{/* Product Name and Variant */}
|
|
203
|
+
<div className="mb-2">
|
|
204
|
+
<h3 className="text-lg font-semibold text-gray-900 line-clamp-1 group-hover:text-primary-600 transition-colors">
|
|
205
|
+
{product.name}
|
|
206
|
+
</h3>
|
|
207
|
+
{product?.sku && (
|
|
208
|
+
<p className="text-xs text-gray-400 mt-1">SKU: {product.sku}</p>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
106
211
|
|
|
107
212
|
{/* Price */}
|
|
108
213
|
<div className="flex items-baseline gap-2">
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
214
|
+
<div className="flex flex-col">
|
|
215
|
+
<span className="text-2xl font-bold text-gray-900">
|
|
216
|
+
{formatPrice(product.finalPrice)}
|
|
217
|
+
</span>
|
|
218
|
+
{product.inventoryCount > 0 && (
|
|
219
|
+
<span className="text-xs text-gray-500">
|
|
220
|
+
{product.inventoryCount > 0 ? "In Stock" : "Out of Stock"}
|
|
221
|
+
</span>
|
|
222
|
+
)}
|
|
223
|
+
</div>
|
|
224
|
+
{product.priceBeforeDiscount && product.priceBeforeDiscount > product.finalPrice && (
|
|
113
225
|
<span className="text-sm text-gray-500 line-through">
|
|
114
|
-
{formatPrice(product.
|
|
226
|
+
{formatPrice(product.priceBeforeDiscount)}
|
|
115
227
|
</span>
|
|
116
228
|
)}
|
|
117
229
|
</div>
|
|
118
230
|
</div>
|
|
119
|
-
|
|
231
|
+
<div className="mt-auto p-4 pt-0">
|
|
232
|
+
<button
|
|
233
|
+
type="button"
|
|
234
|
+
onClick={(e) => {
|
|
235
|
+
e.stopPropagation();
|
|
236
|
+
router.push(`/products/${product._id}`);
|
|
237
|
+
}}
|
|
238
|
+
className={`w-full flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium bg-primary-600 hover:bg-primary-700 text-white`}
|
|
239
|
+
>
|
|
240
|
+
View Product
|
|
241
|
+
</button>
|
|
242
|
+
|
|
243
|
+
{showFavoriteButton && (
|
|
244
|
+
<button
|
|
245
|
+
type="button"
|
|
246
|
+
onClick={handleFavorite}
|
|
247
|
+
className="mt-2 w-full flex items-center justify-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-primary-600 hover:bg-gray-50"
|
|
248
|
+
aria-label={isFavorite ? 'Remove from wishlist' : 'Add to wishlist'}
|
|
249
|
+
>
|
|
250
|
+
<Heart
|
|
251
|
+
className={`mr-2 h-4 w-4 ${isFavorite ? 'fill-red-500 text-red-500' : 'text-primary-600'}`}
|
|
252
|
+
/>
|
|
253
|
+
{isFavorite ? 'Saved' : 'Save for later'}
|
|
254
|
+
</button>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
</motion.article>
|
|
120
258
|
);
|
|
121
259
|
}
|
|
122
|
-
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
1
3
|
import React from 'react';
|
|
2
|
-
import
|
|
4
|
+
import dynamic from 'next/dynamic';
|
|
5
|
+
|
|
6
|
+
const MotionDiv = dynamic(() => import('framer-motion').then((mod) => mod.motion.div), {
|
|
7
|
+
ssr: false,
|
|
8
|
+
});
|
|
3
9
|
|
|
4
10
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
11
|
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
|
|
@@ -17,7 +23,7 @@ export function Button({
|
|
|
17
23
|
children,
|
|
18
24
|
...props
|
|
19
25
|
}: ButtonProps) {
|
|
20
|
-
const baseStyles = 'font-medium rounded-lg transition-all duration-200 inline-flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed';
|
|
26
|
+
const baseStyles = 'font-medium rounded-lg transition-all duration-200 inline-flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary-500';
|
|
21
27
|
|
|
22
28
|
const variants = {
|
|
23
29
|
primary: 'bg-primary-600 text-white hover:bg-primary-700 shadow-lg shadow-primary-500/30 hover:shadow-xl hover:shadow-primary-500/40',
|
|
@@ -33,7 +39,7 @@ export function Button({
|
|
|
33
39
|
};
|
|
34
40
|
|
|
35
41
|
return (
|
|
36
|
-
<
|
|
42
|
+
<MotionDiv
|
|
37
43
|
whileHover={{ scale: 1.02 }}
|
|
38
44
|
whileTap={{ scale: 0.98 }}
|
|
39
45
|
className="inline-block"
|
|
@@ -41,11 +47,13 @@ export function Button({
|
|
|
41
47
|
<button
|
|
42
48
|
className={`${baseStyles} ${variants[variant]} ${sizes[size]} ${className}`}
|
|
43
49
|
disabled={disabled || isLoading}
|
|
50
|
+
aria-disabled={disabled || isLoading}
|
|
51
|
+
aria-busy={isLoading}
|
|
44
52
|
{...props}
|
|
45
53
|
>
|
|
46
54
|
{isLoading ? (
|
|
47
55
|
<>
|
|
48
|
-
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
56
|
+
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" aria-hidden="true">
|
|
49
57
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
50
58
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
51
59
|
</svg>
|
|
@@ -55,7 +63,7 @@ export function Button({
|
|
|
55
63
|
children
|
|
56
64
|
)}
|
|
57
65
|
</button>
|
|
58
|
-
</
|
|
66
|
+
</MotionDiv>
|
|
59
67
|
);
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
// Base card component
|
|
4
|
+
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Card({ className = '', ...props }: CardProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={`rounded-lg border bg-white shadow-sm ${className}`}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Card Header component
|
|
18
|
+
interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
19
|
+
|
|
20
|
+
export function CardHeader({ className = '', ...props }: CardHeaderProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className={`flex flex-col space-y-1.5 p-6 ${className}`}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Card Title component
|
|
30
|
+
interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
|
|
31
|
+
|
|
32
|
+
export function CardTitle({ className = '', ...props }: CardTitleProps) {
|
|
33
|
+
return (
|
|
34
|
+
<h3
|
|
35
|
+
className={`text-2xl font-semibold leading-none tracking-tight ${className}`}
|
|
36
|
+
{...props}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Card Content component
|
|
42
|
+
interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
43
|
+
|
|
44
|
+
export function CardContent({ className = '', ...props }: CardContentProps) {
|
|
45
|
+
return <div className={`p-6 pt-0 ${className}`} {...props} />;
|
|
46
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Address, AddressesApi, CreateAddressDto, UpdateAddressDto } from '@/lib/Apis';
|
|
3
|
+
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
4
|
+
|
|
5
|
+
interface UseAddressesReturn {
|
|
6
|
+
addresses: Address[];
|
|
7
|
+
defaultAddress: Address | null;
|
|
8
|
+
isLoading: boolean;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
refresh: () => Promise<void>;
|
|
11
|
+
addAddress: (payload: CreateAddressDto) => Promise<Address>;
|
|
12
|
+
updateAddress: (id: string, payload: UpdateAddressDto) => Promise<Address>;
|
|
13
|
+
removeAddress: (id: string) => Promise<void>;
|
|
14
|
+
markAsDefault: (id: string) => Promise<Address>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useAddresses(): UseAddressesReturn {
|
|
18
|
+
const [addresses, setAddresses] = useState<Address[]>([]);
|
|
19
|
+
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const refresh = useCallback(async () => {
|
|
23
|
+
setIsLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
const response = await new AddressesApi(AXIOS_CONFIG).getMyAddresses();
|
|
26
|
+
setAddresses(response.data || []);
|
|
27
|
+
setIsLoading(false);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
refresh();
|
|
32
|
+
}, [refresh]);
|
|
33
|
+
|
|
34
|
+
const sortedAddresses = useMemo(() => {
|
|
35
|
+
return [...addresses].sort((a, b) => {
|
|
36
|
+
if (a.isDefault === b.isDefault) {
|
|
37
|
+
return (b.updatedAt.toISOString() || '').localeCompare(a.updatedAt.toISOString() || '');
|
|
38
|
+
}
|
|
39
|
+
return a.isDefault ? -1 : 1;
|
|
40
|
+
});
|
|
41
|
+
}, [addresses]);
|
|
42
|
+
|
|
43
|
+
const defaultAddress = useMemo(
|
|
44
|
+
() => sortedAddresses.find((address) => address.isDefault) || null,
|
|
45
|
+
[sortedAddresses]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const addAddress = useCallback(async (payload: CreateAddressDto) => {
|
|
49
|
+
const response = await new AddressesApi(AXIOS_CONFIG).createAddressForUser(payload);
|
|
50
|
+
setAddresses((prev) => [...prev, response.data]);
|
|
51
|
+
return response.data;
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
const updateAddress = useCallback(async (id: string, payload: UpdateAddressDto) => {
|
|
55
|
+
const response = await new AddressesApi(AXIOS_CONFIG).updateUserAddress(payload, id);
|
|
56
|
+
setAddresses((prev) => prev.map((address) => address.id === id ? response.data : address));
|
|
57
|
+
return response.data;
|
|
58
|
+
}, []);
|
|
59
|
+
|
|
60
|
+
const removeAddress = useCallback(async (id: string) => {
|
|
61
|
+
await new AddressesApi(AXIOS_CONFIG).deleteUserAddress(id);
|
|
62
|
+
setAddresses((prev) => prev.filter((address) => address.id !== id));
|
|
63
|
+
return
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
const markAsDefault = useCallback(async (id: string) => {
|
|
67
|
+
const response = await new AddressesApi(AXIOS_CONFIG).updateDefaultAddress(id);
|
|
68
|
+
setAddresses((prev) => prev.map((address) => address.id === id ? response.data : address));
|
|
69
|
+
return response.data;
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
addresses: sortedAddresses,
|
|
74
|
+
defaultAddress,
|
|
75
|
+
isLoading,
|
|
76
|
+
error,
|
|
77
|
+
refresh,
|
|
78
|
+
addAddress,
|
|
79
|
+
updateAddress,
|
|
80
|
+
removeAddress,
|
|
81
|
+
markAsDefault,
|
|
82
|
+
};
|
|
83
|
+
}
|
package/src/hooks/useOrders.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { OrdersApi } from '@/lib/Apis';
|
|
3
|
+
import { getApiConfiguration } from '@/lib/api-adapter';
|
|
4
|
+
import { PopulatedOrder } from '@/lib/Apis/models';
|
|
5
|
+
import { useAuth } from '@/providers/AuthProvider';
|
|
4
6
|
|
|
5
|
-
export function useOrders(
|
|
6
|
-
|
|
7
|
+
export function useOrders(
|
|
8
|
+
page: number = 1,
|
|
9
|
+
limit: number = 10,
|
|
10
|
+
orderStatus?: string,
|
|
11
|
+
paymentStatus?: string
|
|
12
|
+
) {
|
|
13
|
+
const [orders, setOrders] = useState<PopulatedOrder[]>([]);
|
|
7
14
|
const [isLoading, setIsLoading] = useState(true);
|
|
8
15
|
const [error, setError] = useState<Error | null>(null);
|
|
9
16
|
const [pagination, setPagination] = useState({
|
|
@@ -12,20 +19,36 @@ export function useOrders(page: number = 1, limit: number = 10) {
|
|
|
12
19
|
total: 0,
|
|
13
20
|
totalPages: 0,
|
|
14
21
|
});
|
|
22
|
+
const { user } = useAuth();
|
|
23
|
+
const resolvedUserId = (user as any)?._id || (user as any)?.id;
|
|
15
24
|
|
|
16
25
|
const fetchOrders = useCallback(async () => {
|
|
17
26
|
setIsLoading(true);
|
|
18
27
|
setError(null);
|
|
19
28
|
try {
|
|
20
|
-
const response = await
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
const response = await new OrdersApi(getApiConfiguration()).getAllOrders(
|
|
30
|
+
paymentStatus && paymentStatus !== 'All' ? paymentStatus : undefined,
|
|
31
|
+
orderStatus && orderStatus !== 'All' ? orderStatus : undefined,
|
|
32
|
+
page,
|
|
33
|
+
limit,
|
|
34
|
+
undefined,
|
|
35
|
+
undefined,
|
|
36
|
+
undefined,
|
|
37
|
+
resolvedUserId
|
|
38
|
+
);
|
|
39
|
+
setOrders(response.data.data || []);
|
|
40
|
+
setPagination({
|
|
41
|
+
page: response.data.page || page,
|
|
42
|
+
limit: response.data.limit || limit,
|
|
43
|
+
total: response.data.total ?? 0,
|
|
44
|
+
totalPages: response.data.totalPages || 1,
|
|
45
|
+
});
|
|
23
46
|
} catch (err) {
|
|
24
47
|
setError(err as Error);
|
|
25
48
|
} finally {
|
|
26
49
|
setIsLoading(false);
|
|
27
50
|
}
|
|
28
|
-
}, [page, limit]);
|
|
51
|
+
}, [page, limit, resolvedUserId, orderStatus, paymentStatus]);
|
|
29
52
|
|
|
30
53
|
useEffect(() => {
|
|
31
54
|
fetchOrders();
|
|
@@ -41,7 +64,7 @@ export function useOrders(page: number = 1, limit: number = 10) {
|
|
|
41
64
|
}
|
|
42
65
|
|
|
43
66
|
export function useOrder(id: string) {
|
|
44
|
-
const [order, setOrder] = useState<
|
|
67
|
+
const [order, setOrder] = useState<PopulatedOrder | null>(null);
|
|
45
68
|
const [isLoading, setIsLoading] = useState(true);
|
|
46
69
|
const [error, setError] = useState<Error | null>(null);
|
|
47
70
|
|
|
@@ -49,10 +72,8 @@ export function useOrder(id: string) {
|
|
|
49
72
|
setIsLoading(true);
|
|
50
73
|
setError(null);
|
|
51
74
|
try {
|
|
52
|
-
const response = await
|
|
53
|
-
|
|
54
|
-
setOrder(response.data);
|
|
55
|
-
}
|
|
75
|
+
const response = await new OrdersApi(getApiConfiguration()).getSingleOrder(id);
|
|
76
|
+
setOrder(response.data);
|
|
56
77
|
} catch (err) {
|
|
57
78
|
setError(err as Error);
|
|
58
79
|
} finally {
|
|
@@ -70,7 +91,7 @@ export function useOrder(id: string) {
|
|
|
70
91
|
}
|
|
71
92
|
|
|
72
93
|
export function useCurrentOrders() {
|
|
73
|
-
const [orders, setOrders] = useState<
|
|
94
|
+
const [orders, setOrders] = useState<PopulatedOrder[]>([]);
|
|
74
95
|
const [isLoading, setIsLoading] = useState(true);
|
|
75
96
|
const [error, setError] = useState<Error | null>(null);
|
|
76
97
|
|
|
@@ -78,10 +99,8 @@ export function useCurrentOrders() {
|
|
|
78
99
|
setIsLoading(true);
|
|
79
100
|
setError(null);
|
|
80
101
|
try {
|
|
81
|
-
const response = await
|
|
82
|
-
|
|
83
|
-
setOrders(response.data);
|
|
84
|
-
}
|
|
102
|
+
const response = await new OrdersApi(getApiConfiguration()).getUserOrders();
|
|
103
|
+
setOrders(response.data || []);
|
|
85
104
|
} catch (err) {
|
|
86
105
|
setError(err as Error);
|
|
87
106
|
} finally {
|
|
@@ -95,4 +114,3 @@ export function useCurrentOrders() {
|
|
|
95
114
|
|
|
96
115
|
return { orders, isLoading, error, refetch: fetchCurrentOrders };
|
|
97
116
|
}
|
|
98
|
-
|