hey-pharmacist-ecommerce 1.0.5 → 1.0.6
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 +107 -1
- 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 +17 -14
- 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
|
@@ -5,28 +5,45 @@ import { motion } from 'framer-motion';
|
|
|
5
5
|
import { useForm } from 'react-hook-form';
|
|
6
6
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
8
|
+
import Link from 'next/link';
|
|
9
|
+
import { useRouter } from 'next/navigation';
|
|
10
|
+
import {
|
|
11
|
+
CheckCircle2,
|
|
12
|
+
Eye,
|
|
13
|
+
EyeOff,
|
|
14
|
+
Heart,
|
|
15
|
+
HeartPulse,
|
|
16
|
+
Shield,
|
|
17
|
+
UserPlus,
|
|
18
|
+
} from 'lucide-react';
|
|
10
19
|
import { Input } from '@/components/ui/Input';
|
|
20
|
+
import { Button } from '@/components/ui/Button';
|
|
11
21
|
import { useAuth } from '@/providers/AuthProvider';
|
|
12
|
-
import { useRouter } from 'next/navigation';
|
|
13
22
|
import { toast } from 'sonner';
|
|
14
|
-
import
|
|
15
|
-
|
|
16
|
-
const registerSchema = z
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
import { CreateUserDtoCustomerTypeEnum, CreateUserDtoRoleEnum } from '@/lib/Apis/models';
|
|
24
|
+
|
|
25
|
+
const registerSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
firstName: z.string().min(2, 'First name is required'),
|
|
28
|
+
lastName: z.string().min(2, 'Last name is required'),
|
|
29
|
+
email: z.string().email('Enter a valid email'),
|
|
30
|
+
phone: z.string().optional(),
|
|
31
|
+
password: z.string().min(6, 'Password must be at least 6 characters'),
|
|
32
|
+
confirmPassword: z.string(),
|
|
33
|
+
})
|
|
34
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
35
|
+
message: "Passwords don't match",
|
|
36
|
+
path: ['confirmPassword'],
|
|
37
|
+
});
|
|
27
38
|
|
|
28
39
|
type RegisterFormData = z.infer<typeof registerSchema>;
|
|
29
40
|
|
|
41
|
+
const BENEFITS = [
|
|
42
|
+
'Personalized care plans from licensed pharmacists',
|
|
43
|
+
'Express delivery with cold-chain handling',
|
|
44
|
+
'Automatic refills and refill reminders',
|
|
45
|
+
];
|
|
46
|
+
|
|
30
47
|
export function RegisterScreen() {
|
|
31
48
|
const router = useRouter();
|
|
32
49
|
const { register: registerUser } = useAuth();
|
|
@@ -46,7 +63,12 @@ export function RegisterScreen() {
|
|
|
46
63
|
setIsSubmitting(true);
|
|
47
64
|
try {
|
|
48
65
|
const { confirmPassword, ...registerData } = data;
|
|
49
|
-
await registerUser(
|
|
66
|
+
await registerUser({
|
|
67
|
+
...registerData,
|
|
68
|
+
addresses: [],
|
|
69
|
+
customerType: CreateUserDtoCustomerTypeEnum.Regular,
|
|
70
|
+
role: CreateUserDtoRoleEnum.User,
|
|
71
|
+
});
|
|
50
72
|
toast.success('Account created successfully!');
|
|
51
73
|
router.push('/');
|
|
52
74
|
} catch (error: any) {
|
|
@@ -57,144 +79,171 @@ export function RegisterScreen() {
|
|
|
57
79
|
};
|
|
58
80
|
|
|
59
81
|
return (
|
|
60
|
-
<div className="min-h-screen bg-
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
<div className="
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
82
|
+
<div className="min-h-screen bg-slate-50">
|
|
83
|
+
<div className="grid min-h-screen overflow-hidden bg-white lg:grid-cols-[0.95fr_1.05fr]">
|
|
84
|
+
<motion.section
|
|
85
|
+
initial={{ opacity: 0, x: -24 }}
|
|
86
|
+
animate={{ opacity: 1, x: 0 }}
|
|
87
|
+
transition={{ duration: 0.4 }}
|
|
88
|
+
className="flex flex-col justify-between bg-gradient-to-br from-[rgb(var(--header-from))] via-[rgb(var(--header-via))] to-[rgb(var(--header-to))] px-10 py-14 text-white"
|
|
89
|
+
>
|
|
90
|
+
<div className="space-y-6">
|
|
91
|
+
<span className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-sm font-semibold uppercase tracking-[0.35em] text-white/70 backdrop-blur">
|
|
92
|
+
<HeartPulse className="h-4 w-4" />
|
|
93
|
+
Join Hey Pharmacist
|
|
94
|
+
</span>
|
|
95
|
+
<h1 className="text-4xl font-bold leading-tight lg:text-5xl">
|
|
96
|
+
Create your wellness account
|
|
97
|
+
</h1>
|
|
98
|
+
<p className="max-w-xl text-white/80">
|
|
99
|
+
Unlock concierge-level pharmacy support, curated product recommendations, and smarter
|
|
100
|
+
refills designed for busy families.
|
|
101
|
+
</p>
|
|
74
102
|
</div>
|
|
75
103
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
/>
|
|
85
|
-
<Input
|
|
86
|
-
label="Last Name"
|
|
87
|
-
placeholder="Doe"
|
|
88
|
-
{...register('lastName')}
|
|
89
|
-
error={errors.lastName?.message}
|
|
90
|
-
/>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
<Input
|
|
94
|
-
type="email"
|
|
95
|
-
label="Email Address"
|
|
96
|
-
placeholder="you@example.com"
|
|
97
|
-
{...register('email')}
|
|
98
|
-
error={errors.email?.message}
|
|
99
|
-
/>
|
|
104
|
+
<div className="space-y-4 rounded-3xl bg-white/10 p-6 backdrop-blur">
|
|
105
|
+
{BENEFITS.map((benefit) => (
|
|
106
|
+
<div key={benefit} className="flex items-center gap-3">
|
|
107
|
+
<CheckCircle2 className="h-5 w-5 text-white" />
|
|
108
|
+
<p className="text-sm text-white/85">{benefit}</p>
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
100
112
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
<div className="flex items-center gap-6 text-sm text-white/80">
|
|
114
|
+
<span>Already part of the community?</span>
|
|
115
|
+
<Link
|
|
116
|
+
href="/login"
|
|
117
|
+
className="inline-flex items-center gap-2 rounded-full bg-white/15 px-4 py-2 font-semibold transition hover:bg-white/25"
|
|
118
|
+
>
|
|
119
|
+
Sign in
|
|
120
|
+
</Link>
|
|
121
|
+
</div>
|
|
122
|
+
</motion.section>
|
|
123
|
+
|
|
124
|
+
<motion.section
|
|
125
|
+
initial={{ opacity: 0, x: 24 }}
|
|
126
|
+
animate={{ opacity: 1, x: 0 }}
|
|
127
|
+
transition={{ duration: 0.4 }}
|
|
128
|
+
className="flex items-center justify-center px-6 py-12 lg:px-16"
|
|
129
|
+
>
|
|
130
|
+
<div className="w-full max-w-lg space-y-10">
|
|
131
|
+
<div className="space-y-3">
|
|
132
|
+
<h2 className="text-3xl font-bold text-slate-900">Create an account</h2>
|
|
133
|
+
<p className="text-sm text-slate-500">
|
|
134
|
+
Start your personalized care journey with pharmacist-backed guidance.
|
|
135
|
+
</p>
|
|
136
|
+
</div>
|
|
108
137
|
|
|
109
|
-
<
|
|
138
|
+
<form
|
|
139
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
140
|
+
className="space-y-6 rounded-3xl border border-slate-100 bg-white p-8 shadow-lg shadow-primary-50"
|
|
141
|
+
>
|
|
142
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
143
|
+
<Input
|
|
144
|
+
label="First name"
|
|
145
|
+
placeholder="Alex"
|
|
146
|
+
{...register('firstName')}
|
|
147
|
+
error={errors.firstName?.message}
|
|
148
|
+
/>
|
|
149
|
+
<Input
|
|
150
|
+
label="Last name"
|
|
151
|
+
placeholder="Morgan"
|
|
152
|
+
{...register('lastName')}
|
|
153
|
+
error={errors.lastName?.message}
|
|
154
|
+
/>
|
|
155
|
+
</div>
|
|
110
156
|
<Input
|
|
111
|
-
type=
|
|
112
|
-
label="
|
|
113
|
-
placeholder="
|
|
114
|
-
{...register('
|
|
115
|
-
error={errors.
|
|
157
|
+
type="email"
|
|
158
|
+
label="Email address"
|
|
159
|
+
placeholder="you@example.com"
|
|
160
|
+
{...register('email')}
|
|
161
|
+
error={errors.email?.message}
|
|
116
162
|
/>
|
|
117
|
-
<button
|
|
118
|
-
type="button"
|
|
119
|
-
onClick={() => setShowPassword(!showPassword)}
|
|
120
|
-
className="absolute right-3 top-[42px] text-gray-500 hover:text-gray-700"
|
|
121
|
-
>
|
|
122
|
-
{showPassword ? (
|
|
123
|
-
<EyeOff className="w-5 h-5" />
|
|
124
|
-
) : (
|
|
125
|
-
<Eye className="w-5 h-5" />
|
|
126
|
-
)}
|
|
127
|
-
</button>
|
|
128
|
-
</div>
|
|
129
|
-
|
|
130
|
-
<div className="relative">
|
|
131
163
|
<Input
|
|
132
|
-
type=
|
|
133
|
-
label="
|
|
134
|
-
placeholder="
|
|
135
|
-
{...register('
|
|
136
|
-
error={errors.
|
|
164
|
+
type="tel"
|
|
165
|
+
label="Phone number (optional)"
|
|
166
|
+
placeholder="+1 (555) 123-4567"
|
|
167
|
+
{...register('phone')}
|
|
168
|
+
error={errors.phone?.message}
|
|
137
169
|
/>
|
|
138
|
-
<
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
170
|
+
<div className="relative">
|
|
171
|
+
<Input
|
|
172
|
+
type={showPassword ? 'text' : 'password'}
|
|
173
|
+
label="Password"
|
|
174
|
+
placeholder="••••••••"
|
|
175
|
+
{...register('password')}
|
|
176
|
+
error={errors.password?.message}
|
|
177
|
+
/>
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
onClick={() => setShowPassword((prev) => !prev)}
|
|
181
|
+
className="absolute right-3 top-[42px] text-slate-400 transition hover:text-slate-600"
|
|
182
|
+
>
|
|
183
|
+
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
184
|
+
</button>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="relative">
|
|
187
|
+
<Input
|
|
188
|
+
type={showConfirmPassword ? 'text' : 'password'}
|
|
189
|
+
label="Confirm password"
|
|
190
|
+
placeholder="••••••••"
|
|
191
|
+
{...register('confirmPassword')}
|
|
192
|
+
error={errors.confirmPassword?.message}
|
|
193
|
+
/>
|
|
194
|
+
<button
|
|
195
|
+
type="button"
|
|
196
|
+
onClick={() => setShowConfirmPassword((prev) => !prev)}
|
|
197
|
+
className="absolute right-3 top-[42px] text-slate-400 transition hover:text-slate-600"
|
|
198
|
+
>
|
|
199
|
+
{showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
|
|
200
|
+
</button>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="flex items-start gap-3 rounded-2xl bg-slate-50 p-4 text-sm text-slate-600">
|
|
204
|
+
<Shield className="mt-0.5 h-5 w-5 text-primary-500" />
|
|
205
|
+
<span>
|
|
206
|
+
By creating an account, you agree to our{' '}
|
|
207
|
+
<Link href="/terms" className="font-semibold text-primary-600 hover:text-primary-700">
|
|
208
|
+
Terms of Service
|
|
209
|
+
</Link>{' '}
|
|
210
|
+
and{' '}
|
|
211
|
+
<Link
|
|
212
|
+
href="/privacy"
|
|
213
|
+
className="font-semibold text-primary-600 hover:text-primary-700"
|
|
214
|
+
>
|
|
215
|
+
Privacy Policy
|
|
216
|
+
</Link>
|
|
217
|
+
. We protect your information with bank-level encryption.
|
|
218
|
+
</span>
|
|
219
|
+
</div>
|
|
220
|
+
|
|
221
|
+
<Button
|
|
222
|
+
type="submit"
|
|
223
|
+
size="lg"
|
|
224
|
+
isLoading={isSubmitting}
|
|
225
|
+
className="w-full"
|
|
142
226
|
>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<Link href="/terms" className="text-primary-600 hover:text-primary-700">
|
|
160
|
-
Terms of Service
|
|
161
|
-
</Link>{' '}
|
|
162
|
-
and{' '}
|
|
163
|
-
<Link href="/privacy" className="text-primary-600 hover:text-primary-700">
|
|
164
|
-
Privacy Policy
|
|
165
|
-
</Link>
|
|
166
|
-
</label>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
<Button
|
|
170
|
-
type="submit"
|
|
171
|
-
size="lg"
|
|
172
|
-
isLoading={isSubmitting}
|
|
173
|
-
className="w-full"
|
|
174
|
-
>
|
|
175
|
-
Create Account
|
|
176
|
-
</Button>
|
|
177
|
-
</form>
|
|
178
|
-
|
|
179
|
-
{/* Divider */}
|
|
180
|
-
<div className="relative my-8">
|
|
181
|
-
<div className="absolute inset-0 flex items-center">
|
|
182
|
-
<div className="w-full border-t border-gray-200"></div>
|
|
183
|
-
</div>
|
|
184
|
-
<div className="relative flex justify-center text-sm">
|
|
185
|
-
<span className="px-4 bg-white text-gray-500">Already have an account?</span>
|
|
227
|
+
<UserPlus className="h-5 w-5" />
|
|
228
|
+
Create my account
|
|
229
|
+
</Button>
|
|
230
|
+
</form>
|
|
231
|
+
|
|
232
|
+
<div className="rounded-3xl border border-slate-100 bg-slate-50 p-6 text-sm text-slate-600">
|
|
233
|
+
<div className="flex items-start gap-3">
|
|
234
|
+
<Heart className="mt-0.5 h-5 w-5 text-primary-500" />
|
|
235
|
+
<div>
|
|
236
|
+
<p className="font-semibold text-slate-800">Members love us</p>
|
|
237
|
+
<p>
|
|
238
|
+
92% of patients report improved adherence after receiving pharmacist touchpoints
|
|
239
|
+
through their Hey Pharmacist account.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
186
243
|
</div>
|
|
187
244
|
</div>
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
<Link href="/login">
|
|
191
|
-
<Button variant="outline" size="lg" className="w-full">
|
|
192
|
-
Sign In
|
|
193
|
-
</Button>
|
|
194
|
-
</Link>
|
|
195
|
-
</div>
|
|
196
|
-
</motion.div>
|
|
245
|
+
</motion.section>
|
|
246
|
+
</div>
|
|
197
247
|
</div>
|
|
198
248
|
);
|
|
199
249
|
}
|
|
200
|
-
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { useSearchParams } from 'next/navigation';
|
|
5
|
+
import { ProductsApi } from '@/lib/Apis';
|
|
6
|
+
import { ExtendedProductDTO } from '@/lib/Apis';
|
|
7
|
+
import { ProductCard } from '@/components/ProductCard';
|
|
8
|
+
import { Skeleton } from '@/components/ui/Skeleton';
|
|
9
|
+
import { Input } from '@/components/ui/Input';
|
|
10
|
+
import { Search, X } from 'lucide-react';
|
|
11
|
+
import Link from 'next/link';
|
|
12
|
+
import { AXIOS_CONFIG } from '@/lib/Apis/wrapper';
|
|
13
|
+
import { useWishlist } from '@/providers/WishlistProvider';
|
|
14
|
+
import { useRouter } from 'next/navigation';
|
|
15
|
+
|
|
16
|
+
export default function SearchPage() {
|
|
17
|
+
const router = useRouter();
|
|
18
|
+
const searchParams = useSearchParams();
|
|
19
|
+
const searchQuery = searchParams.get('q') || '';
|
|
20
|
+
const [products, setProducts] = useState<ExtendedProductDTO[]>([]);
|
|
21
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
22
|
+
const [searchInput, setSearchInput] = useState(searchQuery);
|
|
23
|
+
const [hasSearched, setHasSearched] = useState(false);
|
|
24
|
+
const { isInWishlist } = useWishlist();
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const fetchSearchResults = async () => {
|
|
28
|
+
if (!searchQuery.trim()) {
|
|
29
|
+
setProducts([]);
|
|
30
|
+
setIsLoading(false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
setIsLoading(true);
|
|
36
|
+
console.log(searchQuery);
|
|
37
|
+
const api = new ProductsApi(AXIOS_CONFIG);
|
|
38
|
+
const response = await api.getAllProductsForStore(
|
|
39
|
+
searchQuery,
|
|
40
|
+
undefined, // productType
|
|
41
|
+
undefined, // categoryId
|
|
42
|
+
undefined, // maxPrice
|
|
43
|
+
undefined, // minPrice
|
|
44
|
+
undefined, // brandFilter
|
|
45
|
+
'in-stock', // availability
|
|
46
|
+
'relevance', // sort
|
|
47
|
+
true, // includeNoVariantProducts
|
|
48
|
+
true, // isActive
|
|
49
|
+
20, // limit
|
|
50
|
+
1 // page
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (response.data?.data) {
|
|
54
|
+
const transformedProducts = response.data.data.map((item: ExtendedProductDTO) => ({
|
|
55
|
+
...item,
|
|
56
|
+
id: item._id || '',
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
setProducts(transformedProducts);
|
|
60
|
+
} else {
|
|
61
|
+
setProducts([]);
|
|
62
|
+
}
|
|
63
|
+
setHasSearched(true);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Error fetching search results:', error);
|
|
66
|
+
setProducts([]);
|
|
67
|
+
} finally {
|
|
68
|
+
setIsLoading(false);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
fetchSearchResults();
|
|
73
|
+
}, [searchQuery]);
|
|
74
|
+
|
|
75
|
+
const handleSearch = (e: React.FormEvent) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
if (searchInput.trim()) {
|
|
78
|
+
window.location.href = `/search?q=${encodeURIComponent(searchInput.trim())}`;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const clearSearch = () => {
|
|
83
|
+
setSearchInput('');
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="container mx-auto px-4 py-8">
|
|
88
|
+
<div className="max-w-2xl mx-auto mb-8">
|
|
89
|
+
<form onSubmit={handleSearch} className="relative">
|
|
90
|
+
<div className="relative">
|
|
91
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
|
|
92
|
+
<Input
|
|
93
|
+
type="text"
|
|
94
|
+
value={searchInput}
|
|
95
|
+
onChange={(e) => setSearchInput(e.target.value)}
|
|
96
|
+
placeholder="Search for products..."
|
|
97
|
+
className="pl-10 pr-10 py-6 text-base rounded-lg border-gray-300 focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
98
|
+
/>
|
|
99
|
+
{searchInput && (
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
onClick={clearSearch}
|
|
103
|
+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
|
104
|
+
>
|
|
105
|
+
<X className="h-5 w-5" />
|
|
106
|
+
</button>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
<button
|
|
110
|
+
type="submit"
|
|
111
|
+
className="mt-4 w-full bg-primary-600 hover:bg-primary-700 text-white font-medium py-2 px-4 rounded-lg transition-colors"
|
|
112
|
+
>
|
|
113
|
+
Search
|
|
114
|
+
</button>
|
|
115
|
+
</form>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div>
|
|
119
|
+
<h1 className="text-2xl font-bold text-gray-900 mb-6">
|
|
120
|
+
{searchQuery ? `Search results for "${searchQuery}"` : 'Search Products'}
|
|
121
|
+
</h1>
|
|
122
|
+
|
|
123
|
+
{isLoading ? (
|
|
124
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
125
|
+
{[...Array(8)].map((_, i) => (
|
|
126
|
+
<div key={i} className="space-y-3">
|
|
127
|
+
<Skeleton className="h-48 w-full rounded-lg" />
|
|
128
|
+
<Skeleton className="h-4 w-3/4" />
|
|
129
|
+
<Skeleton className="h-4 w-1/2" />
|
|
130
|
+
</div>
|
|
131
|
+
))}
|
|
132
|
+
</div>
|
|
133
|
+
) : products.length > 0 ? (
|
|
134
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
135
|
+
{products.map((product) => (
|
|
136
|
+
<ProductCard
|
|
137
|
+
product={product}
|
|
138
|
+
isFavorited={isInWishlist(product.id)}
|
|
139
|
+
onClickProduct={(p) => router.push(`/products/${p.id}`)}
|
|
140
|
+
/>
|
|
141
|
+
))}
|
|
142
|
+
</div>
|
|
143
|
+
) : hasSearched ? (
|
|
144
|
+
<div className="text-center py-12">
|
|
145
|
+
<div className="text-gray-500 text-lg mb-4">
|
|
146
|
+
No products found for "{searchQuery}"
|
|
147
|
+
</div>
|
|
148
|
+
<p className="text-gray-500 mb-6">
|
|
149
|
+
Try different keywords or check out our{" "}
|
|
150
|
+
<Link href="/shop" className="text-primary-600 hover:underline ml-1 font-medium">
|
|
151
|
+
featured products
|
|
152
|
+
</Link>
|
|
153
|
+
</p>
|
|
154
|
+
</div>
|
|
155
|
+
) : (
|
|
156
|
+
<div className="text-center py-12">
|
|
157
|
+
<p className="text-gray-500">
|
|
158
|
+
Enter a search term to find products
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|