akarshanxyz 1.0.0 → 1.0.1
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/a10/Pagination+FilteringwithReduxCache +422 -0
- package/a7/Login&PaymentForm(Using Yup) +132 -0
- package/a7/SellerRegistrationForm(zodschemavalidation) +199 -0
- package/a7/UserRegistrationForm(zodschemavalidation) +32 -0
- package/a8/MiniE-commerceApp(using redux) +49 -0
- package/a9/PerformanceOptimization +141 -0
- package/package.json +1 -1
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import mongoose from "mongoose";
|
|
3
|
+
import cors from "cors";
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useState } from "react";
|
|
6
|
+
import {
|
|
7
|
+
configureStore,
|
|
8
|
+
createAsyncThunk,
|
|
9
|
+
createSlice,
|
|
10
|
+
} from "@reduxjs/toolkit";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
Provider,
|
|
14
|
+
useDispatch,
|
|
15
|
+
useSelector,
|
|
16
|
+
} from "react-redux";
|
|
17
|
+
|
|
18
|
+
/* ================= BACKEND ================= */
|
|
19
|
+
|
|
20
|
+
const app = express();
|
|
21
|
+
|
|
22
|
+
app.use(cors());
|
|
23
|
+
app.use(express.json());
|
|
24
|
+
|
|
25
|
+
mongoose.connect("mongodb://127.0.0.1:27017/ecommerce");
|
|
26
|
+
|
|
27
|
+
const productSchema = new mongoose.Schema({
|
|
28
|
+
title: String,
|
|
29
|
+
price: Number,
|
|
30
|
+
category: String,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const Product = mongoose.model(
|
|
34
|
+
"Product",
|
|
35
|
+
productSchema
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const seedProducts = async () => {
|
|
39
|
+
const count = await Product.countDocuments();
|
|
40
|
+
|
|
41
|
+
if (count === 0) {
|
|
42
|
+
await Product.insertMany([
|
|
43
|
+
{
|
|
44
|
+
title: "Laptop",
|
|
45
|
+
price: 50000,
|
|
46
|
+
category: "electronics",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: "Phone",
|
|
50
|
+
price: 20000,
|
|
51
|
+
category: "electronics",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: "TV",
|
|
55
|
+
price: 40000,
|
|
56
|
+
category: "electronics",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
title: "Speaker",
|
|
60
|
+
price: 5000,
|
|
61
|
+
category: "electronics",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: "Camera",
|
|
65
|
+
price: 35000,
|
|
66
|
+
category: "electronics",
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
title: "Shirt",
|
|
71
|
+
price: 1500,
|
|
72
|
+
category: "fashion",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
title: "Jeans",
|
|
76
|
+
price: 2500,
|
|
77
|
+
category: "fashion",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
title: "Shoes",
|
|
81
|
+
price: 3000,
|
|
82
|
+
category: "fashion",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
title: "Jacket",
|
|
86
|
+
price: 4500,
|
|
87
|
+
category: "fashion",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: "Watch",
|
|
91
|
+
price: 5000,
|
|
92
|
+
category: "fashion",
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
{
|
|
96
|
+
title: "Rice",
|
|
97
|
+
price: 1200,
|
|
98
|
+
category: "grocery",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
title: "Milk",
|
|
102
|
+
price: 60,
|
|
103
|
+
category: "grocery",
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
title: "Bread",
|
|
107
|
+
price: 40,
|
|
108
|
+
category: "grocery",
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
title: "Eggs",
|
|
112
|
+
price: 120,
|
|
113
|
+
category: "grocery",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
title: "Sugar",
|
|
117
|
+
price: 50,
|
|
118
|
+
category: "grocery",
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
{
|
|
122
|
+
title: "Keyboard",
|
|
123
|
+
price: 1800,
|
|
124
|
+
category: "electronics",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
title: "Mouse",
|
|
128
|
+
price: 700,
|
|
129
|
+
category: "electronics",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
title: "Sneakers",
|
|
133
|
+
price: 3500,
|
|
134
|
+
category: "fashion",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
title: "Oil",
|
|
138
|
+
price: 180,
|
|
139
|
+
category: "grocery",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
title: "T-shirt",
|
|
143
|
+
price: 800,
|
|
144
|
+
category: "fashion",
|
|
145
|
+
},
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
seedProducts();
|
|
151
|
+
|
|
152
|
+
app.get("/products", async (req, res) => {
|
|
153
|
+
const page = Number(req.query.page) || 1;
|
|
154
|
+
const limit = Number(req.query.limit) || 5;
|
|
155
|
+
|
|
156
|
+
const totalProducts =
|
|
157
|
+
await Product.countDocuments();
|
|
158
|
+
|
|
159
|
+
const totalPages = Math.ceil(
|
|
160
|
+
totalProducts / limit
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const products = await Product.find()
|
|
164
|
+
.skip((page - 1) * limit)
|
|
165
|
+
.limit(limit);
|
|
166
|
+
|
|
167
|
+
res.json({
|
|
168
|
+
products,
|
|
169
|
+
currentPage: page,
|
|
170
|
+
totalPages,
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
app.get(
|
|
175
|
+
"/products/category/:category",
|
|
176
|
+
async (req, res) => {
|
|
177
|
+
const products = await Product.find({
|
|
178
|
+
category: req.params.category,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
res.json({ products });
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
app.listen(5000, () => {
|
|
186
|
+
console.log("Server running on port 5000");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/* ================= FRONTEND ================= */
|
|
190
|
+
|
|
191
|
+
type ProductType = {
|
|
192
|
+
_id: string;
|
|
193
|
+
title: string;
|
|
194
|
+
price: number;
|
|
195
|
+
category: string;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
type ProductState = {
|
|
199
|
+
pages: {
|
|
200
|
+
[key: number]: ProductType[];
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
currentPage: number;
|
|
204
|
+
totalPages: number;
|
|
205
|
+
filteredProducts: ProductType[];
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const fetchProducts = createAsyncThunk(
|
|
209
|
+
"products/fetchProducts",
|
|
210
|
+
async (page: number) => {
|
|
211
|
+
const response = await fetch(
|
|
212
|
+
`http://localhost:5000/products?page=${page}&limit=5`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return response.json();
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const fetchCategoryProducts =
|
|
220
|
+
createAsyncThunk(
|
|
221
|
+
"products/fetchCategoryProducts",
|
|
222
|
+
async (category: string) => {
|
|
223
|
+
const response = await fetch(
|
|
224
|
+
`http://localhost:5000/products/category/${category}`
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return response.json();
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const initialState: ProductState = {
|
|
232
|
+
pages: {},
|
|
233
|
+
currentPage: 1,
|
|
234
|
+
totalPages: 1,
|
|
235
|
+
filteredProducts: [],
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const productSlice = createSlice({
|
|
239
|
+
name: "products",
|
|
240
|
+
|
|
241
|
+
initialState,
|
|
242
|
+
|
|
243
|
+
reducers: {
|
|
244
|
+
setCurrentPage: (state, action) => {
|
|
245
|
+
state.currentPage = action.payload;
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
extraReducers: (builder) => {
|
|
250
|
+
builder.addCase(
|
|
251
|
+
fetchProducts.fulfilled,
|
|
252
|
+
(state, action) => {
|
|
253
|
+
state.pages[action.payload.currentPage] =
|
|
254
|
+
action.payload.products;
|
|
255
|
+
|
|
256
|
+
state.currentPage =
|
|
257
|
+
action.payload.currentPage;
|
|
258
|
+
|
|
259
|
+
state.totalPages =
|
|
260
|
+
action.payload.totalPages;
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
builder.addCase(
|
|
265
|
+
fetchCategoryProducts.fulfilled,
|
|
266
|
+
(state, action) => {
|
|
267
|
+
state.filteredProducts =
|
|
268
|
+
action.payload.products;
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
},
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const { setCurrentPage } =
|
|
275
|
+
productSlice.actions;
|
|
276
|
+
|
|
277
|
+
const store = configureStore({
|
|
278
|
+
reducer: {
|
|
279
|
+
products: productSlice.reducer,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
type RootState = ReturnType<
|
|
284
|
+
typeof store.getState
|
|
285
|
+
>;
|
|
286
|
+
|
|
287
|
+
type AppDispatch = typeof store.dispatch;
|
|
288
|
+
|
|
289
|
+
const ProductList = () => {
|
|
290
|
+
const dispatch = useDispatch<AppDispatch>();
|
|
291
|
+
|
|
292
|
+
const {
|
|
293
|
+
pages,
|
|
294
|
+
currentPage,
|
|
295
|
+
totalPages,
|
|
296
|
+
filteredProducts,
|
|
297
|
+
} = useSelector(
|
|
298
|
+
(state: RootState) => state.products
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
const [category, setCategory] =
|
|
302
|
+
useState("");
|
|
303
|
+
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!pages[currentPage]) {
|
|
306
|
+
dispatch(fetchProducts(currentPage));
|
|
307
|
+
}
|
|
308
|
+
}, [currentPage]);
|
|
309
|
+
|
|
310
|
+
const handleNext = () => {
|
|
311
|
+
if (currentPage < totalPages) {
|
|
312
|
+
dispatch(
|
|
313
|
+
setCurrentPage(currentPage + 1)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const handlePrevious = () => {
|
|
319
|
+
if (currentPage > 1) {
|
|
320
|
+
dispatch(
|
|
321
|
+
setCurrentPage(currentPage - 1)
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const handleCategory = (
|
|
327
|
+
e: React.ChangeEvent<HTMLSelectElement>
|
|
328
|
+
) => {
|
|
329
|
+
const value = e.target.value;
|
|
330
|
+
|
|
331
|
+
setCategory(value);
|
|
332
|
+
|
|
333
|
+
if (value) {
|
|
334
|
+
dispatch(fetchCategoryProducts(value));
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const products = category
|
|
339
|
+
? filteredProducts
|
|
340
|
+
: pages[currentPage] || [];
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
<div style={{ padding: "20px" }}>
|
|
344
|
+
<h1>Product Listing</h1>
|
|
345
|
+
|
|
346
|
+
<select
|
|
347
|
+
value={category}
|
|
348
|
+
onChange={handleCategory}
|
|
349
|
+
>
|
|
350
|
+
<option value="">All</option>
|
|
351
|
+
|
|
352
|
+
<option value="electronics">
|
|
353
|
+
Electronics
|
|
354
|
+
</option>
|
|
355
|
+
|
|
356
|
+
<option value="fashion">
|
|
357
|
+
Fashion
|
|
358
|
+
</option>
|
|
359
|
+
|
|
360
|
+
<option value="grocery">
|
|
361
|
+
Grocery
|
|
362
|
+
</option>
|
|
363
|
+
</select>
|
|
364
|
+
|
|
365
|
+
<div>
|
|
366
|
+
{products.map((product) => (
|
|
367
|
+
<div
|
|
368
|
+
key={product._id}
|
|
369
|
+
style={{
|
|
370
|
+
border: "1px solid gray",
|
|
371
|
+
margin: "10px 0",
|
|
372
|
+
padding: "10px",
|
|
373
|
+
}}
|
|
374
|
+
>
|
|
375
|
+
<h3>{product.title}</h3>
|
|
376
|
+
|
|
377
|
+
<p>₹{product.price}</p>
|
|
378
|
+
|
|
379
|
+
<p>{product.category}</p>
|
|
380
|
+
</div>
|
|
381
|
+
))}
|
|
382
|
+
</div>
|
|
383
|
+
|
|
384
|
+
{!category && (
|
|
385
|
+
<div>
|
|
386
|
+
<button
|
|
387
|
+
onClick={handlePrevious}
|
|
388
|
+
disabled={currentPage === 1}
|
|
389
|
+
>
|
|
390
|
+
Previous
|
|
391
|
+
</button>
|
|
392
|
+
|
|
393
|
+
<span
|
|
394
|
+
style={{ margin: "0 10px" }}
|
|
395
|
+
>
|
|
396
|
+
Page {currentPage} of{" "}
|
|
397
|
+
{totalPages}
|
|
398
|
+
</span>
|
|
399
|
+
|
|
400
|
+
<button
|
|
401
|
+
onClick={handleNext}
|
|
402
|
+
disabled={
|
|
403
|
+
currentPage === totalPages
|
|
404
|
+
}
|
|
405
|
+
>
|
|
406
|
+
Next
|
|
407
|
+
</button>
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const App = () => {
|
|
415
|
+
return (
|
|
416
|
+
<Provider store={store}>
|
|
417
|
+
<ProductList />
|
|
418
|
+
</Provider>
|
|
419
|
+
);
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
export default App;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
4
|
+
import * as yup from "yup";
|
|
5
|
+
|
|
6
|
+
type FormData = {
|
|
7
|
+
email: string;
|
|
8
|
+
phoneNumber: string;
|
|
9
|
+
paymentMethod: "Card" | "UPI";
|
|
10
|
+
cardNumber?: string;
|
|
11
|
+
cvv?: string;
|
|
12
|
+
upiId?: string;
|
|
13
|
+
terms: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const schema = yup.object({
|
|
17
|
+
email: yup
|
|
18
|
+
.string()
|
|
19
|
+
.email("Invalid email")
|
|
20
|
+
.required("Email is required"),
|
|
21
|
+
|
|
22
|
+
phoneNumber: yup
|
|
23
|
+
.string()
|
|
24
|
+
.matches(/^[0-9]{10}$/, "Phone number must be exactly 10 digits")
|
|
25
|
+
.required("Phone number is required"),
|
|
26
|
+
|
|
27
|
+
paymentMethod: yup
|
|
28
|
+
.string()
|
|
29
|
+
.oneOf(["Card", "UPI"])
|
|
30
|
+
.required("Payment method is required"),
|
|
31
|
+
|
|
32
|
+
cardNumber: yup.string().when("paymentMethod", {
|
|
33
|
+
is: "Card",
|
|
34
|
+
then: (schema) =>
|
|
35
|
+
schema
|
|
36
|
+
.required("Card number is required")
|
|
37
|
+
.matches(/^[0-9]{16}$/, "Card number must be 16 digits"),
|
|
38
|
+
otherwise: (schema) => schema.notRequired(),
|
|
39
|
+
}),
|
|
40
|
+
|
|
41
|
+
cvv: yup.string().when("paymentMethod", {
|
|
42
|
+
is: "Card",
|
|
43
|
+
then: (schema) =>
|
|
44
|
+
schema
|
|
45
|
+
.required("CVV is required")
|
|
46
|
+
.matches(/^[0-9]{3}$/, "CVV must be 3 digits"),
|
|
47
|
+
otherwise: (schema) => schema.notRequired(),
|
|
48
|
+
}),
|
|
49
|
+
|
|
50
|
+
upiId: yup.string().when("paymentMethod", {
|
|
51
|
+
is: "UPI",
|
|
52
|
+
then: (schema) =>
|
|
53
|
+
schema
|
|
54
|
+
.required("UPI ID is required")
|
|
55
|
+
.matches(
|
|
56
|
+
/^[a-zA-Z0-9._-]+@[a-zA-Z]+$/,
|
|
57
|
+
"Invalid UPI ID format"
|
|
58
|
+
),
|
|
59
|
+
otherwise: (schema) => schema.notRequired(),
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
terms: yup
|
|
63
|
+
.boolean()
|
|
64
|
+
.oneOf([true], "You must accept terms and conditions"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export default function CheckoutForm() {
|
|
68
|
+
const {
|
|
69
|
+
register,
|
|
70
|
+
handleSubmit,
|
|
71
|
+
watch,
|
|
72
|
+
formState: { errors },
|
|
73
|
+
} = useForm<FormData>({
|
|
74
|
+
resolver: yupResolver(schema),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const paymentMethod = watch("paymentMethod");
|
|
78
|
+
|
|
79
|
+
const onSubmit = (data: FormData) => {
|
|
80
|
+
console.log(data);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
85
|
+
<input type="email" placeholder="Email" {...register("email")} />
|
|
86
|
+
<p>{errors.email?.message}</p>
|
|
87
|
+
|
|
88
|
+
<input
|
|
89
|
+
type="text"
|
|
90
|
+
placeholder="Phone Number"
|
|
91
|
+
{...register("phoneNumber")}
|
|
92
|
+
/>
|
|
93
|
+
<p>{errors.phoneNumber?.message}</p>
|
|
94
|
+
|
|
95
|
+
<select {...register("paymentMethod")}>
|
|
96
|
+
<option value="">Select Payment Method</option>
|
|
97
|
+
<option value="Card">Card</option>
|
|
98
|
+
<option value="UPI">UPI</option>
|
|
99
|
+
</select>
|
|
100
|
+
<p>{errors.paymentMethod?.message}</p>
|
|
101
|
+
|
|
102
|
+
{paymentMethod === "Card" && (
|
|
103
|
+
<>
|
|
104
|
+
<input
|
|
105
|
+
type="text"
|
|
106
|
+
placeholder="Card Number"
|
|
107
|
+
{...register("cardNumber")}
|
|
108
|
+
/>
|
|
109
|
+
<p>{errors.cardNumber?.message}</p>
|
|
110
|
+
|
|
111
|
+
<input type="text" placeholder="CVV" {...register("cvv")} />
|
|
112
|
+
<p>{errors.cvv?.message}</p>
|
|
113
|
+
</>
|
|
114
|
+
)}
|
|
115
|
+
|
|
116
|
+
{paymentMethod === "UPI" && (
|
|
117
|
+
<>
|
|
118
|
+
<input type="text" placeholder="UPI ID" {...register("upiId")} />
|
|
119
|
+
<p>{errors.upiId?.message}</p>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
<label>
|
|
124
|
+
<input type="checkbox" {...register("terms")} />
|
|
125
|
+
Accept Terms & Conditions
|
|
126
|
+
</label>
|
|
127
|
+
<p>{errors.terms?.message}</p>
|
|
128
|
+
|
|
129
|
+
<button type="submit">Pay Now</button>
|
|
130
|
+
</form>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
5
|
+
|
|
6
|
+
const sellerSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
firstName: z.string().min(2, "First name must be at least 2 characters"),
|
|
9
|
+
lastName: z.string().optional(),
|
|
10
|
+
email: z.string().email("Invalid email"),
|
|
11
|
+
phoneNumber: z
|
|
12
|
+
.string()
|
|
13
|
+
.regex(/^[0-9]{10}$/, "Phone number must be exactly 10 digits"),
|
|
14
|
+
age: z.number().min(18, "Age must be 18 or above"),
|
|
15
|
+
|
|
16
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
17
|
+
confirmPassword: z.string(),
|
|
18
|
+
|
|
19
|
+
accountType: z.enum(["Individual", "Business"]),
|
|
20
|
+
|
|
21
|
+
shopName: z.string().min(1, "Shop name is required"),
|
|
22
|
+
gstNumber: z
|
|
23
|
+
.string()
|
|
24
|
+
.regex(/^[A-Z0-9]{5,15}$/, "Invalid GST number"),
|
|
25
|
+
businessAddress: z
|
|
26
|
+
.string()
|
|
27
|
+
.min(1, "Business address is required"),
|
|
28
|
+
|
|
29
|
+
address: z.object({
|
|
30
|
+
street: z.string().min(1, "Street is required"),
|
|
31
|
+
city: z.string().min(1, "City is required"),
|
|
32
|
+
state: z.string().min(1, "State is required"),
|
|
33
|
+
pincode: z
|
|
34
|
+
.string()
|
|
35
|
+
.regex(/^[0-9]{6}$/, "Pincode must be exactly 6 digits"),
|
|
36
|
+
}),
|
|
37
|
+
|
|
38
|
+
categories: z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.min(1, "Select at least one category"),
|
|
41
|
+
|
|
42
|
+
profileImage: z.any().optional(),
|
|
43
|
+
|
|
44
|
+
idProof: z.any().refine((file) => file?.length > 0, {
|
|
45
|
+
message: "ID Proof is required",
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
bankDetails: z.object({
|
|
49
|
+
accountNumber: z.string().min(1, "Account number is required"),
|
|
50
|
+
ifscCode: z
|
|
51
|
+
.string()
|
|
52
|
+
.regex(/^[A-Z]{4}0[A-Z0-9]{6}$/, "Invalid IFSC code"),
|
|
53
|
+
accountHolderName: z
|
|
54
|
+
.string()
|
|
55
|
+
.min(1, "Account holder name is required"),
|
|
56
|
+
}),
|
|
57
|
+
|
|
58
|
+
terms: z.literal(true, {
|
|
59
|
+
errorMap: () => ({
|
|
60
|
+
message: "You must accept terms and conditions",
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
65
|
+
path: ["confirmPassword"],
|
|
66
|
+
message: "Passwords do not match",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
type SellerFormData = z.infer<typeof sellerSchema>;
|
|
70
|
+
|
|
71
|
+
export default function SellerRegistrationForm() {
|
|
72
|
+
const {
|
|
73
|
+
register,
|
|
74
|
+
handleSubmit,
|
|
75
|
+
formState: { errors },
|
|
76
|
+
} = useForm<SellerFormData>({
|
|
77
|
+
resolver: zodResolver(sellerSchema),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const onSubmit = (data: SellerFormData) => {
|
|
81
|
+
console.log(data);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
86
|
+
<input placeholder="First Name" {...register("firstName")} />
|
|
87
|
+
<p>{errors.firstName?.message}</p>
|
|
88
|
+
|
|
89
|
+
<input placeholder="Last Name" {...register("lastName")} />
|
|
90
|
+
<p>{errors.lastName?.message}</p>
|
|
91
|
+
|
|
92
|
+
<input placeholder="Email" {...register("email")} />
|
|
93
|
+
<p>{errors.email?.message}</p>
|
|
94
|
+
|
|
95
|
+
<input placeholder="Phone Number" {...register("phoneNumber")} />
|
|
96
|
+
<p>{errors.phoneNumber?.message}</p>
|
|
97
|
+
|
|
98
|
+
<input
|
|
99
|
+
type="number"
|
|
100
|
+
placeholder="Age"
|
|
101
|
+
{...register("age", { valueAsNumber: true })}
|
|
102
|
+
/>
|
|
103
|
+
<p>{errors.age?.message}</p>
|
|
104
|
+
|
|
105
|
+
<input
|
|
106
|
+
type="password"
|
|
107
|
+
placeholder="Password"
|
|
108
|
+
{...register("password")}
|
|
109
|
+
/>
|
|
110
|
+
<p>{errors.password?.message}</p>
|
|
111
|
+
|
|
112
|
+
<input
|
|
113
|
+
type="password"
|
|
114
|
+
placeholder="Confirm Password"
|
|
115
|
+
{...register("confirmPassword")}
|
|
116
|
+
/>
|
|
117
|
+
<p>{errors.confirmPassword?.message}</p>
|
|
118
|
+
|
|
119
|
+
<select {...register("accountType")}>
|
|
120
|
+
<option value="">Select Account Type</option>
|
|
121
|
+
<option value="Individual">Individual</option>
|
|
122
|
+
<option value="Business">Business</option>
|
|
123
|
+
</select>
|
|
124
|
+
<p>{errors.accountType?.message}</p>
|
|
125
|
+
|
|
126
|
+
<input placeholder="Shop Name" {...register("shopName")} />
|
|
127
|
+
<p>{errors.shopName?.message}</p>
|
|
128
|
+
|
|
129
|
+
<input placeholder="GST Number" {...register("gstNumber")} />
|
|
130
|
+
<p>{errors.gstNumber?.message}</p>
|
|
131
|
+
|
|
132
|
+
<input
|
|
133
|
+
placeholder="Business Address"
|
|
134
|
+
{...register("businessAddress")}
|
|
135
|
+
/>
|
|
136
|
+
<p>{errors.businessAddress?.message}</p>
|
|
137
|
+
|
|
138
|
+
<input placeholder="Street" {...register("address.street")} />
|
|
139
|
+
<p>{errors.address?.street?.message}</p>
|
|
140
|
+
|
|
141
|
+
<input placeholder="City" {...register("address.city")} />
|
|
142
|
+
<p>{errors.address?.city?.message}</p>
|
|
143
|
+
|
|
144
|
+
<input placeholder="State" {...register("address.state")} />
|
|
145
|
+
<p>{errors.address?.state?.message}</p>
|
|
146
|
+
|
|
147
|
+
<input placeholder="Pincode" {...register("address.pincode")} />
|
|
148
|
+
<p>{errors.address?.pincode?.message}</p>
|
|
149
|
+
|
|
150
|
+
<label>
|
|
151
|
+
<input type="checkbox" value="Electronics" {...register("categories")} />
|
|
152
|
+
Electronics
|
|
153
|
+
</label>
|
|
154
|
+
|
|
155
|
+
<label>
|
|
156
|
+
<input type="checkbox" value="Fashion" {...register("categories")} />
|
|
157
|
+
Fashion
|
|
158
|
+
</label>
|
|
159
|
+
|
|
160
|
+
<label>
|
|
161
|
+
<input type="checkbox" value="Books" {...register("categories")} />
|
|
162
|
+
Books
|
|
163
|
+
</label>
|
|
164
|
+
|
|
165
|
+
<p>{errors.categories?.message}</p>
|
|
166
|
+
|
|
167
|
+
<input type="file" {...register("profileImage")} />
|
|
168
|
+
|
|
169
|
+
<input type="file" {...register("idProof")} />
|
|
170
|
+
<p>{errors.idProof?.message as string}</p>
|
|
171
|
+
|
|
172
|
+
<input
|
|
173
|
+
placeholder="Account Number"
|
|
174
|
+
{...register("bankDetails.accountNumber")}
|
|
175
|
+
/>
|
|
176
|
+
<p>{errors.bankDetails?.accountNumber?.message}</p>
|
|
177
|
+
|
|
178
|
+
<input
|
|
179
|
+
placeholder="IFSC Code"
|
|
180
|
+
{...register("bankDetails.ifscCode")}
|
|
181
|
+
/>
|
|
182
|
+
<p>{errors.bankDetails?.ifscCode?.message}</p>
|
|
183
|
+
|
|
184
|
+
<input
|
|
185
|
+
placeholder="Account Holder Name"
|
|
186
|
+
{...register("bankDetails.accountHolderName")}
|
|
187
|
+
/>
|
|
188
|
+
<p>{errors.bankDetails?.accountHolderName?.message}</p>
|
|
189
|
+
|
|
190
|
+
<label>
|
|
191
|
+
<input type="checkbox" {...register("terms")} />
|
|
192
|
+
Accept Terms & Conditions
|
|
193
|
+
</label>
|
|
194
|
+
<p>{errors.terms?.message}</p>
|
|
195
|
+
|
|
196
|
+
<button type="submit">Register Seller</button>
|
|
197
|
+
</form>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const registerSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
fullName: z.string().min(2, "Full name must be at least 2 characters"),
|
|
6
|
+
email: z.string().email("Invalid email"),
|
|
7
|
+
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
8
|
+
confirmPassword: z.string(),
|
|
9
|
+
age: z.number().min(19, "Age must be above 18"),
|
|
10
|
+
})
|
|
11
|
+
.refine((data) => data.password === data.confirmPassword, {
|
|
12
|
+
message: "Passwords do not match",
|
|
13
|
+
path: ["confirmPassword"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
type RegisterFormData = z.infer<typeof registerSchema>;
|
|
17
|
+
|
|
18
|
+
const formData: RegisterFormData = {
|
|
19
|
+
fullName: "Bharat",
|
|
20
|
+
email: "bharat@example.com",
|
|
21
|
+
password: "123456",
|
|
22
|
+
confirmPassword: "123456",
|
|
23
|
+
age: 20,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const result = registerSchema.safeParse(formData);
|
|
27
|
+
|
|
28
|
+
if (!result.success) {
|
|
29
|
+
console.log(result.error.flatten().fieldErrors);
|
|
30
|
+
} else {
|
|
31
|
+
console.log("Valid Data", result.data);
|
|
32
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// productSlice.js
|
|
2
|
+
|
|
3
|
+
import { createSlice } from "@reduxjs/toolkit";
|
|
4
|
+
|
|
5
|
+
const initialState = {
|
|
6
|
+
products: [
|
|
7
|
+
{
|
|
8
|
+
id: 1,
|
|
9
|
+
title: "Handmade Vase",
|
|
10
|
+
price: 800,
|
|
11
|
+
stock: 5,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 2,
|
|
15
|
+
title: "Wooden Craft",
|
|
16
|
+
price: 1200,
|
|
17
|
+
stock: 3,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const productSlice = createSlice({
|
|
23
|
+
name: "products",
|
|
24
|
+
initialState,
|
|
25
|
+
reducers: {
|
|
26
|
+
decreaseStock: (state, action) => {
|
|
27
|
+
const product = state.products.find(
|
|
28
|
+
(item) => item.id === action.payload
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
if (product && product.stock > 0) {
|
|
32
|
+
product.stock -= 1;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
increaseStock: (state, action) => {
|
|
37
|
+
const product = state.products.find(
|
|
38
|
+
(item) => item.id === action.payload
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (product) {
|
|
42
|
+
product.stock += 1;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const { decreaseStock, increaseStock } = productSlice.actions;
|
|
49
|
+
export default productSlice.reducer;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
Suspense,
|
|
3
|
+
lazy,
|
|
4
|
+
useEffect,
|
|
5
|
+
useState,
|
|
6
|
+
} from "react";
|
|
7
|
+
|
|
8
|
+
type Product = {
|
|
9
|
+
id: number;
|
|
10
|
+
title: string;
|
|
11
|
+
price: number;
|
|
12
|
+
thumbnail: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ProductCardProps = {
|
|
16
|
+
title: string;
|
|
17
|
+
price: number;
|
|
18
|
+
thumbnail: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ProductCard = ({
|
|
22
|
+
title,
|
|
23
|
+
price,
|
|
24
|
+
thumbnail,
|
|
25
|
+
}: ProductCardProps) => {
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
style={{
|
|
29
|
+
border: "1px solid gray",
|
|
30
|
+
padding: "10px",
|
|
31
|
+
marginBottom: "10px",
|
|
32
|
+
width: "250px",
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<img
|
|
36
|
+
src={thumbnail}
|
|
37
|
+
alt={title}
|
|
38
|
+
width="200"
|
|
39
|
+
height="150"
|
|
40
|
+
/>
|
|
41
|
+
|
|
42
|
+
<h3>{title}</h3>
|
|
43
|
+
|
|
44
|
+
<p>${price}</p>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const ProductListComponent = () => {
|
|
50
|
+
const [products, setProducts] = useState<Product[]>([]);
|
|
51
|
+
const [skip, setSkip] = useState(0);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const controller = new AbortController();
|
|
55
|
+
|
|
56
|
+
const fetchProducts = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const response = await fetch(
|
|
59
|
+
`https://dummyjson.com/products?limit=5&skip=${skip}`,
|
|
60
|
+
{
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const data = await response.json();
|
|
66
|
+
|
|
67
|
+
setProducts((prev) => [
|
|
68
|
+
...prev,
|
|
69
|
+
...data.products,
|
|
70
|
+
]);
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
if (error.name === "AbortError") {
|
|
73
|
+
console.log("Request Cancelled");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
fetchProducts();
|
|
79
|
+
|
|
80
|
+
return () => {
|
|
81
|
+
controller.abort();
|
|
82
|
+
};
|
|
83
|
+
}, [skip]);
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<h2>Products</h2>
|
|
88
|
+
|
|
89
|
+
{products.map((product) => (
|
|
90
|
+
<ProductCard
|
|
91
|
+
key={product.id}
|
|
92
|
+
title={product.title}
|
|
93
|
+
price={product.price}
|
|
94
|
+
thumbnail={product.thumbnail}
|
|
95
|
+
/>
|
|
96
|
+
))}
|
|
97
|
+
|
|
98
|
+
<button onClick={() => setSkip((prev) => prev + 5)}>
|
|
99
|
+
Load More
|
|
100
|
+
</button>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const ProductList = lazy(
|
|
106
|
+
() =>
|
|
107
|
+
new Promise<{ default: React.ComponentType }>(
|
|
108
|
+
(resolve) => {
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
resolve({
|
|
111
|
+
default: ProductListComponent,
|
|
112
|
+
});
|
|
113
|
+
}, 1000);
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const App = () => {
|
|
119
|
+
const [showProducts, setShowProducts] =
|
|
120
|
+
useState(false);
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div>
|
|
124
|
+
<h1>Performance Optimization Demo</h1>
|
|
125
|
+
|
|
126
|
+
<button onClick={() => setShowProducts(true)}>
|
|
127
|
+
Show Products
|
|
128
|
+
</button>
|
|
129
|
+
|
|
130
|
+
{showProducts && (
|
|
131
|
+
<Suspense
|
|
132
|
+
fallback={<h2>Loading Products...</h2>}
|
|
133
|
+
>
|
|
134
|
+
<ProductList />
|
|
135
|
+
</Suspense>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default App;
|
package/package.json
CHANGED