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.
Files changed (60) hide show
  1. package/dist/index.d.mts +344 -640
  2. package/dist/index.d.ts +344 -640
  3. package/dist/index.js +1807 -838
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +1807 -840
  6. package/dist/index.mjs.map +1 -1
  7. package/package.json +1 -1
  8. package/src/components/AccountOrdersTab.tsx +1 -1
  9. package/src/components/AccountSettingsTab.tsx +88 -6
  10. package/src/components/CartItem.tsx +1 -1
  11. package/src/components/Header.tsx +8 -2
  12. package/src/components/OrderCard.tsx +4 -4
  13. package/src/components/ProductCard.tsx +59 -42
  14. package/src/components/QuickViewModal.tsx +13 -13
  15. package/src/hooks/useAddresses.ts +4 -1
  16. package/src/hooks/usePaymentMethods.ts +26 -31
  17. package/src/hooks/useProducts.ts +63 -64
  18. package/src/hooks/useWishlistProducts.ts +4 -5
  19. package/src/index.ts +2 -0
  20. package/src/lib/Apis/api.ts +0 -1
  21. package/src/lib/Apis/apis/auth-api.ts +18 -29
  22. package/src/lib/Apis/apis/products-api.ts +845 -405
  23. package/src/lib/Apis/models/category-populated.ts +0 -12
  24. package/src/lib/Apis/models/category-sub-category-populated.ts +2 -2
  25. package/src/lib/Apis/models/category.ts +0 -18
  26. package/src/lib/Apis/models/{table-cell-dto.ts → change-password-dto.ts} +6 -6
  27. package/src/lib/Apis/models/create-product-dto.ts +30 -23
  28. package/src/lib/Apis/models/create-sub-category-dto.ts +6 -0
  29. package/src/lib/Apis/models/create-variant-dto.ts +29 -29
  30. package/src/lib/Apis/models/index.ts +5 -7
  31. package/src/lib/Apis/models/paginated-products-dto.ts +6 -6
  32. package/src/lib/Apis/models/product-summary.ts +69 -0
  33. package/src/lib/Apis/models/product-variant.ts +34 -65
  34. package/src/lib/Apis/models/product.ts +138 -0
  35. package/src/lib/Apis/models/products-insights-dto.ts +12 -0
  36. package/src/lib/Apis/models/single-product-media.ts +0 -12
  37. package/src/lib/Apis/models/sub-category.ts +6 -12
  38. package/src/lib/Apis/models/update-product-dto.ts +30 -19
  39. package/src/lib/Apis/models/update-sub-category-dto.ts +6 -0
  40. package/src/lib/Apis/models/{update-product-variant-dto.ts → update-variant-dto.ts} +51 -45
  41. package/src/lib/Apis/models/{shallow-parent-category-dto.ts → variant-id-inventory-body.ts} +5 -11
  42. package/src/lib/api-adapter/config.ts +53 -0
  43. package/src/lib/validations/address.ts +1 -1
  44. package/src/providers/FavoritesProvider.tsx +5 -5
  45. package/src/providers/WishlistProvider.tsx +4 -4
  46. package/src/screens/CartScreen.tsx +1 -1
  47. package/src/screens/ChangePasswordScreen.tsx +2 -6
  48. package/src/screens/CheckoutScreen.tsx +40 -11
  49. package/src/screens/ForgotPasswordScreen.tsx +153 -0
  50. package/src/screens/ProductDetailScreen.tsx +51 -60
  51. package/src/screens/RegisterScreen.tsx +31 -31
  52. package/src/screens/ResetPasswordScreen.tsx +202 -0
  53. package/src/screens/SearchResultsScreen.tsx +264 -26
  54. package/src/screens/ShopScreen.tsx +42 -45
  55. package/src/screens/WishlistScreen.tsx +35 -31
  56. package/src/lib/Apis/apis/product-variants-api.ts +0 -552
  57. package/src/lib/Apis/models/create-single-variant-product-dto.ts +0 -154
  58. package/src/lib/Apis/models/extended-product-dto.ts +0 -206
  59. package/src/lib/Apis/models/frequently-bought-product-dto.ts +0 -71
  60. 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.productVariants?.length && selectedVariant) {
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.id,
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?.productMedia?.length) {
103
- return selectedVariant.productMedia.map((media: any) => ({
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?.productMedia?.length) {
115
- return product.productMedia.map((media: any) => ({
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?.images?.length) {
126
- return product.images.map((image: string) => ({
127
- src: image,
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: image,
132
- file: image,
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?.id) return;
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.id, {
198
- params: {
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
- return () => {
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.id,
227
+ product._id,
236
228
  quantity,
237
- selectedVariant.id
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.id));
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.id);
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?.id || 'default'}
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.parentCategories?.[0]?.name || 'Uncategorized'}
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
- <div className="flex items-center gap-3 mb-6 pb-6 border-b-2 border-gray-100">
474
- <span className="font-['Poppins',sans-serif] font-bold text-[40px] text-[#E67E50]">
475
- ${variantPrice.toFixed(2)}
476
- </span>
477
- {variantComparePrice && variantComparePrice > variantPrice && (
478
- <>
479
- <span className="font-['Poppins',sans-serif] text-[24px] text-muted line-through">
480
- ${variantComparePrice.toFixed(2)}
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
- </div>
487
- </>
488
- )}
489
- </div>
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?.productVariants && product.productVariants.length > 0 && (
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.productVariants.map((variant: ProductVariant) => {
542
- const isSelected = selectedVariant?.id === variant.id;
543
- const variantImage = variant.productMedia?.[0]?.file
544
- || product.productMedia?.[0]?.file
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.id}
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
- Loading...
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.id}
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
- firstName: z.string().min(2, 'First name is required'),
29
- lastName: z.string().min(2, 'Last name is required'),
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
- boxShadow: '0px 4px 6px -4px #0000001A, 0px 10px 15px -3px #0000001A',
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
- <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
- />
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
- <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
- />
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
- <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
- />
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
- <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
- />
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
+ }