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
|
@@ -31,7 +31,6 @@ import { useAuth } from '@/providers/AuthProvider';
|
|
|
31
31
|
import { formatDate, formatPrice } from '@/lib/utils/format';
|
|
32
32
|
import { useNotification } from '@/providers/NotificationProvider';
|
|
33
33
|
import { useRouter } from 'next/navigation';
|
|
34
|
-
import { ProductVariantsApi } from '@/lib/Apis/apis/product-variants-api';
|
|
35
34
|
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
36
35
|
import { useWishlist } from '@/providers/WishlistProvider';
|
|
37
36
|
import { ProductsApi, ProductVariant, ProductVariantInventoryStatusEnum } from '@/lib/Apis';
|
|
@@ -84,13 +83,13 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
84
83
|
|
|
85
84
|
if (!productData) return null;
|
|
86
85
|
|
|
87
|
-
if (productData.
|
|
86
|
+
if (productData.variants?.length && selectedVariant) {
|
|
88
87
|
return {
|
|
89
88
|
...productData,
|
|
90
89
|
price: selectedVariant.finalPrice,
|
|
91
90
|
inStock: selectedVariant.isActive,
|
|
92
91
|
sku: selectedVariant.sku || productData.sku,
|
|
93
|
-
variantId: selectedVariant.
|
|
92
|
+
variantId: selectedVariant._id,
|
|
94
93
|
variantName: selectedVariant.name
|
|
95
94
|
};
|
|
96
95
|
}
|
|
@@ -99,8 +98,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
99
98
|
}, [productData, selectedVariant, initialProductData]);
|
|
100
99
|
|
|
101
100
|
const getVariantImages = () => {
|
|
102
|
-
if (selectedVariant?.
|
|
103
|
-
return selectedVariant.
|
|
101
|
+
if (selectedVariant?.media?.length) {
|
|
102
|
+
return selectedVariant.media.map((media: any) => ({
|
|
104
103
|
src: media.file,
|
|
105
104
|
width: 800,
|
|
106
105
|
height: 1000,
|
|
@@ -111,8 +110,8 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
// Fallback to product media if no variant media
|
|
114
|
-
if (product?.
|
|
115
|
-
return product.
|
|
113
|
+
if (product?.media?.length) {
|
|
114
|
+
return product.media.map((media: any) => ({
|
|
116
115
|
src: media.file,
|
|
117
116
|
width: 800,
|
|
118
117
|
height: 1000,
|
|
@@ -122,14 +121,14 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
122
121
|
}));
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
if (product?.
|
|
126
|
-
return product.
|
|
127
|
-
src:
|
|
124
|
+
if (product?.media?.length) {
|
|
125
|
+
return product.media.map((media: any) => ({
|
|
126
|
+
src: media.file,
|
|
128
127
|
width: 800,
|
|
129
128
|
height: 1000,
|
|
130
129
|
alt: product?.name || 'Product image',
|
|
131
|
-
url:
|
|
132
|
-
file:
|
|
130
|
+
url: media.file,
|
|
131
|
+
file: media.file,
|
|
133
132
|
type: 'image' as const
|
|
134
133
|
}));
|
|
135
134
|
}
|
|
@@ -189,26 +188,19 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
189
188
|
}, [product?.productVariants]);
|
|
190
189
|
|
|
191
190
|
useEffect(() => {
|
|
192
|
-
if (!product?.
|
|
191
|
+
if (!product?._id) return;
|
|
193
192
|
|
|
194
|
-
let isMounted = true;
|
|
195
193
|
const fetchRelated = async () => {
|
|
196
194
|
try {
|
|
197
|
-
const response = await new ProductsApi(AXIOS_CONFIG).getRelatedProducts(product.
|
|
198
|
-
|
|
199
|
-
limit: 4
|
|
200
|
-
}
|
|
201
|
-
});
|
|
195
|
+
const response = await new ProductsApi(AXIOS_CONFIG).getRelatedProducts(product._id, 4);
|
|
196
|
+
console.log("response.data", response.data);
|
|
202
197
|
setRelatedProducts(response.data || []);
|
|
203
198
|
} catch (error: any) {
|
|
204
199
|
console.error('Failed to fetch related products', error);
|
|
205
200
|
}
|
|
206
201
|
};
|
|
207
202
|
fetchRelated();
|
|
208
|
-
|
|
209
|
-
isMounted = false;
|
|
210
|
-
};
|
|
211
|
-
}, [product?.id]);
|
|
203
|
+
}, [product?._id]);
|
|
212
204
|
|
|
213
205
|
|
|
214
206
|
const handleVariantSelect = async (variant: ProductVariant) => {
|
|
@@ -232,9 +224,9 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
232
224
|
setIsAddingToCart(true);
|
|
233
225
|
try {
|
|
234
226
|
await addToCart(
|
|
235
|
-
product.
|
|
227
|
+
product._id,
|
|
236
228
|
quantity,
|
|
237
|
-
selectedVariant.
|
|
229
|
+
selectedVariant._id
|
|
238
230
|
);
|
|
239
231
|
notification.success(
|
|
240
232
|
'Added to cart',
|
|
@@ -256,7 +248,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
256
248
|
// Initialize isFavorited based on whether the product is in the wishlist
|
|
257
249
|
useEffect(() => {
|
|
258
250
|
if (product) {
|
|
259
|
-
setIsFavorited(isInWishlist(product.
|
|
251
|
+
setIsFavorited(isInWishlist(product._id));
|
|
260
252
|
}
|
|
261
253
|
}, [product, isInWishlist]);
|
|
262
254
|
|
|
@@ -265,7 +257,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
265
257
|
|
|
266
258
|
try {
|
|
267
259
|
if (isFavorited) {
|
|
268
|
-
await removeFromWishlist(product.
|
|
260
|
+
await removeFromWishlist(product._id);
|
|
269
261
|
notification.info(
|
|
270
262
|
'Removed from saved items',
|
|
271
263
|
`${product.name} was removed from your saved items.`
|
|
@@ -362,7 +354,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
362
354
|
<section className="rounded-3xl ">
|
|
363
355
|
<div className="space-y-6">
|
|
364
356
|
<motion.div
|
|
365
|
-
key={selectedVariant?.
|
|
357
|
+
key={selectedVariant?._id || 'default'}
|
|
366
358
|
initial={{ opacity: 0.4, scale: 0.98 }}
|
|
367
359
|
animate={{ opacity: 1, scale: 1 }}
|
|
368
360
|
transition={{ duration: 0.35 }}
|
|
@@ -447,7 +439,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
447
439
|
{/* Category Path */}
|
|
448
440
|
<div className="mb-4">
|
|
449
441
|
<p className="font-['Poppins',sans-serif] text-[12px] text-primary uppercase tracking-wide font-medium mb-2">
|
|
450
|
-
{product.brand} • {product.
|
|
442
|
+
{product.brand} • {product.categoryIds?.[0]?.name || 'Uncategorized'}
|
|
451
443
|
</p>
|
|
452
444
|
<h1 className="text-3xl font-['Poppins',sans-serif] font-semibold text-secondary tracking-[-1.5px] mb-3">
|
|
453
445
|
{selectedVariant?.name || product.name}
|
|
@@ -469,38 +461,37 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
469
461
|
{product.rating} ({product.reviewCount ? product.reviewCount : 0} reviews)
|
|
470
462
|
</span>
|
|
471
463
|
</div>
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
</span>
|
|
482
|
-
<div className="px-3 py-1 rounded-full bg-[#E67E50]/10">
|
|
483
|
-
<span className="font-['Poppins',sans-serif] font-semibold text-[13px] text-[#E67E50]">
|
|
484
|
-
Save ${formatPrice(variantComparePrice - variantPrice)}
|
|
464
|
+
{selectedVariant && (
|
|
465
|
+
<div className="flex items-center gap-3 mb-6 pb-6 border-b-2 border-gray-100">
|
|
466
|
+
<span className="font-['Poppins',sans-serif] font-bold text-[40px] text-[#E67E50]">
|
|
467
|
+
${variantPrice.toFixed(2)}
|
|
468
|
+
</span>
|
|
469
|
+
{variantComparePrice && variantComparePrice > variantPrice && (
|
|
470
|
+
<>
|
|
471
|
+
<span className="font-['Poppins',sans-serif] text-[24px] text-muted line-through">
|
|
472
|
+
${variantComparePrice.toFixed(2)}
|
|
485
473
|
</span>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
474
|
+
<div className="px-3 py-1 rounded-full bg-[#E67E50]/10">
|
|
475
|
+
<span className="font-['Poppins',sans-serif] font-semibold text-[13px] text-[#E67E50]">
|
|
476
|
+
Save ${formatPrice(variantComparePrice - variantPrice)}
|
|
477
|
+
</span>
|
|
478
|
+
</div>
|
|
479
|
+
</>
|
|
480
|
+
)}
|
|
481
|
+
</div>
|
|
482
|
+
)}
|
|
491
483
|
{/* Stock Status */}
|
|
492
484
|
{selectedVariant && (
|
|
493
485
|
<div className="mb-6">
|
|
494
486
|
<div className="flex items-center gap-2 mb-2">
|
|
495
|
-
{selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK
|
|
496
|
-
selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK ? (
|
|
487
|
+
{selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK ? (
|
|
497
488
|
<>
|
|
498
489
|
<div className="size-3 rounded-full bg-red-500" />
|
|
499
490
|
<span className="font-['Poppins',sans-serif] text-[13px] text-red-600 font-medium">
|
|
500
491
|
Out of Stock
|
|
501
492
|
</span>
|
|
502
493
|
</>
|
|
503
|
-
) : selectedVariant.inventoryCount <= 10 ? (
|
|
494
|
+
) : selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.LOWSTOCK || selectedVariant.inventoryCount <= 10 ? (
|
|
504
495
|
<>
|
|
505
496
|
<div className="size-3 rounded-full bg-primary animate-pulse" />
|
|
506
497
|
<span className="font-['Poppins',sans-serif] text-[13px] text-primary font-medium">
|
|
@@ -518,7 +509,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
518
509
|
|
|
519
510
|
</div>
|
|
520
511
|
<p className="font-['Poppins',sans-serif] text-[12px] text-muted">
|
|
521
|
-
SKU: {selectedVariant?.sku}
|
|
512
|
+
SKU: {selectedVariant?.sku || product?.sku}
|
|
522
513
|
</p>
|
|
523
514
|
</div>
|
|
524
515
|
)}
|
|
@@ -532,21 +523,21 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
532
523
|
)}
|
|
533
524
|
|
|
534
525
|
{/* Variant Selector with Images */}
|
|
535
|
-
{product?.
|
|
526
|
+
{product?.variants && product.variants.length > 0 && (
|
|
536
527
|
<div className="mb-6">
|
|
537
528
|
<h3 className="font-['Poppins',sans-serif] font-semibold text-[14px] text-secondary mb-3">
|
|
538
529
|
Select Variant
|
|
539
530
|
</h3>
|
|
540
531
|
<div className="flex flex-wrap gap-3">
|
|
541
|
-
{product.
|
|
542
|
-
const isSelected = selectedVariant?.
|
|
543
|
-
const variantImage = variant.
|
|
544
|
-
|| product.
|
|
532
|
+
{product.variants.map((variant: ProductVariant) => {
|
|
533
|
+
const isSelected = selectedVariant?._id === variant._id;
|
|
534
|
+
const variantImage = variant.media?.[0]?.file
|
|
535
|
+
|| product.media?.[0]?.file
|
|
545
536
|
|| product.images?.[0]
|
|
546
537
|
|| '/placeholder-product.jpg';
|
|
547
538
|
return (
|
|
548
539
|
<button
|
|
549
|
-
key={variant.
|
|
540
|
+
key={variant._id}
|
|
550
541
|
type="button"
|
|
551
542
|
onClick={() => handleVariantSelect(variant)}
|
|
552
543
|
className={`flex items-start gap-3 px-4 py-2.5 rounded-xl border-2 transition-all ${isSelected
|
|
@@ -623,7 +614,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
623
614
|
<button
|
|
624
615
|
className="flex-1 font-['Poppins',sans-serif] font-medium text-[14px] px-8 py-4 rounded-full transition-all duration-300 flex items-center justify-center gap-3 bg-[#E67E50] text-white hover:bg-[#d66f45] hover:shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
|
|
625
616
|
onClick={handleAddToCart}
|
|
626
|
-
disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK}
|
|
617
|
+
disabled={!selectedVariant || selectedVariant.inventoryStatus === ProductVariantInventoryStatusEnum.OUTOFSTOCK || isAddingToCart}
|
|
627
618
|
>
|
|
628
619
|
{isAddingToCart ? (
|
|
629
620
|
<>
|
|
@@ -631,7 +622,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
631
622
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
|
632
623
|
<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>
|
|
633
624
|
</svg>
|
|
634
|
-
|
|
625
|
+
Adding...
|
|
635
626
|
</>
|
|
636
627
|
) : (
|
|
637
628
|
<>
|
|
@@ -787,7 +778,7 @@ export function ProductDetailScreen({ productId }: ProductDetailScreenProps) {
|
|
|
787
778
|
{relatedProducts.map(relatedProduct => (
|
|
788
779
|
|
|
789
780
|
<ProductCard
|
|
790
|
-
key={relatedProduct.
|
|
781
|
+
key={relatedProduct._id}
|
|
791
782
|
product={relatedProduct}
|
|
792
783
|
// viewMode="grid"
|
|
793
784
|
onClickProduct={(item) => {
|
|
@@ -25,8 +25,8 @@ import { useBasePath } from '@/providers/BasePathProvider';
|
|
|
25
25
|
|
|
26
26
|
const registerSchema = z
|
|
27
27
|
.object({
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
firstname: z.string().min(2, 'First name is required'),
|
|
29
|
+
lastname: z.string().min(2, 'Last name is required'),
|
|
30
30
|
email: z.string().email('Enter a valid email'),
|
|
31
31
|
phone: z.string().optional(),
|
|
32
32
|
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
@@ -108,44 +108,44 @@ export function RegisterScreen() {
|
|
|
108
108
|
onSubmit={handleSubmit(onSubmit)}
|
|
109
109
|
className="text-start space-y-6 rounded-3xl border border-slate-100 bg-white p-8 shadow-lg shadow-primary-50"
|
|
110
110
|
style={{
|
|
111
|
-
|
|
112
|
-
|
|
111
|
+
boxShadow: '0px 4px 6px -4px #0000001A, 0px 10px 15px -3px #0000001A',
|
|
112
|
+
}}
|
|
113
113
|
>
|
|
114
114
|
<div className="grid gap-4 md:grid-cols-2 text-start">
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
<div>
|
|
116
|
+
<h2 className="text-sm text-secondary mb-3">First name <span className='text-primary-500'>*</span></h2>
|
|
117
|
+
<Input
|
|
118
|
+
placeholder="Alex"
|
|
119
|
+
{...register('firstname')}
|
|
120
|
+
error={errors.firstname?.message}
|
|
121
|
+
/>
|
|
122
122
|
</div>
|
|
123
123
|
<div>
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
124
|
+
<h2 className="text-sm text-secondary mb-3">Last name <span className='text-primary-500'>*</span></h2>
|
|
125
|
+
<Input
|
|
126
|
+
placeholder="Morgan"
|
|
127
|
+
{...register('lastname')}
|
|
128
|
+
error={errors.lastname?.message}
|
|
129
|
+
/>
|
|
130
130
|
</div>
|
|
131
131
|
</div>
|
|
132
132
|
<div>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
<h2 className="text-sm text-secondary mb-3">Email Address <span className='text-primary-500'>*</span></h2>
|
|
134
|
+
<Input
|
|
135
|
+
type="email"
|
|
136
|
+
placeholder="you@example.com"
|
|
137
|
+
{...register('email')}
|
|
138
|
+
error={errors.email?.message}
|
|
139
|
+
/>
|
|
140
140
|
</div>
|
|
141
141
|
<div>
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
142
|
+
<h2 className="text-sm text-secondary mb-3">Phone Number</h2>
|
|
143
|
+
<Input
|
|
144
|
+
type="tel"
|
|
145
|
+
placeholder="+1 (555) 123-4567"
|
|
146
|
+
{...register('phone')}
|
|
147
|
+
error={errors.phone?.message}
|
|
148
|
+
/>
|
|
149
149
|
</div>
|
|
150
150
|
<div className="relative">
|
|
151
151
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from 'react';
|
|
4
|
+
import { motion } from 'framer-motion';
|
|
5
|
+
import { useForm } from 'react-hook-form';
|
|
6
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { useRouter, useParams } from 'next/navigation';
|
|
9
|
+
import Link from 'next/link';
|
|
10
|
+
import { Eye, EyeOff, Lock, ShieldCheck } from 'lucide-react';
|
|
11
|
+
import { Input } from '@/components/ui/Input';
|
|
12
|
+
import { Button } from '@/components/ui/Button';
|
|
13
|
+
import { AuthApi } from '@/lib/Apis/apis/auth-api';
|
|
14
|
+
import { AXIOS_CONFIG } from '@/lib/Apis/sharedConfig';
|
|
15
|
+
import { useBasePath } from '@/providers/BasePathProvider';
|
|
16
|
+
|
|
17
|
+
const resetPasswordSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
|
20
|
+
confirmPassword: z.string().min(8, 'Confirm your new password'),
|
|
21
|
+
})
|
|
22
|
+
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
23
|
+
path: ['confirmPassword'],
|
|
24
|
+
message: 'Passwords do not match',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type ResetPasswordFormData = z.infer<typeof resetPasswordSchema>;
|
|
28
|
+
|
|
29
|
+
export function ResetPasswordScreen() {
|
|
30
|
+
const router = useRouter();
|
|
31
|
+
const { buildPath } = useBasePath();
|
|
32
|
+
const params = useParams();
|
|
33
|
+
const token = params?.token as string | undefined;
|
|
34
|
+
const storeId = params?.storeId as string | undefined;
|
|
35
|
+
const [showPassword, setShowPassword] = useState(false);
|
|
36
|
+
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
37
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
38
|
+
const [status, setStatus] = useState<{ type: 'success' | 'error'; message: string } | null>(
|
|
39
|
+
null
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const {
|
|
43
|
+
register,
|
|
44
|
+
handleSubmit,
|
|
45
|
+
formState: { errors },
|
|
46
|
+
} = useForm<ResetPasswordFormData>({
|
|
47
|
+
resolver: zodResolver(resetPasswordSchema),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!token) {
|
|
52
|
+
setStatus({
|
|
53
|
+
type: 'error',
|
|
54
|
+
message: 'Invalid or missing reset token. Please request a new password reset link.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}, [token]);
|
|
58
|
+
|
|
59
|
+
const onSubmit = async (data: ResetPasswordFormData) => {
|
|
60
|
+
if (!token) {
|
|
61
|
+
setStatus({
|
|
62
|
+
type: 'error',
|
|
63
|
+
message: 'Reset token is missing. Please use the link from your email.',
|
|
64
|
+
});
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setIsSubmitting(true);
|
|
69
|
+
setStatus(null);
|
|
70
|
+
try {
|
|
71
|
+
const authApi = new AuthApi(AXIOS_CONFIG);
|
|
72
|
+
await authApi.resetPassword(data.newPassword, token);
|
|
73
|
+
setStatus({
|
|
74
|
+
type: 'success',
|
|
75
|
+
message: 'Password reset successfully! Redirecting to login...',
|
|
76
|
+
});
|
|
77
|
+
setTimeout(() => router.push(buildPath('/login')), 2000);
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
setStatus({
|
|
80
|
+
type: 'error',
|
|
81
|
+
message:
|
|
82
|
+
error?.response?.data?.message ||
|
|
83
|
+
'Unable to reset password. The link may have expired.',
|
|
84
|
+
});
|
|
85
|
+
} finally {
|
|
86
|
+
setIsSubmitting(false);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div className="min-h-screen bg-linear-to-b from-[#F8FAFC] to-[#EBF4FB]">
|
|
92
|
+
<div className="grid min-h-screen overflow-hidden pb-12">
|
|
93
|
+
<motion.section
|
|
94
|
+
initial={{ opacity: 0, x: 24 }}
|
|
95
|
+
animate={{ opacity: 1, x: 0 }}
|
|
96
|
+
transition={{ duration: 0.4 }}
|
|
97
|
+
className="flex items-center justify-center px-6 py-12 lg:px-16"
|
|
98
|
+
>
|
|
99
|
+
<div className="w-full max-w-lg space-y-10 text-center">
|
|
100
|
+
<div className="space-y-2">
|
|
101
|
+
<Lock
|
|
102
|
+
strokeWidth={2}
|
|
103
|
+
className="h-16 w-16 mx-auto text-white rounded-full bg-secondary m-2 mb-4 px-4"
|
|
104
|
+
/>
|
|
105
|
+
<h2 className="text-4xl text-secondary">Reset Password</h2>
|
|
106
|
+
<p className="text-sm text-muted">
|
|
107
|
+
Enter your new password below to reset your account password.
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<form
|
|
112
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
113
|
+
className="space-y-6 rounded-3xl border bg-white p-8"
|
|
114
|
+
style={{
|
|
115
|
+
boxShadow: '0px 4px 6px -4px #0000001A, 0px 10px 15px -3px #0000001A',
|
|
116
|
+
}}
|
|
117
|
+
>
|
|
118
|
+
{status && (
|
|
119
|
+
<div
|
|
120
|
+
className={`flex items-start gap-2 rounded-2xl border px-4 py-3 text-sm ${
|
|
121
|
+
status.type === 'success'
|
|
122
|
+
? 'border-green-200 bg-green-50 text-green-800'
|
|
123
|
+
: 'border-red-200 bg-red-50 text-red-700'
|
|
124
|
+
}`}
|
|
125
|
+
>
|
|
126
|
+
<span className="mt-[2px] text-base">{status.type === 'success' ? '✔' : '!'}</span>
|
|
127
|
+
<span>{status.message}</span>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<div className="relative text-start text-secondary">
|
|
132
|
+
<h2 className="text-sm text-secondary mb-3">
|
|
133
|
+
New Password <span className="text-primary-500">*</span>
|
|
134
|
+
</h2>
|
|
135
|
+
<Input
|
|
136
|
+
type={showPassword ? 'text' : 'password'}
|
|
137
|
+
placeholder="Enter new password (min. 8 characters)"
|
|
138
|
+
{...register('newPassword')}
|
|
139
|
+
error={errors.newPassword?.message}
|
|
140
|
+
className="text-secondary"
|
|
141
|
+
/>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={() => setShowPassword((prev) => !prev)}
|
|
145
|
+
className="absolute right-3 top-[42px] text-slate-400 transition hover:text-slate-600"
|
|
146
|
+
>
|
|
147
|
+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div className="relative text-start text-secondary">
|
|
152
|
+
<h2 className="text-sm text-secondary mb-3">
|
|
153
|
+
Confirm Password <span className="text-primary-500">*</span>
|
|
154
|
+
</h2>
|
|
155
|
+
<Input
|
|
156
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
157
|
+
placeholder="Re-enter new password"
|
|
158
|
+
{...register('confirmPassword')}
|
|
159
|
+
error={errors.confirmPassword?.message}
|
|
160
|
+
className="text-secondary"
|
|
161
|
+
/>
|
|
162
|
+
<button
|
|
163
|
+
type="button"
|
|
164
|
+
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
|
165
|
+
className="absolute right-3 top-[42px] text-slate-400 transition hover:text-slate-600"
|
|
166
|
+
>
|
|
167
|
+
{showConfirmPassword ? (
|
|
168
|
+
<EyeOff className="h-5 w-5" />
|
|
169
|
+
) : (
|
|
170
|
+
<Eye className="h-5 w-5" />
|
|
171
|
+
)}
|
|
172
|
+
</button>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<button
|
|
176
|
+
type="submit"
|
|
177
|
+
disabled={isSubmitting || !token}
|
|
178
|
+
className="w-full bg-secondary hover:opacity-80 text-white font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-70 disabled:cursor-not-allowed"
|
|
179
|
+
>
|
|
180
|
+
{isSubmitting ? 'Resetting Password...' : 'Reset Password'}
|
|
181
|
+
</button>
|
|
182
|
+
|
|
183
|
+
<div className="flex items-center gap-2 rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
|
|
184
|
+
<ShieldCheck className="h-4 w-4 text-primary-600" />
|
|
185
|
+
Use a strong password that you haven't used elsewhere.
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div className="pt-4 border-t border-slate-200">
|
|
189
|
+
<Link
|
|
190
|
+
href={buildPath('/login')}
|
|
191
|
+
className="text-sm font-medium text-primary transition hover:opacity-80"
|
|
192
|
+
>
|
|
193
|
+
Back to login
|
|
194
|
+
</Link>
|
|
195
|
+
</div>
|
|
196
|
+
</form>
|
|
197
|
+
</div>
|
|
198
|
+
</motion.section>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|