├── .github
└── workflows
│ └── aws.yml
├── .gitignore
├── README.md
├── env
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── _redirects
├── favicon.ico
├── index.html
└── manifest.json
├── server
├── .gitignore
├── Utils
│ └── nodemailer.js
├── app.js
├── controller
│ ├── paginate.js
│ ├── productController.js
│ └── user.js
├── env.local
├── middleware
│ └── auth.js
├── models
│ ├── productModel.js
│ ├── user.js
│ └── valideUser.js
├── package-lock.json
├── package.json
└── routes
│ ├── productRoute.js
│ └── users.js
├── src
├── App.js
├── assets
│ ├── commingsoon.gif
│ ├── fastLoading.gif
│ ├── favicon.ico
│ └── homepage.svg
├── components
│ ├── Banner.js
│ ├── Footer.js
│ ├── Header.js
│ ├── Model
│ │ ├── AuthModel.js
│ │ ├── addProduct.js
│ │ ├── logoutModel.js
│ │ └── productFunctions
│ │ │ ├── category.js
│ │ │ └── shoeForOption.js
│ ├── Product.js
│ ├── ProductList.js
│ ├── cart.js
│ ├── filterProduct
│ │ ├── BrandDropdown.js
│ │ ├── Category.js
│ │ ├── PriceRangeDropdown.js
│ │ ├── Search.js
│ │ └── pagination.js
│ ├── firebase
│ │ ├── UploadImage.js
│ │ └── index.js
│ └── pageNotFound.js
├── data.js
├── index.css
├── index.js
├── pages
│ ├── AllProduct.js
│ ├── Home.js
│ ├── ProductDetails.js
│ ├── UserEmailVerification.js
│ ├── topProduct.js
│ └── wishlist.js
├── statemanagement
│ ├── api
│ │ ├── AuthenticationApi.js
│ │ ├── ShoeApi.js
│ │ ├── WishListApi.js
│ │ ├── cartApi.js
│ │ └── index.js
│ ├── slice
│ │ ├── AuthenticationSlice
│ │ │ └── index.js
│ │ ├── ShoeSlice
│ │ │ ├── Shoe.js
│ │ │ └── index.js
│ │ ├── WishList
│ │ │ └── index.js
│ │ ├── cartSlice
│ │ │ └── index.js
│ │ └── filterShoes.js
│ └── storage
│ │ └── index.js
└── toastify.js
└── tailwind.config.js
/.github/workflows/aws.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build and push a new container image to Amazon ECR,
2 | # and then will deploy a new task definition to Amazon ECS, when there is a push to the "Part_1_Shoe_Store" branch.
3 | #
4 | # To use this workflow, you will need to complete the following set-up steps:
5 | #
6 | # 1. Create an ECR repository to store your images.
7 | # For example: `aws ecr create-repository --repository-name my-ecr-repo --region us-east-2`.
8 | # Replace the value of the `ECR_REPOSITORY` environment variable in the workflow below with your repository's name.
9 | # Replace the value of the `AWS_REGION` environment variable in the workflow below with your repository's region.
10 | #
11 | # 2. Create an ECS task definition, an ECS cluster, and an ECS service.
12 | # For example, follow the Getting Started guide on the ECS console:
13 | # https://us-east-2.console.aws.amazon.com/ecs/home?region=us-east-2#/firstRun
14 | # Replace the value of the `ECS_SERVICE` environment variable in the workflow below with the name you set for the Amazon ECS service.
15 | # Replace the value of the `ECS_CLUSTER` environment variable in the workflow below with the name you set for the cluster.
16 | #
17 | # 3. Store your ECS task definition as a JSON file in your repository.
18 | # The format should follow the output of `aws ecs register-task-definition --generate-cli-skeleton`.
19 | # Replace the value of the `ECS_TASK_DEFINITION` environment variable in the workflow below with the path to the JSON file.
20 | # Replace the value of the `CONTAINER_NAME` environment variable in the workflow below with the name of the container
21 | # in the `containerDefinitions` section of the task definition.
22 | #
23 | # 4. Store an IAM user access key in GitHub Actions secrets named `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`.
24 | # See the documentation for each action used below for the recommended IAM policies for this IAM user,
25 | # and best practices on handling the access key credentials.
26 |
27 | name: Deploy to Amazon ECS
28 |
29 | on:
30 | push:
31 | branches: [ "Part_1_Shoe_Store" ]
32 |
33 | env:
34 | AWS_REGION: MY_AWS_REGION # set this to your preferred AWS region, e.g. us-west-1
35 | ECR_REPOSITORY: MY_ECR_REPOSITORY # set this to your Amazon ECR repository name
36 | ECS_SERVICE: MY_ECS_SERVICE # set this to your Amazon ECS service name
37 | ECS_CLUSTER: MY_ECS_CLUSTER # set this to your Amazon ECS cluster name
38 | ECS_TASK_DEFINITION: MY_ECS_TASK_DEFINITION # set this to the path to your Amazon ECS task definition
39 | # file, e.g. .aws/task-definition.json
40 | CONTAINER_NAME: MY_CONTAINER_NAME # set this to the name of the container in the
41 | # containerDefinitions section of your task definition
42 |
43 | permissions:
44 | contents: read
45 |
46 | jobs:
47 | deploy:
48 | name: Deploy
49 | runs-on: ubuntu-latest
50 | environment: production
51 |
52 | steps:
53 | - name: Checkout
54 | uses: actions/checkout@v3
55 |
56 | - name: Configure AWS credentials
57 | uses: aws-actions/configure-aws-credentials@v1
58 | with:
59 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
60 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
61 | aws-region: ${{ env.AWS_REGION }}
62 |
63 | - name: Login to Amazon ECR
64 | id: login-ecr
65 | uses: aws-actions/amazon-ecr-login@v1
66 |
67 | - name: Build, tag, and push image to Amazon ECR
68 | id: build-image
69 | env:
70 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
71 | IMAGE_TAG: ${{ github.sha }}
72 | run: |
73 | # Build a docker container and
74 | # push it to ECR so that it can
75 | # be deployed to ECS.
76 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
77 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
78 | echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT
79 |
80 | - name: Fill in the new image ID in the Amazon ECS task definition
81 | id: task-def
82 | uses: aws-actions/amazon-ecs-render-task-definition@v1
83 | with:
84 | task-definition: ${{ env.ECS_TASK_DEFINITION }}
85 | container-name: ${{ env.CONTAINER_NAME }}
86 | image: ${{ steps.build-image.outputs.image }}
87 |
88 | - name: Deploy Amazon ECS task definition
89 | uses: aws-actions/amazon-ecs-deploy-task-definition@v1
90 | with:
91 | task-definition: ${{ steps.task-def.outputs.task-definition }}
92 | service: ${{ env.ECS_SERVICE }}
93 | cluster: ${{ env.ECS_CLUSTER }}
94 | wait-for-service-stability: true
95 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .env
8 | # testing
9 | /coverage
10 |
11 | # misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to Shoes Store
2 | All the things like
3 | - Email verification
4 | - Firebase storage
5 | - mongoodb url
6 | - jwt token
7 |
8 | which is need to add on .env file is available on https://youtube.com/playlist?list=PLJ3uCOeGaRaKKNzSJKb1RD5-mO9mQ1qKD
9 |
10 | Don't forget to check out the video before cloning this project.
11 |
12 | Ecommerce Web Shop - Build & Deploy a Modern Shoe Store | React.js, Redux Toolkit, Node, & Tailwind.
13 | Dear Friends, In this Project We are Creating an e-commerce app from scratch. MERN Stack e-commerce project for Beginners. React, Node.js, Redux Toolkit, and Tailwind CSS for Responsive and Full-Stack Shoes Store App Course Tutorial and deploy the project to frontend project to the Netlify app and backend to the Railway app.✅
14 |
15 | Course Link : https://youtube.com/playlist?list=PLJ3uCOeGaRaKKNzSJKb1RD5-mO9mQ1qKD
16 |
--------------------------------------------------------------------------------
/env:
--------------------------------------------------------------------------------
1 | REACT_APP_BASE_URL = 'http://localhost:3000'
2 | REACT_APP_APIKEY = add your firebase api key here
3 | REACT_APP_AUTHDOMAIN = add your firebase auth domain here
4 | REACT_APP_PROJECTID = add your firebase project id here
5 | REACT_APP_STORAGEBUCKET = add your firebase storage bucket here
6 | REACT_APP_MESSAGINGSENDERID = add your firebase messaging sender id here
7 | REACT_APP_APPID = add your firebase app id here
8 |
9 | // Watch this video to create Firebase Storage Bucket:
10 | https://www.youtube.com/watch?v=0E1MM3tBqRo&list=PLJ3uCOeGaRaKKNzSJKb1RD5-mO9mQ1qKD&index=2
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shoe-store",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.4",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "firebase": "^9.9.4",
10 | "react": "^18.1.0",
11 | "react-dom": "^18.1.0",
12 | "react-scripts": "5.0.1",
13 | "swiper": "^8.4.5",
14 | "web-vitals": "^2.1.4"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": [
24 | "react-app",
25 | "react-app/jest"
26 | ]
27 | },
28 | "browserslist": {
29 | "production": [
30 | ">0.2%",
31 | "not dead",
32 | "not op_mini all"
33 | ],
34 | "development": [
35 | "last 1 chrome version",
36 | "last 1 firefox version",
37 | "last 1 safari version"
38 | ]
39 | },
40 | "devDependencies": {
41 | "@headlessui/react": "^1.6.4",
42 | "@reduxjs/toolkit": "^1.9.0",
43 | "@tailwindcss/forms": "^0.5.3",
44 | "autoprefixer": "^10.4.7",
45 | "axios": "^1.1.3",
46 | "postcss": "^8.4.14",
47 | "react-icons": "^4.4.0",
48 | "react-jwt": "^1.1.7",
49 | "react-redux": "^8.0.5",
50 | "react-router-dom": "^6.3.0",
51 | "react-toastify": "^9.1.1",
52 | "tailwindcss": "^3.0.24"
53 | },
54 | "main": "postcss.config.js",
55 | "author": "dilip dawadi",
56 | "license": "MIT"
57 | }
58 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dilip-dawadi/shoeStore/46f637932e73f92fb2cfcd22ab13b7d7fd64a5f0/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
23 | Shoe Store
24 |
25 |
26 |
27 | You need to enable JavaScript to run this app.
28 |
29 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 | /.env
15 | .env
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 | .pnpm-debug.log*
28 |
29 | # local env files
30 | .env*.local
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/server/Utils/nodemailer.js:
--------------------------------------------------------------------------------
1 | import { createTransport } from "nodemailer";
2 |
3 | export const sendEmail = async (email, subject, text) => {
4 | try {
5 | const transporter = createTransport({
6 | host: process.env.HOST,
7 | service: process.env.SERVICE,
8 | port: Number(process.env.EMAIL_PORT),
9 | secure: Boolean(process.env.SECURE),
10 | auth: {
11 | user: process.env.USER,
12 | pass: process.env.PASS,
13 | },
14 | });
15 | await transporter.sendMail({
16 | from: process.env.USER,
17 | to: email,
18 | subject: subject,
19 | text: text,
20 | html: `
21 |
22 |
31 |
${subject}
32 |
33 | Thank you for registering with us. Please click on the link below to verify your email address. Note: This link will expire in 24 hours
34 |
Verify Account
35 |
If you did not register with us, please ignore this email.
36 |
37 |
44 |
© 2022 Shoe Store. All rights reserved.
45 |
46 |
47 | `,
48 | });
49 | return { status: 200 };
50 | } catch (error) {
51 | return { status: error.responseCode };
52 | }
53 | };
54 |
55 | export const CheckoutEmail = async (subject, user, total, cart, products) => {
56 | try {
57 | const transporter = createTransport({
58 | host: process.env.HOST,
59 | service: process.env.SERVICE,
60 | port: Number(process.env.EMAIL_PORT),
61 | secure: Boolean(process.env.SECURE),
62 | auth: {
63 | user: process.env.USER,
64 | pass: process.env.PASS,
65 | },
66 | });
67 | await transporter.sendMail({
68 | from: process.env.USER,
69 | to: user.email,
70 | subject: subject,
71 | text: "Thank you for shopping with us",
72 | html: `
73 |
74 |
75 |
76 |
77 |
78 |
79 | Order Summary
80 |
117 |
118 |
119 |
120 |
Order Confirmation
121 |
122 | Dear ${user.name}, Thank you for shopping with us. Use your order id ${user._id} to track your order.
123 |
124 |
125 | The total amount of your order is ${total}$.
126 |
127 |
Order Summary
128 |
129 |
130 | Product Name
131 | Quantity
132 | Price
133 | Address
134 |
135 |
136 | ${products.map((item) => { return `${item.title}`; })}
137 | ${products.map((item) => {
138 | const quantity = cart.find((cartItem) => cartItem.cartId === item._id.toString()).quantity;
139 | return `${quantity}`;
140 | })}
141 | ${products.map((item) => { return `${item.price}`; })}
142 | ${user.address}
143 |
144 |
145 |
Thank you for shopping with us. Your order has been received and is being processed. You will receive a confirmation email once your order has shipped.
146 |
If you did not place this order, please ignore this email.
147 |
148 |
154 |
© 2022 Shoe Store. All rights reserved.
155 |
156 |
157 | `,
158 | });
159 | return { status: 200 };
160 | } catch (error) {
161 | console.log(error + "error");
162 | return { status: error.responseCode };
163 | }
164 | };
165 |
166 |
--------------------------------------------------------------------------------
/server/app.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import bodyParser from "body-parser";
3 | import mongoose from "mongoose";
4 | import cors from "cors";
5 | import dotenv from "dotenv";
6 | import userRoutes from "./routes/users.js";
7 | import shoesPageRoutes from "./routes/productRoute.js";
8 | import morgan from "morgan";
9 | import cookieParser from "cookie-parser";
10 | const app = express();
11 | app.use(cookieParser());
12 |
13 | dotenv.config();
14 | app.use(bodyParser.json({ limit: "30mb", extended: true }));
15 | app.use(bodyParser.urlencoded({ limit: "30mb", extended: true }));
16 | app.use(cors(
17 | {
18 | origin: "http://localhost:3000",
19 | credentials: true,
20 | exposedHeaders: ["set-cookie"],
21 | }
22 | ));
23 | app.use(morgan("dev"));
24 | app.use("/user", userRoutes);
25 | app.use("/shoesPage", shoesPageRoutes);
26 | app.get("/", (req, res) => {
27 | res.send("Hello this is Shoes Store");
28 | });
29 |
30 | const PORT = process.env.PORT || 5000;
31 |
32 | mongoose
33 | .connect(process.env.CONNECTION_URL, {
34 | useNewUrlParser: true,
35 | useUnifiedTopology: true,
36 | })
37 | .then(() => app.listen(PORT, console.log(`Server running ${PORT}`)))
38 | .catch(error => console.log(error));
39 |
40 | export default app;
41 |
--------------------------------------------------------------------------------
/server/controller/paginate.js:
--------------------------------------------------------------------------------
1 | export class APIfeatures {
2 | constructor(query, queryString) {
3 | this.query = query;
4 | this.queryString = queryString;
5 | this.running = query;
6 | this.lounging = query;
7 | this.everyday = query;
8 | this.paginate = queryString;
9 | }
10 | filtering() {
11 | const queryObj = { ...this.queryString };
12 | if (queryObj.price) {
13 | queryObj.price = queryObj.price.split(" - ");
14 | queryObj.price = {
15 | $gte: parseInt(queryObj.price[0]),
16 | $lte: parseInt(queryObj.price[1]),
17 | };
18 | }
19 | if (queryObj.brand) {
20 | queryObj.brand = {
21 | $regex: queryObj.brand,
22 | $options: "i",
23 | };
24 | }
25 | const excludedFields = ["page", "sort", "limit"];
26 | excludedFields.forEach((el) => delete queryObj[el]);
27 | if (queryObj.brand && queryObj.category && queryObj.price) {
28 | this.query.find(queryObj);
29 | this.queryString = queryObj;
30 | return this;
31 | } else if (!queryObj.brand && queryObj.category && queryObj.price) {
32 | delete queryObj.brand;
33 | this.queryString = queryObj;
34 | this.query.find(queryObj);
35 | return this;
36 | } else if (queryObj.brand && !queryObj.category && queryObj.price) {
37 | delete queryObj.category;
38 | this.queryString = queryObj;
39 | this.query.find(queryObj);
40 | return this;
41 | } else if (queryObj.brand && queryObj.category && !queryObj.price) {
42 | delete queryObj.price;
43 | this.queryString = queryObj;
44 | this.query.find(queryObj);
45 | return this;
46 | } else if (!queryObj.brand && !queryObj.category && queryObj.price) {
47 | delete queryObj.brand;
48 | delete queryObj.category;
49 | this.queryString = queryObj;
50 | this.query.find(queryObj);
51 | return this;
52 | } else if (!queryObj.brand && queryObj.category && !queryObj.price) {
53 | delete queryObj.brand;
54 | delete queryObj.price;
55 | this.queryString = queryObj;
56 | this.query.find(queryObj);
57 | return this;
58 | } else if (queryObj.brand && !queryObj.category && !queryObj.price) {
59 | delete queryObj.category;
60 | delete queryObj.price;
61 | this.queryString = queryObj;
62 | this.query.find(queryObj);
63 | return this;
64 | } else {
65 | this.queryString = {};
66 | this.query.find();
67 | return this;
68 | }
69 | }
70 | sorting() {
71 | if (this.queryString.sort) {
72 | const sortBy = this.queryString.sort;
73 | this.query = this.query.sort(sortBy);
74 | } else {
75 | this.query = this.query.sort("-createdAt");
76 | }
77 |
78 | return this;
79 | }
80 | paginating() {
81 | const page = this.paginate.page || 1;
82 | const limit = this.paginate.limit * 1 || 8;
83 | const skip = (page - 1) * limit;
84 | this.paginate = { skip, limit };
85 | this.query = this.query.skip(skip).limit(limit);
86 | return this;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/server/controller/productController.js:
--------------------------------------------------------------------------------
1 | import productModel from "../models/productModel.js";
2 | import { APIfeatures } from "./paginate.js";
3 |
4 | export const getproductPage = async (req, res) => {
5 | try {
6 | req.query.page = parseInt(req.query.page);
7 | req.query.limit = parseInt(req.query.limit);
8 | const features = new APIfeatures(productModel.find(), req.query).sorting().paginating().filtering()
9 | const data = await features.query;
10 | const paginateRemaining = features.paginate;
11 | const runnning = await productModel.find(features.queryString).find({ shoeFor: "Running" }).skip(paginateRemaining.skip).limit(paginateRemaining.limit);
12 | const lounging = await productModel.find(features.queryString).find({ shoeFor: "Lounging" }).skip(paginateRemaining.skip).limit(paginateRemaining.limit);
13 | const everyday = await productModel.find(features.queryString).find({ shoeFor: "Everyday" }).skip(paginateRemaining.skip).limit(paginateRemaining.limit);
14 | res.status(200).json({ data, runnning, lounging, everyday });
15 | } catch (error) {
16 | console.log(error);
17 | res.status(404).json({ message: error });
18 | }
19 | };
20 |
21 | export const getTopProducts = async (req, res) => {
22 | try {
23 | const data = await productModel.find({}).sort({ sold: -1 }).limit(12);
24 | res.json({ data });
25 | } catch (error) {
26 | res.status(404).json({ message: error.message });
27 | }
28 | };
29 |
30 | export const createproductPage = async (req, res) => {
31 | const { title, description, selectedFile, price, category, quantity, shoeFor, brand } = req.body;
32 | try {
33 | if (!title || !description) {
34 | return res.status(400).json({
35 | message: "Please provide all required fields",
36 | });
37 | }
38 | if (!selectedFile) {
39 | return res.status(400).json({
40 | message: "Please provide a file",
41 | });
42 | }
43 | if (!price) {
44 | return res.status(400).json({
45 | message: "Please provide a price",
46 | });
47 | }
48 | if (!category) {
49 | return res.status(400).json({
50 | message: "Please provide a category",
51 | });
52 | }
53 | if (!quantity) {
54 | return res.status(400).json({
55 | message: "Please provide a quantity",
56 | });
57 | }
58 | if (!shoeFor) {
59 | return res.status(400).json({
60 | message: "Please provide a shoeFor",
61 | });
62 | }
63 | if (!brand) {
64 | return res.status(400).json({
65 | message: "Please provide a brand",
66 | });
67 | }
68 | const productPageData = new productModel({
69 | title,
70 | description,
71 | selectedFile,
72 | price,
73 | category,
74 | quantity,
75 | shoeFor,
76 | brand,
77 | });
78 | const savedproductPage = await productPageData.save();
79 | res
80 | .status(200)
81 | .json({ data: savedproductPage, message: `${savedproductPage.title} created successfully` });
82 | } catch (error) {
83 | res.json({
84 | message: error.message,
85 | });
86 | }
87 | };
88 |
89 | export const updateProductById = async (req, res) => {
90 | const { id } = req.params;
91 | const { title, description, selectedFile, price, category, quantity, shoeFor, brand } = req.body;
92 | try {
93 | if (!title || !description) {
94 | return res.status(400).json({
95 | message: "Please provide all required fields",
96 | });
97 | }
98 | if (!selectedFile) {
99 | return res.status(400).json({
100 | message: "Please provide a file",
101 | });
102 | }
103 | if (!price) {
104 | return res.status(400).json({
105 | message: "Please provide a price",
106 | });
107 | }
108 | if (!category) {
109 | return res.status(400).json({
110 | message: "Please provide a category",
111 | });
112 | }
113 | if (!quantity) {
114 | return res.status(400).json({
115 | message: "Please provide a quantity",
116 | });
117 | }
118 | if (!shoeFor) {
119 | return res.status(400).json({
120 | message: "Please provide a shoeFor",
121 | });
122 | }
123 | if (!brand) {
124 | return res.status(400).json({
125 | message: "Please provide a brand",
126 | });
127 | }
128 | const updatedProduct = await productModel.findByIdAndUpdate(
129 | id,
130 | {
131 | title,
132 | description,
133 | selectedFile,
134 | price,
135 | category,
136 | quantity,
137 | shoeFor,
138 | brand,
139 | },
140 | { new: true }
141 | );
142 | res.status(200).json({ data: updatedProduct, message: `${updatedProduct.title} updated successfully` });
143 | } catch (error) {
144 | res.status(404).json({ message: error.message });
145 | }
146 | };
147 |
148 | export const getProductById = async (req, res) => {
149 | const { id } = req.params;
150 | try {
151 | const ProductById = await productModel.findById(id);
152 | const title = ProductById.title;
153 | res.json({ data: ProductById, message: "Product " + title });
154 | } catch (error) {
155 | res.status(404).json({ message: error });
156 | }
157 | };
158 |
159 | export const getfilterProduct = async (req, res) => {
160 | try {
161 | const data = await productModel.find({}).select("brand category");
162 | const pages = await productModel.find().countDocuments();
163 | const limit = 8;
164 | const totalPages = Math.ceil(pages / limit);
165 | const pageArray = [];
166 | for (let i = 1; i <= totalPages; i++) {
167 | pageArray.push(i);
168 | }
169 | const brand = data.map((item) => item.brand);
170 | const category = data.map((item) => item.category);
171 | const allBrand = brand.reduce((acc, val) => acc.concat(val), []);
172 | const allCategory = category.reduce((acc, val) => acc.concat(val), []);
173 | const brandFilter = allBrand.filter((item) => item !== undefined && item !== null);
174 | const categoryFilter = allCategory.filter((item) => item !== undefined && item !== null);
175 | const brandCapatalize = brandFilter.map((item) => item.charAt(0).toUpperCase() + item.slice(1));
176 | const categoryCapatalize = categoryFilter.map((item) => item.charAt(0).toUpperCase() + item.slice(1));
177 | const uniqueBrand = [...new Set(brandCapatalize)];
178 | const uniqueCategory = [...new Set(categoryCapatalize)];
179 | res.json({ data: { brand: uniqueBrand, category: uniqueCategory, pageNumbers: pageArray } });
180 | } catch (error) {
181 | res.status(404).json({ message: error.message });
182 | }
183 | };
--------------------------------------------------------------------------------
/server/controller/user.js:
--------------------------------------------------------------------------------
1 | import bcrypt from 'bcryptjs';
2 | import jwt from 'jsonwebtoken';
3 | import User from "../models/user.js";
4 | import verifyUser from "../models/valideUser.js";
5 | import { CheckoutEmail, sendEmail } from "../Utils/nodemailer.js";
6 | import Product from '../models/productModel.js';
7 | import { APIfeatures } from './paginate.js';
8 | const generateToken = (data) => {
9 | const { email, name, role, wishlist, number, address, cart } = data;
10 | return jwt.sign({ email, name, role, wishlist, number, address, cart }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN });
11 | }
12 | const generateSessionToken = (data, res) => {
13 | const { _id, role } = data;
14 | const token = jwt.sign({ _id, role }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN });
15 | res.cookie('token', token, {
16 | expires: new Date(Date.now() + process.env.JWT_COOKIE_EXPIRES_IN * 24 * 60 * 60 * 1000),
17 | httpOnly: true,
18 | secure: process.env.NODE_ENV === 'production' ? true : false,
19 | sameSite: 'strict'
20 | });
21 | }
22 |
23 | export const signin = async (req, res) => {
24 | const { email, password } = req.body;
25 | try {
26 | if (!email || !password) {
27 | return res.status(400).json({ message: "Please fill all the fields" });
28 | }
29 | const existingUser = await User.findOne({ email });
30 | if (!existingUser) return res.status(404).json({ message: "User doesn't exist." });
31 | const isPasswordCorrect = await bcrypt.compare(password, existingUser.password);
32 | if (!isPasswordCorrect) return res.status(400).json({ message: "Invalid credentials." });
33 | const token = generateToken(existingUser);
34 | if (!existingUser?.verifiedUser) {
35 | let checkVerify = await verifyUser.findOne({ userId: existingUser._id });
36 | if (!checkVerify) {
37 | checkVerify = await new verifyUser({
38 | userId: existingUser._id,
39 | token: token,
40 | }).save();
41 | const url = `${process.env.BASE_URL}user/${existingUser._id}/verify/${checkVerify.token}`;
42 | sendEmail(existingUser.email, "Verify Email from Shoes Store", url);
43 | return res.status(355).json({ message: 'Please verify your email' });
44 | }
45 | return res
46 | .status(355)
47 | .send({ message: "Verify link has already been sent to your email" });
48 | }
49 | generateSessionToken(existingUser, res);
50 | // check it is morning or evening
51 | const time = new Date().getHours();
52 | let greeting;
53 | if (time >= 5 && time < 12) {
54 | greeting = "Good Morning";
55 | } else if (time >= 12 && time < 17) {
56 | greeting = "Good Afternoon";
57 | } else if (time >= 17 && time < 20) {
58 | greeting = "Good Evening";
59 | } else {
60 | greeting = "Good Night";
61 | }
62 | existingUser.role === true ? res.status(200).json({ token, message: `${greeting} & Welcome Admin, ${existingUser.name.split(" ")[0]}` }) : res.status(200).json({ token, message: `${greeting} & Welcome Back, ${existingUser.name.split(" ")[0]}` });
63 | } catch (err) {
64 | res.status(500).json({ message: err.message })
65 | }
66 | }
67 |
68 | export const signOut = async (req, res) => {
69 | const { userId } = req;
70 | try {
71 | if (!userId) {
72 | return res.status(400).json({ message: "User not found" });
73 | }
74 | const userName = await User.findById(userId).select("name");
75 | res.clearCookie("token");
76 | res.status(200).json({ message: `Goodbye, ${userName.name.split(" ")[0]}` });
77 | } catch (err) {
78 | res.status(500).json({ message: err.message })
79 | }
80 | }
81 |
82 | export const signup = async (req, res) => {
83 | const { email, password, firstName, number, lastName, role, address } = req.body;
84 | try {
85 | let existingUser = await User.findOne({ email });
86 | if (existingUser) return res.status(400).json({ message: "User already exists." });
87 | if (firstName === "" || lastName === "" || email === "" || password === "") {
88 | return res.status(400).json({ message: "Please fill all the fields" });
89 | } else if (password.length < 6) {
90 | return res.status(400).json({ message: "Password must be atleast 6 characters long" });
91 | }
92 | const hashedPassword = await bcrypt.hash(password, 12);
93 | existingUser = await new User({
94 | email,
95 | password: hashedPassword,
96 | name: `${firstName} ${lastName}`,
97 | number,
98 | role,
99 | address
100 | }).save();
101 | const createVerify = await new verifyUser({
102 | userId: existingUser._id,
103 | token: generateToken(existingUser),
104 | }).save();
105 | const url = `${process.env.BASE_URL}user/${existingUser._id}/verify/${createVerify.token}`;
106 | const { status } = await sendEmail(existingUser.email, "Verify Email from Shoes Store", url);
107 | if (status < 400) {
108 | res.status(200).json({ message: "User registered successfully. Please verify your email" });
109 | } else {
110 | res.status(355).json({ message: "email verification fail check your terminal you got error message" });
111 | }
112 | } catch (error) {
113 | res.json({
114 | message: error.message
115 | })
116 | }
117 | };
118 |
119 | export const getVerified = async (req, res) => {
120 | const { userId, verifyId } = req.params;
121 | try {
122 | const user = await User.findOne({ _id: userId });
123 | if (!user) return res.status(404).json({ message: "User not found" });
124 | const Verified = await verifyUser.findOne({
125 | userId: user._id,
126 | token: verifyId,
127 | });
128 | if (!Verified) return res.status(404).json({ message: "Invalid verification link" });
129 | await User.updateOne({ _id: user._id }, { verifiedUser: true });
130 | await Verified.remove();
131 | const token = generateToken(user);
132 | generateSessionToken(user, res);
133 | user.role === 1 ? res.status(200).json({ token, message: `Welcome Admin, ${user.name.split(" ")[0]}`, verifyMessage: " Email Verified" }) : res.status(200).json({ token, message: `Welcome Back, ${user.name.split(" ")[0]}`, verifyMessage: " Email Verified" });
134 | } catch (error) {
135 | res.status(500).send({ message: error.message });
136 | }
137 | };
138 |
139 | export const addWishlist = async (req, res) => {
140 | const { id } = req.params;
141 | try {
142 | const user = await User.findById(req.userId);
143 | if (!user) return res.status(404).json({ message: "User not found" });
144 | const checkWishlist = user.wishlist.find((item) => item === id);
145 | if (checkWishlist) {
146 | return res.status(400).json({ data: user.wishlist, message: "Product already in wishlist" });
147 | } else {
148 | user.wishlist.push(id);
149 | await user.save();
150 | const token = generateToken(user);
151 | res.status(200).json({ token, data: user.wishlist, message: "Product added to wishlist" });
152 | }
153 | } catch (error) {
154 | res.status(500).json({ message: error.message });
155 | }
156 | };
157 |
158 | export const removeWishlist = async (req, res) => {
159 | const { id } = req.params;
160 | try {
161 | const user = await User.findById(req.userId);
162 | if (!user) return res.status(404).json({ message: "User not found" });
163 | const checkWishlist = user.wishlist.find((item) => item === id);
164 | if (!checkWishlist) {
165 | return res.status(400).json({ data: user.wishlist, message: "Product not in wishlist" });
166 | }
167 | user.wishlist = user.wishlist.filter((item) => item !== id);
168 | await user.save();
169 | const token = generateToken(user);
170 | res.status(200).json({ token, data: user.wishlist, message: "Product removed from wishlist" });
171 | } catch (error) {
172 | res.status(500).json({ message: error.message });
173 | }
174 | };
175 |
176 | export const getWishlist = async (req, res) => {
177 | try {
178 | const user = await User.findById(req.userId);
179 | if (!user) return res.status(404).json({ message: "User not found" });
180 | req.query.page = parseInt(req.query.page);
181 | req.query.limit = parseInt(req.query.limit);
182 | const features = new APIfeatures(Product.find({ _id: { $in: user.wishlist } }), req.query).sorting().paginating().filtering()
183 | const data = await features.query;
184 | const token = generateToken(user);
185 | res.status(200).json({ token, data });
186 | } catch (error) {
187 | res.status(500).json({ message: error.message });
188 | }
189 | };
190 |
191 | export const getCart = async (req, res) => {
192 | try {
193 | const user = await User.findById(req.userId);
194 | if (!user) return res.status(404).json({ message: "User not found" });
195 | const products = await Product.find({ _id: { $in: user.cart.map((item) => item.cartId) } });
196 | const token = generateToken(user);
197 | res.status(200).json({ token, data: products });
198 | } catch (error) {
199 | res.status(500).json({ message: error.message });
200 | }
201 | };
202 |
203 | export const addCart = async (req, res) => {
204 | const { id } = req.params;
205 | try {
206 | const user = await User.findById(req.userId);
207 | if (!user) return res.status(404).json({ message: "User not found" });
208 | const checkCart = user.cart.find((item) => item.cartId === id);
209 | if (checkCart) {
210 | return res.status(400).json({ data: user.cart, message: "Product already in cart" });
211 | } else {
212 | const product = await Product.findById(id);
213 | if (product.quantity === 0) {
214 | return res.status(400).json({ data: user.cart, message: "Product out of stock" });
215 | } else {
216 | const cartId = id;
217 | const quantity = 1;
218 | user.cart.push({ cartId, quantity });
219 | await user.save();
220 | const token = generateToken(user);
221 | res.status(200).json({ token, data: user.cart, message: "Product added to cart" });
222 | }
223 | }
224 | } catch (error) {
225 | res.status(500).json({ message: error.message });
226 | }
227 | };
228 |
229 | export const removeCart = async (req, res) => {
230 | const { id } = req.params;
231 | try {
232 | const user = await User.findById(req.userId);
233 | if (!user) return res.status(404).json({ message: "User not found" });
234 | const checkCart = user.cart.find((item) => item.cartId === id);
235 | if (!checkCart) {
236 | return res.status(400).json({ data: user.cart, message: "Product not in cart" });
237 | } else {
238 | user.cart = user.cart.filter((item) => item.cartId !== id);
239 | await user.save();
240 | const token = generateToken(user);
241 | res.status(200).json({ token, data: user.cart, message: "Product removed from cart" });
242 | }
243 | } catch (error) {
244 | res.status(500).json({ message: error.message });
245 | }
246 | };
247 |
248 | export const cartQuantity = async (req, res) => {
249 | try {
250 | const { id } = req.params;
251 | const { status } = req.body;
252 | const user = await User.findById(req.userId);
253 | if (!user) return res.status(400).json({ message: "User does not exist." });
254 | const product = await Product.findById(id);
255 | const quantity = user.cart.find((item) => item.cartId === id).quantity;
256 | if (status === "increase") {
257 | if (product.quantity <= quantity) { return res.status(400).json({ message: "Product out of stock" }); }
258 | }
259 | const checkCart = user.cart.find((item) => item.cartId === id);
260 | if (!checkCart) {
261 | return res.status(400).json({ data: user.cart, message: "Product not in cart" });
262 | } else {
263 | if (status === "increase") {
264 | user.cart = user.cart.map((item) => {
265 | console.log(item);
266 | if (item.cartId === id) {
267 | item.quantity = item.quantity + 1;
268 | }
269 | return item;
270 | });
271 | } else {
272 | user.cart = user.cart.map((item) => {
273 | if (item.cartId === id) {
274 | if (item.quantity === 1) {
275 | return item;
276 | } else {
277 | item.quantity = item.quantity - 1;
278 | }
279 | }
280 | return item;
281 | });
282 | }
283 | await User.findByIdAndUpdate({ _id: req.userId }, {
284 | cart: user.cart,
285 | });
286 | console.log(user.cart);
287 | const token = generateToken(user);
288 | res.status(200).json({ token, data: user.cart, message: "Cart quantity updated" });
289 | }
290 | } catch (err) {
291 | return res.status(500).json({ message: err.message })
292 | }
293 | }
294 |
295 | export const checkout = async (req, res) => {
296 | try {
297 | const { total } = req.body;
298 | const user = await User.findById(req.userId);
299 | if (!user) return res.status(404).json({ message: "User not found" });
300 | const products = await Product.find({ _id: { $in: user.cart.map((item) => item.cartId) } });
301 | const cart = user.cart;
302 | CheckoutEmail("Order Summary", user, total, cart, products);
303 | const updateUser = await User.findByIdAndUpdate({ _id: req.userId }, {
304 | cart: [],
305 | });
306 | const token = generateToken(updateUser);
307 | res.status(200).json({ token, message: "Checkout successful, check your email for details" });
308 | } catch (error) {
309 | res.status(500).json({ message: error.message });
310 | }
311 | };
--------------------------------------------------------------------------------
/server/env.local:
--------------------------------------------------------------------------------
1 | CONNECTION_URL = https://shoestore12.netlify.app/
2 | JWT = add secure password
3 | HOST = smtp.gmail.com
4 | SALT = 10
5 | SERVICE = gmail
6 | EMAIL_PORT = 587
7 | SECURE = true
8 | USER = 'your gmail account'
9 | PASS = 'your generate password'
10 | PORT = 5000
11 | BASE_URL = 'your base url of frontend'
12 |
13 | // Watch this video to create CONNECTION_URL, gmail account ( User ) and generate password ( Pass )
14 | https://www.youtube.com/watch?v=0E1MM3tBqRo
15 |
--------------------------------------------------------------------------------
/server/middleware/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | const auth = async (req, res, next) => {
3 | try {
4 | if (!req.cookies.token) {
5 | return res.status(401).json({ message: "Session expired, please login again" });
6 | }
7 | const cookie = req.cookies.token;
8 | let decodedData;
9 | decodedData = jwt.verify(cookie, process.env.JWT_SECRET);
10 | if (decodedData) {
11 | req.userId = decodedData?._id;
12 | next();
13 | } else {
14 | return res.clearCookie("token");
15 | }
16 | } catch (error) {
17 | res.clearCookie("token");
18 | res.status(440).json({ message: "Sorry, you are not authorized" });
19 | }
20 | };
21 |
22 | const checkAdmin = async (req, res, next) => {
23 | try {
24 | if (!req.cookies.token) {
25 | return res.status(401).json({ message: "Session expired, please login again" });
26 | }
27 | const cookie = req.cookies.token;
28 | let decodedData;
29 | decodedData = jwt.verify(cookie, process.env.JWT_SECRET);
30 | if (decodedData?.role === true) {
31 | req.userId = decodedData?._id;
32 | next();
33 | } else {
34 | res.status(440).json({ message: "Unauthorized Admin" });
35 | }
36 | } catch (error) {
37 | res.status(440).json({ message: error.message });
38 | }
39 | };
40 | export { auth, checkAdmin };
41 |
--------------------------------------------------------------------------------
/server/models/productModel.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const ProductSchema = mongoose.Schema({
4 | selectedFile: { type: [String] },
5 | title: { type: String },
6 | description: {
7 | type: String
8 | },
9 | price: {
10 | type: Number,
11 | default: 0
12 | },
13 | brand: {
14 | type: String
15 | },
16 | category: {
17 | type: [String]
18 | },
19 | shoeFor: {
20 | type: [String]
21 | },
22 | quantity: {
23 | type: Number,
24 | default: 0
25 | },
26 | sold: {
27 | type: Number,
28 | default: 0
29 | },
30 | comments: {
31 | type: Array,
32 | default: []
33 | }
34 | },
35 | {
36 | timestamps: true
37 | }
38 | );
39 |
40 | const Product = mongoose.model('Product', ProductSchema);
41 |
42 | export default Product;
--------------------------------------------------------------------------------
/server/models/user.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = mongoose.Schema(
4 | {
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | email: {
10 | type: String,
11 | required: true,
12 | unique: true,
13 | },
14 | password: {
15 | type: String,
16 | },
17 | role: { type: Boolean, default: false },
18 | wishlist: { type: Array, default: [] },
19 | cart: { type: Array, default: [] },
20 | number: { type: String },
21 | address: { type: String },
22 | verifiedUser: { type: Boolean, default: false },
23 | },
24 | {
25 | timestamps: true,
26 | }
27 | );
28 |
29 | const userDetail = mongoose.model("UserDetails", userSchema);
30 |
31 | export default userDetail;
32 |
--------------------------------------------------------------------------------
/server/models/valideUser.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | const Schema = mongoose.Schema;
3 |
4 | const tokenSchema = new Schema({
5 | userId: {
6 | type: Schema.Types.ObjectId,
7 | required: true,
8 | ref: "UserDetails",
9 | unique: true,
10 | },
11 | token: { type: String, required: true },
12 | createdAt: { type: Date, default: Date.now, expires: 1440 },
13 | });
14 | const verifyUser = mongoose.model('VerifyedUser', tokenSchema);
15 |
16 | export default verifyUser;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "app.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "nodemon app.js",
9 | "test": "jest"
10 | },
11 | "jest": {
12 | "testEnvironment": "node"
13 | },
14 | "author": "",
15 | "license": "ISC",
16 | "dependencies": {
17 | "bcryptjs": "^2.4.3",
18 | "body-parser": "^1.20.0",
19 | "concurrently": "^7.2.2",
20 | "cookie-parser": "^1.4.6",
21 | "cors": "^2.8.5",
22 | "dotenv": "^16.0.1",
23 | "express": "^4.18.1",
24 | "express-async-handler": "^1.2.0",
25 | "jest": "^28.1.3",
26 | "jsonwebtoken": "^8.5.1",
27 | "mongoose": "^6.3.3",
28 | "morgan": "^1.10.0",
29 | "nodemailer": "^6.7.5",
30 | "nodemon": "^2.0.16",
31 | "supertest": "^6.2.4"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/routes/productRoute.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getproductPage, createproductPage, getProductById, getTopProducts, getfilterProduct, updateProductById } from "../controller/productController.js";
3 | import { checkAdmin } from "../middleware/auth.js";
4 |
5 | const router = express.Router();
6 | router.get("/", getproductPage);
7 | router.get("/top", getTopProducts);
8 | router.get("/filter", getfilterProduct);
9 | router.post("/", checkAdmin, createproductPage);
10 | router.patch('/:id', updateProductById)
11 | router.get('/:id', getProductById)
12 | export default router;
--------------------------------------------------------------------------------
/server/routes/users.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import { signin, signup, signOut, getVerified, addWishlist, removeWishlist, getWishlist, getCart, addCart, removeCart, cartQuantity, checkout } from '../controller/user.js';
3 | import { auth } from '../middleware/auth.js';
4 | const router = express.Router();
5 | router.post('/signin', signin)
6 | router.post('/signup', signup)
7 | router.get('/signout', auth, signOut)
8 | router.get('/:userId/verify/:verifyId', getVerified)
9 | router.post('/wishlist/:id', auth, addWishlist)
10 | router.delete('/wishlist/:id', auth, removeWishlist)
11 | router.get('/wishlist', auth, getWishlist)
12 | router.get('/cart', auth, getCart)
13 | router.post('/cart/:id', auth, addCart)
14 | router.delete('/cart/:id', auth, removeCart)
15 | router.post('/cart/:id/quantity', auth, cartQuantity)
16 | router.post('/checkout', auth, checkout)
17 | export default router;
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { decodeToken } from 'react-jwt';
3 |
4 | import { Routes, Route, useNavigate } from 'react-router-dom';
5 | import Footer from './components/Footer';
6 | import { Header } from './components/Header';
7 | import PageNotFound from './components/pageNotFound';
8 |
9 | // import pages
10 | import Home from './pages/Home';
11 | import Products from './pages/AllProduct';
12 | import Wishlist from './pages/wishlist';
13 | import ProductDetails from './pages/ProductDetails';
14 | import UserVerification from './pages/UserEmailVerification';
15 | import { NotifyInfo } from './toastify';
16 | import { useLocation } from 'react-router-dom';
17 | const App = () => {
18 | const navigate = useNavigate();
19 | const location = useLocation();
20 | const token = localStorage.getItem('authenticate');
21 | React.useEffect(() => {
22 | if (!token) {
23 | navigate(location.pathname);
24 | } else {
25 | const decodeData = decodeToken(token);
26 | const hoursLeft = (decodeData.exp * 1000 - new Date().getTime()) / 1000 / 60 / 60;
27 | if (hoursLeft < 0) {
28 | localStorage.removeItem('authenticate');
29 | NotifyInfo('Your session has expired. Please login again');
30 | navigate('/');
31 | }
32 | if (hoursLeft < 24) {
33 | NotifyInfo('Your session will expire in ' + hoursLeft + ' hours');
34 | }
35 | }
36 | }, [token, navigate]);
37 | return (
38 |
39 |
40 |
41 | } />
42 | } />
43 | } />
44 | } />
45 | } />
46 |
47 | } />
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default App;
55 |
--------------------------------------------------------------------------------
/src/assets/commingsoon.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dilip-dawadi/shoeStore/46f637932e73f92fb2cfcd22ab13b7d7fd64a5f0/src/assets/commingsoon.gif
--------------------------------------------------------------------------------
/src/assets/fastLoading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dilip-dawadi/shoeStore/46f637932e73f92fb2cfcd22ab13b7d7fd64a5f0/src/assets/fastLoading.gif
--------------------------------------------------------------------------------
/src/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dilip-dawadi/shoeStore/46f637932e73f92fb2cfcd22ab13b7d7fd64a5f0/src/assets/favicon.ico
--------------------------------------------------------------------------------
/src/components/Banner.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from '../assets/homepage.svg';
3 | import { setCategoryValue } from '../statemanagement/slice/filterShoes';
4 | import { useDispatch } from 'react-redux';
5 | import { useNavigate } from 'react-router-dom';
6 | const Banner = () => {
7 | const dispatch = useDispatch();
8 | const navigate = useNavigate();
9 | const ShoeForMen = () => {
10 | dispatch(setCategoryValue('Men'));
11 | navigate('/products');
12 | };
13 | const ShoeForWomen = () => {
14 | dispatch(setCategoryValue('Women'));
15 | navigate('/products');
16 | };
17 | const ShoeForKids = () => {
18 | dispatch(setCategoryValue('Kids'));
19 | navigate('/products');
20 | };
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | Shoe Store
dream footwear store
28 |
29 |
30 | Shoe Store is a dream footwear store for all the shoe lovers. We have a wide range of shoes for all the occasions. Fluffy sneakers. Cushy slippers. Nights out. Days in. Quick errands. Transcontinental trips. Durable. Comfortable. Planet-friendly. Home or away, we’ve got what you needs to chill the most.
31 |
32 |
33 |
35 | Men Shoes
36 |
37 |
39 | Women Shoes
40 |
41 |
43 | Kids Shoes
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | export default Banner;
62 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | const Footer = () => {
3 | return (
4 |
5 |
6 |
7 |
10 | Shoe store
11 |
12 |
Copyright ©
13 | {new Date().getFullYear()} Shoe Store.
14 |
All rights reserved
15 |
53 |
54 |
55 |
Company
56 |
Blog
57 |
About Us
58 |
Contact us
59 |
60 |
61 |
Support
62 |
Legal policy
63 |
Status policy
64 |
Privacy policy
65 |
66 |
67 |
Contact
68 |
Shoes Nepal
69 |
Store Location
70 |
Contact Info
71 |
72 |
73 |
74 | );
75 | };
76 |
77 | export default Footer;
78 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Link, useNavigate } from 'react-router-dom';
3 | import Auth from './Model/AuthModel';
4 | import { Popover, Transition } from '@headlessui/react'
5 | import { useDispatch } from 'react-redux';
6 | // react icons
7 | import { IoIosArrowDown } from 'react-icons/io'
8 | import { BiLogOutCircle } from "react-icons/bi";
9 | import { Fragment } from 'react'
10 | import { logoutUser } from '../statemanagement/slice/AuthenticationSlice';
11 | import AddProduct from './Model/addProduct';
12 | import { decodeToken } from 'react-jwt';
13 | import Cart from './cart';
14 | export const Header = () => {
15 | const [IsSignup, setIsSignup] = React.useState(true);
16 | const dispatch = useDispatch();
17 | const navigate = useNavigate();
18 | const token = localStorage.getItem('authenticate');
19 | const decodeData = decodeToken(token);
20 | const handleLogout = () => {
21 | dispatch(logoutUser({ navigate }));
22 | };
23 | return (
24 |
75 | );
76 | };
77 |
78 | const Options = [
79 | {
80 | name: 'Products',
81 | description: 'Discover the best products for your needs.',
82 | href: '/products',
83 | icon: IconOne,
84 | },
85 | {
86 | name: 'Wishlist',
87 | description: 'Create your own collection of products.',
88 | href: '/wishlist',
89 | icon: IconTwo,
90 | },
91 | ]
92 |
93 | export default function PopoverFunction({
94 | IsSignup,
95 | setIsSignup,
96 | handleLogout,
97 | token,
98 | decodeData
99 | }) {
100 | const [open, setOpen] = React.useState(false)
101 | function openModalDropDown() {
102 | setOpen(!open)
103 | }
104 | function closeModalDropDown() {
105 | setOpen(false)
106 | }
107 | return (
108 |
109 | <>
110 |
116 | Menu
117 |
121 |
122 |
132 |
133 |
134 |
135 | {Options.map((item) => (
136 |
138 |
139 |
140 |
141 |
142 |
143 | {item.name}
144 |
145 |
146 | {item.description}
147 |
148 |
149 |
150 | ))}
151 | {!token ? (
152 |
153 |
{
156 | setIsSignup(false)
157 | }}>
158 |
159 |
160 |
{
164 | setIsSignup(true)
165 | }}>
166 |
167 |
168 |
169 | ) : (
170 |
171 |
172 | {decodeData?.role === true &&
}
173 |
174 |
175 |
176 |
177 | )}
178 |
179 |
194 |
195 |
196 |
197 | >
198 |
199 | )
200 | }
201 |
202 | function IconOne() {
203 | return (
204 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | )
219 | }
220 |
221 | function IconTwo() {
222 | return (
223 |
230 |
231 |
236 |
243 |
244 | )
245 | }
246 |
247 |
--------------------------------------------------------------------------------
/src/components/Model/AuthModel.js:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from "@headlessui/react";
2 | import React, { Fragment } from "react";
3 | import { useNavigate } from "react-router-dom";
4 | import { useDispatch } from "react-redux";
5 | import {
6 | loginaUser,
7 | registeraUser,
8 | } from "../../statemanagement/slice/AuthenticationSlice";
9 | import { NotifyInfo } from "../../toastify";
10 | export default function Auth({
11 | IsSignup,
12 | setIsSignup,
13 | text,
14 | closeModalDropDown,
15 | }) {
16 | const [isOpen, setIsOpen] = React.useState(false);
17 | const [checkbox, setCheckbox] = React.useState(false);
18 | const navigate = useNavigate();
19 | const dispatch = useDispatch();
20 | const [authData, setAuthData] = React.useState({
21 | email: "",
22 | firstName: "",
23 | lastName: "",
24 | password: "",
25 | confirmPassword: "",
26 | address: "",
27 | number: "",
28 | });
29 | function closeModal() {
30 | setIsOpen(false);
31 | window.innerWidth < 768 && closeModalDropDown();
32 | }
33 | function openModal() {
34 | setIsOpen(true);
35 | }
36 | const handleSubmit = (e) => {
37 | e.preventDefault();
38 | if (IsSignup) {
39 | if (checkbox === false) {
40 | return NotifyInfo("Please accept the terms and conditions");
41 | }
42 | }
43 | if (IsSignup) {
44 | if (authData.password !== authData.confirmPassword) {
45 | NotifyInfo("Password and Confirm Password must be same");
46 | }
47 | return dispatch(
48 | registeraUser({ authData, navigate, closeModal, closeModalDropDown })
49 | );
50 | }
51 | dispatch(
52 | loginaUser({ authData, navigate, closeModal, closeModalDropDown })
53 | );
54 | };
55 | const handleChange = (e) => {
56 | setAuthData({ ...authData, [e.target.name]: e.target.value });
57 | };
58 | return (
59 | <>
60 |
61 | {text}
62 |
63 |
64 |
65 |
66 |
75 |
76 |
77 |
78 |
79 |
88 |
89 |
260 |
261 |
262 |
263 |
264 |
265 |
266 | >
267 | );
268 | }
269 |
--------------------------------------------------------------------------------
/src/components/Model/addProduct.js:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from '@headlessui/react'
2 | import React, { Fragment } from 'react'
3 | import { useDispatch } from 'react-redux'
4 | import { createShoe } from '../../statemanagement/slice/ShoeSlice'
5 | import UploadImage from '../firebase/UploadImage'
6 | import { GiRunningShoe } from 'react-icons/gi'
7 | import Category from './productFunctions/category'
8 | import ShoeForOption from './productFunctions/shoeForOption'
9 | export default function AddProduct() {
10 | const [isOpen, setIsOpen] = React.useState(false)
11 | const dispatch = useDispatch()
12 | const [AddProductData, setAddProductData] = React.useState({
13 | title: '',
14 | description: '',
15 | price: '',
16 | category: ["Men", "Women", "Kids"],
17 | shoeFor: ["Lounging", "Everyday", "Running"],
18 | quantity: '',
19 | selectedFile: [],
20 | brand: '',
21 | })
22 | function closeModal() {
23 | setIsOpen(false)
24 | }
25 | function openModal() {
26 | setIsOpen(true)
27 | }
28 | const handleSubmit = (e) => {
29 | e.preventDefault()
30 | dispatch(createShoe({ AddProductData, closeModal }))
31 | };
32 | const handleChange = (e) => {
33 | setAddProductData({ ...AddProductData, [e.target.name]: e.target.value });
34 | };
35 | const size = window.innerWidth > 768 ? 'md' : 'sm'
36 | return (
37 | <>
38 |
42 | +
43 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
59 |
60 |
61 |
70 |
71 |
72 |
76 | Add New Shoes
77 |
78 |
79 |
Title
80 |
81 |
82 |
83 |
84 |
85 |
Brand Name
86 |
87 |
89 |
90 |
91 |
92 |
Description
93 |
94 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
Category
103 |
104 |
105 |
106 |
107 |
108 |
Shoe For
109 |
110 |
111 |
112 |
113 |
114 |
Quantity
115 |
116 |
118 |
119 |
120 |
121 |
Price
122 |
123 |
124 |
125 | Rs.
126 |
127 |
130 |
131 |
132 |
133 |
handleSubmit(e)} className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-[#fe2856] hover:bg-[#fe2856] focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#fe2856]">
134 | Add Product
135 |
136 |
138 | click on the overlay to close the popup window
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | >
148 | );
149 | }
--------------------------------------------------------------------------------
/src/components/Model/logoutModel.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dilip-dawadi/shoeStore/46f637932e73f92fb2cfcd22ab13b7d7fd64a5f0/src/components/Model/logoutModel.js
--------------------------------------------------------------------------------
/src/components/Model/productFunctions/category.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { NotifyInfo } from "../../../toastify";
3 |
4 | export default function Categorys({ category, setCategory, AddProductData }) {
5 | const ENTER = 13;
6 | const COMMA = 188;
7 | const SPACE = 32;
8 | const BACKSPACE = 8;
9 | const [value, setValue] = useState("");
10 |
11 | const handleKeyUp = (e) => {
12 | const key = e.keyCode;
13 | if (key === COMMA || key === SPACE || key === ENTER) {
14 | addCategory();
15 | }
16 | };
17 |
18 | const handleKeyDown = (e) => {
19 | const key = e.keyCode;
20 | if (key === BACKSPACE && !value) {
21 | editCategory();
22 | }
23 | };
24 |
25 | const addCategory = () => {
26 | let addCat = value.trim().replace(/,/g, "");
27 | if (!addCat) return;
28 | if (category.find((t) => t.toLowerCase() === addCat.toLowerCase())) return;
29 | setCategory({ ...AddProductData, category: [...category, addCat] });
30 | setValue("");
31 | };
32 | const resetCategory = () => {
33 | setValue("");
34 | setCategory({
35 | ...AddProductData, category: ["Men", "Women", "Kids"]
36 | })
37 | };
38 | const editCategory = () => setValue(category.pop());
39 |
40 | return (
41 |
47 | {category?.map((cat, index) => (
48 |
58 | {cat}
59 |
{
79 | setCategory({
80 | ...AddProductData,
81 | category: category.filter((t) => t !== cat || category.length === 1),
82 | });
83 | if (category.length === 1) {
84 | NotifyInfo("You must have at least one category");
85 | }
86 | }}
87 | >
88 | Remove Category
89 |
95 |
101 |
102 |
103 |
104 | ))}
105 |
Reset
107 | {/*
setValue(e.target.value)}
112 | onKeyUp={handleKeyUp}
113 | onKeyDown={handleKeyDown}
114 | /> */}
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/Model/productFunctions/shoeForOption.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { NotifyInfo } from "../../../toastify";
3 |
4 | export default function ShoeForOption({ shoeFor, setShoeFor, AddProductData }) {
5 | const reSetshoeFor = () => {
6 | setShoeFor({
7 | ...AddProductData, shoeFor: ["Lounging", "Everyday", "Running"]
8 | })
9 | };
10 | return (
11 |
17 | {shoeFor?.map((forOption, index) => (
18 |
28 | {forOption}
29 |
{
49 | setShoeFor({
50 | ...AddProductData,
51 | shoeFor: shoeFor.filter((t) => t !== forOption || shoeFor.length === 1),
52 | });
53 | if (shoeFor.length === 1) {
54 | NotifyInfo("You must have at least one shoeFor option");
55 | }
56 | }}
57 | >
58 | Remove shoeFor
59 |
65 |
71 |
72 |
73 |
74 | ))}
75 |
Reset
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/src/components/Product.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import icons
3 | import { FaHeart, FaRegHeart } from 'react-icons/fa';
4 | import { BsEyeFill } from 'react-icons/bs';
5 | import { Link } from 'react-router-dom';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { addWishList, deleteWishList } from '../statemanagement/slice/WishList';
8 | import { LoadingCircle, NotifyWarning } from '../toastify';
9 | const Product = ({ Products }) => {
10 | const dispatch = useDispatch();
11 | const { wishListIDs, loading } = useSelector((state) => state.wishList);
12 | const token = localStorage.getItem('authenticate');
13 | return (
14 |
15 |
17 |
19 |
20 |
21 |
22 |
23 |
24 | {!token ? (
25 | NotifyWarning('You must login or register to add items to your wishlist')} />
32 | ) : (
33 | wishListIDs?.find((item) => item === Products?._id) ? (
34 | loading ? :
35 | dispatch(deleteWishList(Products?._id))} />
42 | ) : (
43 | loading ? :
44 | dispatch(addWishList({ shoeId: Products?._id, product: Products }))} />
51 | )
52 | )}
53 |
54 |
55 | {Products?.shoeFor?.map((shoeF, index) => {
56 | const IndexStyle = index === 0 ? 'bg-yellow-400' : 'bg-rose-600';
57 | return (
58 | {shoeF}
59 | );
60 | }
61 | ).splice(0, 2)}
62 |
63 |
64 |
65 | {Products?.title.split(" ").slice(0, 6).join(" ")}
66 |
67 |
68 | Rs. {Products?.price}
69 |
70 |
71 |
72 | );
73 | };
74 |
75 | export default Product;
76 |
--------------------------------------------------------------------------------
/src/components/ProductList.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // import components
3 | import Product from './Product';
4 | import { LoadingCard } from '../toastify';
5 | const ProductList = ({ data, runningData, error, loungingData, everydayData, loading, title, category, style, limit }) => {
6 | if (error === true) {
7 | return (
8 |
9 | {title === 'WishList' ? 'No items in your wishlist!' : 'No Shoes Found!'}
10 |
11 | );
12 | }
13 | return (
14 |
15 |
16 |
{title} {category ? `for ${category}` : ""}
19 |
20 | Fluffy sneakers. Cushy slippers. Ridiculously fluffy pants. Home or away, we’ve got what he needs to chill the most.
21 | {loading ?
:
22 |
= 4 ? `grid md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-8` : data?.length === 3 ? `grid md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-8` : data?.length === 2 ? `grid md:grid-cols-2 lg:grid-cols-2 gap-4 lg:gap-8` : data?.length === 1 ? `grid md:grid-cols-1 lg:grid-cols-1 gap-4 lg:gap-8` : ""}>
23 | {data?.slice()?.reverse()?.map((Products, index) => {
24 | return (
25 |
28 | );
29 | }).slice(0, limit)}
30 |
31 | }
32 | {loungingData &&
33 | loungingData?.length !== 0 &&
34 | <>
35 |
36 |
Lounging Shoe {category ? `for ${category}` : ""}
39 |
40 | Fluffy sneakers. Cushy slippers. Ridiculously fluffy pants. Home or away, we’ve got what he needs to chill the most.
41 |
= 4 ? `grid md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-8` : loungingData?.length === 3 ? `grid md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-8` : loungingData?.length === 2 ? `grid md:grid-cols-2 lg:grid-cols-2 gap-4 lg:gap-8` : loungingData?.length === 1 ? `grid md:grid-cols-1 lg:grid-cols-1 gap-4 lg:gap-8` : ''}>
42 | {loungingData?.slice()?.reverse()?.map((Products, index) => {
43 | return (
44 |
45 | );
46 | }).slice(0, 4)}
47 |
48 | >
49 | }
50 | {everydayData &&
51 | everydayData?.length !== 0 &&
52 | <>
53 |
54 |
Everyday Shoe {category ? `for ${category}` : ""}
57 |
58 | Fluffy sneakers. Cushy slippers. Ridiculously fluffy pants. Home or away, we’ve got what he needs to chill the most.
59 |
= 4 ? `grid md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-8` : everydayData?.length === 3 ? `grid md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-8` : everydayData?.length === 2 ? `grid md:grid-cols-2 lg:grid-cols-2 gap-4 lg:gap-8` : everydayData?.length === 1 ? `grid md:grid-cols-1 lg:grid-cols-1 gap-4 lg:gap-8` : ""}>
60 | {everydayData?.slice()?.reverse()?.map((Products, index) => {
61 | return (
62 |
63 | );
64 | }).slice(0, 4)}
65 |
66 | >
67 | }
68 | {runningData &&
69 | runningData?.length !== 0 &&
70 | <>
71 |
72 |
Running Shoe {category ? `for ${category}` : ""}
75 |
76 | Fluffy sneakers. Cushy slippers. Ridiculously fluffy pants. Home or away, we’ve got what he needs to chill the most.
77 |
= 4 ? `grid md:grid-cols-2 lg:grid-cols-4 gap-4 lg:gap-8` : runningData?.length === 3 ? `grid md:grid-cols-2 lg:grid-cols-3 gap-4 lg:gap-8` : runningData?.length === 2 ? `grid md:grid-cols-2 lg:grid-cols-2 gap-4 lg:gap-8` : runningData?.length === 1 ? `grid md:grid-cols-1 lg:grid-cols-1 gap-4 lg:gap-8` : ""}>
78 | {runningData?.slice()?.reverse()?.map((Products, index) => {
79 | return (
80 |
81 | );
82 | }).slice(0, 4)}
83 |
84 | >
85 | }
86 |
87 |
88 | );
89 | };
90 |
91 | export default ProductList;
92 |
--------------------------------------------------------------------------------
/src/components/cart.js:
--------------------------------------------------------------------------------
1 | import { Dialog, Transition } from '@headlessui/react'
2 | import React, { Fragment, useEffect } from 'react'
3 | import { HiShoppingCart } from "react-icons/hi";
4 | import { IoIosAddCircle } from 'react-icons/io';
5 | import { AiFillMinusCircle } from 'react-icons/ai';
6 | import { useSelector, useDispatch } from 'react-redux'
7 | import { cartQuantity, getCarts, deleteCarts, checkoutAct, CartisOpen } from '../statemanagement/slice/cartSlice'
8 | import { LoadingBtn, NotifyInfo } from '../toastify';
9 | export default function Cart() {
10 | const { isOpenCart } = useSelector((state) => state.cart)
11 | const dispatch = useDispatch()
12 | useEffect(() => {
13 | dispatch(getCarts())
14 | }, [dispatch])
15 |
16 | const { cartData, cartIds, status } = useSelector((state) => state.cart);
17 | const quantityUserHasAdded = cartIds.map((item) => item.quantity)
18 | const cartIdsOnly = cartIds.map((item) => item.cartId)
19 | const data = cartData.map((item) => {
20 | const index = cartIdsOnly.indexOf(item._id)
21 | return {
22 | ...item,
23 | quantityUserAdd: quantityUserHasAdded[index]
24 | }
25 | })
26 | function closeModal() {
27 | dispatch(CartisOpen(false))
28 | }
29 | function openModal() {
30 | if (cartIds.length === 0) {
31 | return NotifyInfo("Your cart is empty")
32 | }
33 | dispatch(CartisOpen(true))
34 | }
35 | function QuantityStatus({ status, shoeId }) {
36 | dispatch(cartQuantity({ status, shoeId }))
37 | }
38 | function DeleteCart(id) {
39 | dispatch(deleteCarts(id))
40 | }
41 | function CheckoutBtn(total) {
42 | dispatch(checkoutAct(total))
43 | dispatch(CartisOpen(false))
44 | }
45 | const size = window.innerWidth > 768 ? 'md' : 'sm'
46 | return (
47 | <>
48 |
49 | {cartIds?.length || 0}
50 |
51 |
52 |
53 |
54 |
55 |
64 |
65 |
66 |
67 |
68 |
77 |
78 |
79 |
83 | Your Cart
84 |
85 |
86 | {data?.slice()?.reverse()?.map((Products, index) => {
87 | return (
88 |
89 |
90 |
91 |
DeleteCart(Products?._id)} />
94 |
95 |
96 |
{Products?.title?.slice(0, 15)}
97 |
98 |
99 | QuantityStatus({
102 | shoeId: Products?._id,
103 | status: 'increase'
104 | })} />
105 |
106 |
{
107 | status === 'increment' ? : Products?.quantityUserAdd
108 | }
109 |
110 |
QuantityStatus({
111 | shoeId: Products?._id,
112 | status: 'decrease'
113 | })} />
114 |
115 |
116 |
117 |
118 | {Products?.quantityUserAdd} x
119 |
120 |
121 | Rs. {Products?.quantityUserAdd * Products?.price}
122 |
123 |
124 |
125 |
126 |
127 |
128 | )
129 | })}
130 |
131 |
132 |
133 |
Total = Rs. {data?.reduce((acc, item) => acc + item?.quantityUserAdd * item?.price, 0)}
134 |
135 |
CheckoutBtn(data?.reduce((acc, item) => acc + item?.quantityUserAdd * item?.price, 0))}>Checkout
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 | >
146 | )
147 | }
--------------------------------------------------------------------------------
/src/components/filterProduct/BrandDropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { RiArrowDownSLine, RiArrowUpSLine } from 'react-icons/ri';
3 | import { Menu } from '@headlessui/react';
4 | import { GiRunningShoe } from 'react-icons/gi';
5 | import { useSelector } from 'react-redux';
6 | import { LoadingBtn } from '../../toastify';
7 |
8 | const BrandDropdown = ({ brand, setbrand }) => {
9 | const { brandData, status } = useSelector((state) => state.filterShoes);
10 | const [isOpen, setIsOpen] = useState(false);
11 | return (
12 |
13 | setIsOpen(!isOpen)}
15 | className='dropdown-btn w-full text-left'
16 | >
17 |
18 |
19 |
{brand}
20 |
Select your brand
21 |
22 | {isOpen ? (
23 |
24 | ) : (
25 |
26 | )}
27 |
28 |
29 |
30 | {status !== 'idle' ? :
31 | brandData?.map((brand, index) => {
32 | return (
33 | setbrand(brand)}
36 | key={index}
37 | className='cursor-pointer hover:text-rose-700 transition'
38 | >
39 | {brand}
40 |
41 | );
42 | })}
43 |
44 |
45 | );
46 | };
47 |
48 | export default BrandDropdown;
49 |
--------------------------------------------------------------------------------
/src/components/filterProduct/Category.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { RiArrowDownSLine, RiArrowUpSLine } from 'react-icons/ri';
3 | import { TbDiscount } from 'react-icons/tb';
4 | import { Menu } from '@headlessui/react';
5 | import { useSelector } from 'react-redux';
6 | import { LoadingBtn } from '../../toastify';
7 |
8 | const ProductDropdown = ({ Category, setCategory }) => {
9 | const { categoryData, status } = useSelector((state) => state.filterShoes);
10 | const [isOpen, setIsOpen] = useState(false);
11 | return (
12 |
13 | setIsOpen(!isOpen)}
15 | className='dropdown-btn w-full text-left'
16 | >
17 |
18 |
19 |
20 | {Category}
21 |
22 |
Choose category
23 |
24 | {isOpen ? (
25 |
26 | ) : (
27 |
28 | )}
29 |
30 |
31 |
32 | {status !== 'idle' ? :
33 | categoryData?.map((Category, index) => {
34 | return (
35 | setCategory(Category)}
38 | key={index}
39 | className='cursor-pointer hover:text-rose-700 transition'
40 | >
41 | {Category}
42 |
43 | );
44 | })}
45 |
46 |
47 | );
48 | };
49 |
50 | export default ProductDropdown;
51 |
--------------------------------------------------------------------------------
/src/components/filterProduct/PriceRangeDropdown.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | RiWallet3Line,
4 | RiArrowDownSLine,
5 | RiArrowUpSLine,
6 | } from 'react-icons/ri';
7 | import { Menu } from '@headlessui/react';
8 |
9 | const PriceRangeDropdown = ({ price, setPrice }) => {
10 | const [isOpen, setIsOpen] = useState(false);
11 | const prices = [
12 | {
13 | value: 'All Prices',
14 | },
15 | {
16 | value: '1000 - 5000',
17 | },
18 | {
19 | value: '5000 - 10000',
20 | },
21 | {
22 | value: '10000 - 15000',
23 | },
24 | {
25 | value: '15000 - 20000',
26 | },
27 | ];
28 |
29 | return (
30 |
31 | setIsOpen(!isOpen)}
33 | className='dropdown-btn w-full'
34 | >
35 |
36 |
37 |
{price}
38 |
Choose price range
39 |
40 | {isOpen ? (
41 |
42 | ) : (
43 |
44 | )}
45 |
46 |
47 |
48 | {prices.map((price, index) => {
49 | return (
50 | setPrice(price.value)}
53 | key={index}
54 | className='cursor-pointer hover:text-rose-700 transition'
55 | >
56 | {price.value}
57 |
58 | );
59 | })}
60 |
61 |
62 | );
63 | };
64 |
65 | export default PriceRangeDropdown;
66 |
--------------------------------------------------------------------------------
/src/components/filterProduct/Search.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import BrandDropdown from './BrandDropdown';
3 | import CategoryDropdown from './Category';
4 | import PriceRangeDropdown from './PriceRangeDropdown';
5 | import { RiSearch2Line } from 'react-icons/ri';
6 | import { setBrandValue, setPriceValue, setCategoryValue, getAllFilterData, setPageValue } from '../../statemanagement/slice/filterShoes';
7 | import { useDispatch } from 'react-redux';
8 | import { NotifyInfo, NotifySuccess, NotifyWarning } from '../../toastify';
9 | import Pagination from './pagination';
10 | const Search = ({ brandValue, categoryValue, priceValue, pageValue }) => {
11 | const dispatch = useDispatch();
12 | React.useEffect(() => {
13 | dispatch(getAllFilterData());
14 | }, [dispatch]);
15 |
16 | const [Category, setCategory] = React.useState(categoryValue || 'Category (any)');
17 | const [Price, setPrice] = React.useState(priceValue || 'Price range (any)');
18 | const [Brand, setbrand] = React.useState(brandValue || 'Brand (any)');
19 | const [Page, setPage] = React.useState(pageValue || "Page (any)");
20 | const handleSubmit = (e) => {
21 | e.preventDefault();
22 | if (Page !== "Page (any)" && Page !== "All Pages") {
23 | dispatch(setPageValue(Page));
24 | } else {
25 | dispatch(setPageValue(''));
26 | }
27 | if (Category !== 'Category (any)' && Category !== 'All Categories') {
28 | dispatch(setCategoryValue(Category));
29 | } else {
30 | dispatch(setCategoryValue(''));
31 | }
32 | if (Price !== 'Price range (any)' && Price !== 'All Prices') {
33 | dispatch(setPriceValue(Price));
34 | } else {
35 | dispatch(setPriceValue(''));
36 | }
37 | if (Brand !== 'Brand (any)' && Brand !== 'All Brands') {
38 | dispatch(setBrandValue(Brand));
39 | } else {
40 | dispatch(setBrandValue(''));
41 | }
42 | if (Brand === 'Brand (any)' && Price === 'Price range (any)' && Category === 'Category (any)' && Page === "Page (any)") {
43 | return NotifyWarning('Please select any filter');
44 | }
45 | if (Category === 'All Categories' && Price === 'All Prices' && Brand === 'All Brands' && Page === "All Pages" && brandValue === '' && priceValue === '' && categoryValue === '' && pageValue === '') {
46 | return NotifyWarning('No filter selected');
47 | }
48 | if (Brand === brandValue && Price === priceValue && Category === categoryValue) {
49 | return NotifyInfo(`You have already selected ${Brand} brand, ${Price} price range and ${Category} category`);
50 | }
51 | NotifySuccess('Filter applied successfully');
52 | };
53 | return (
54 |
55 |
56 |
57 |
58 |
59 |
61 |
62 |
63 |
74 |
84 |
85 | );
86 | };
87 |
88 | export default Search;
89 |
--------------------------------------------------------------------------------
/src/components/filterProduct/pagination.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { RiArrowDownSLine, RiArrowUpSLine } from 'react-icons/ri';
3 | import { TbDiscount } from 'react-icons/tb';
4 | import { Menu } from '@headlessui/react';
5 | import { useSelector } from 'react-redux';
6 | import { LoadingBtn } from '../../toastify';
7 |
8 | const Pagination = ({ Page, setPage }) => {
9 | const { pageData, status } = useSelector((state) => state.filterShoes);
10 | const [isOpen, setIsOpen] = useState(false);
11 | return (
12 |
13 | setIsOpen(!isOpen)}
15 | className='dropdown-btn w-full text-left'
16 | >
17 |
18 |
19 |
20 | {Page === 'Page (any)' || Page === "All Pages" ? Page : `Page ${Page}`}
21 |
22 |
Choose Page
23 |
24 | {isOpen ? (
25 |
26 | ) : (
27 |
28 | )}
29 |
30 |
31 |
32 | {status !== 'idle' ? :
33 | pageData?.map((Page, index) => {
34 | return (
35 | setPage(Page)}
38 | key={index}
39 | className='cursor-pointer hover:text-rose-700 transition'
40 | >
41 | {Page}
42 |
43 | );
44 | })}
45 |
46 |
47 | );
48 | };
49 |
50 | export default Pagination;
51 |
--------------------------------------------------------------------------------
/src/components/firebase/UploadImage.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { storage } from "./index";
3 | import { ref, getDownloadURL, uploadBytesResumable } from "firebase/storage";
4 | import { BiImageAdd } from 'react-icons/bi';
5 | const UploadImage = ({ setAddProductData }) => {
6 | const [images, setImages] = React.useState([]);
7 | const [progress, setProgress] = React.useState(0);
8 |
9 | const handleChange = (e) => {
10 | setImages([]);
11 | for (let i = 0; i < e.target.files.length; i++) {
12 | const newImage = e.target.files[i];
13 | newImage["id"] = Math.random();
14 | setImages((prevState) => [...prevState, newImage]);
15 | }
16 | setProgress(0);
17 | };
18 |
19 | const handleUpload = () => {
20 | try {
21 | images.forEach((image) => {
22 | const uploadTask = uploadBytesResumable(ref(storage, `images/${image.name}`), image);
23 | uploadTask.on(
24 | "state_changed",
25 | (snapshot) => {
26 | const progress = Math.round(
27 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100
28 | );
29 | setProgress(progress);
30 | },
31 | (error) => {
32 | console.log(error);
33 | },
34 | () => {
35 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
36 | setAddProductData((prevState) => ({ ...prevState, selectedFile: [...prevState.selectedFile, downloadURL] }));
37 | });
38 | }
39 | );
40 | });
41 | } catch (error) {
42 | console.log('error', error);
43 | alert('Create Firebase Storage Bucket and add it to .env file');
44 | alert('Example: REACT_APP_STORAGE_BUCKET=your-bucket-name');
45 | alert('Watch this video to create Firebase Storage Bucket: https://www.youtube.com/watch?v=0E1MM3tBqRo&list=PLJ3uCOeGaRaKKNzSJKb1RD5-mO9mQ1qKD&index=2');
46 | }
47 | };
48 | return (
49 | <>
50 | Upload Image
51 |
52 |
53 |
54 |
55 | {images.length ? {images.length} image selected : "Upload Image"}
56 | {progress === 0 ? "Upload" : progress === 100 ? "Done" : progress}
59 |
60 |
65 |
66 | >
67 | )
68 | }
69 |
70 | export default UploadImage
--------------------------------------------------------------------------------
/src/components/firebase/index.js:
--------------------------------------------------------------------------------
1 | import { initializeApp } from "firebase/app";
2 | import { getStorage } from "firebase/storage";
3 |
4 | const firebaseConfig = {
5 | apiKey: process.env.REACT_APP_APIKEY,
6 | authDomain: process.env.REACT_APP_AUTHDOMAIN,
7 | projectId: process.env.REACT_APP_PROJECTID,
8 | storageBucket: process.env.REACT_APP_STORAGEBUCKET,
9 | messagingSenderId: process.env.REACT_APP_MESSAGINGSENDERID,
10 | appId: process.env.REACT_APP_APPID
11 | };
12 |
13 | const app = initializeApp(firebaseConfig);
14 |
15 | const storage = getStorage(app);
16 |
17 | export { storage, app };
--------------------------------------------------------------------------------
/src/components/pageNotFound.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import fastLoading from '../assets/fastLoading.gif'
3 | const PageNotFound = () => {
4 | return (
5 |
19 |
450 ? {
20 | color: 'rgb(0,67,77)',
21 | fontSize: '30px',
22 | fontWeight: 'bold',
23 | letterSpacing: '3px',
24 | display: "block",
25 | marginTop: '370px',
26 | zIndex: '1',
27 | } : {
28 | color: 'rgb(0,67,77)',
29 | fontSize: '20px',
30 | fontWeight: 'bold',
31 | letterSpacing: '3px',
32 | display: "block",
33 | marginTop: '370px',
34 | zIndex: '1',
35 | }}>Available in the next video
36 |
37 | );
38 | }
39 |
40 | export default PageNotFound
--------------------------------------------------------------------------------
/src/data.js:
--------------------------------------------------------------------------------
1 | // import house images
2 | import House1 from './assets/img/houses/house1.png';
3 | import House2 from './assets/img/houses/house2.png';
4 | import House3 from './assets/img/houses/house3.png';
5 | import House4 from './assets/img/houses/house4.png';
6 | import House5 from './assets/img/houses/house5.png';
7 | import House6 from './assets/img/houses/house6.png';
8 | import House7 from './assets/img/houses/house7.png';
9 | import House8 from './assets/img/houses/house8.png';
10 | import House9 from './assets/img/houses/house9.png';
11 | import House10 from './assets/img/houses/house10.png';
12 | import House11 from './assets/img/houses/house11.png';
13 | import House12 from './assets/img/houses/house12.png';
14 | // import house large images
15 | import House1Lg from './assets/img/houses/house1lg.png';
16 | import House2Lg from './assets/img/houses/house2lg.png';
17 | import House3Lg from './assets/img/houses/house3lg.png';
18 | import House4Lg from './assets/img/houses/house4lg.png';
19 | import House5Lg from './assets/img/houses/house5lg.png';
20 | import House6Lg from './assets/img/houses/house6lg.png';
21 | import House7Lg from './assets/img/houses/house7lg.png';
22 | import House8Lg from './assets/img/houses/house8lg.png';
23 | import House9Lg from './assets/img/houses/house9lg.png';
24 | import House10Lg from './assets/img/houses/house10lg.png';
25 | import House11Lg from './assets/img/houses/house11lg.png';
26 | import House12Lg from './assets/img/houses/house12lg.png';
27 |
28 | // import apartments images
29 | import Apartment1 from './assets/img/apartments/a1.png';
30 | import Apartment2 from './assets/img/apartments/a2.png';
31 | import Apartment3 from './assets/img/apartments/a3.png';
32 | import Apartment4 from './assets/img/apartments/a4.png';
33 | import Apartment5 from './assets/img/apartments/a5.png';
34 | import Apartment6 from './assets/img/apartments/a6.png';
35 | // import apartments large images
36 | import Apartment1Lg from './assets/img/apartments/a1lg.png';
37 | import Apartment2Lg from './assets/img/apartments/a2lg.png';
38 | import Apartment3Lg from './assets/img/apartments/a3lg.png';
39 | import Apartment4Lg from './assets/img/apartments/a4lg.png';
40 | import Apartment5Lg from './assets/img/apartments/a5lg.png';
41 | import Apartment6Lg from './assets/img/apartments/a6lg.png';
42 |
43 | // import agents images
44 | import Agent1 from './assets/img/agents/agent1.png';
45 | import Agent2 from './assets/img/agents/agent2.png';
46 | import Agent3 from './assets/img/agents/agent3.png';
47 | import Agent4 from './assets/img/agents/agent4.png';
48 | import Agent5 from './assets/img/agents/agent5.png';
49 | import Agent6 from './assets/img/agents/agent6.png';
50 | import Agent7 from './assets/img/agents/agent7.png';
51 | import Agent8 from './assets/img/agents/agent8.png';
52 | import Agent9 from './assets/img/agents/agent9.png';
53 | import Agent10 from './assets/img/agents/agent10.png';
54 | import Agent11 from './assets/img/agents/agent11.png';
55 | import Agent12 from './assets/img/agents/agent12.png';
56 |
57 | export const housesData = [
58 | {
59 | id: 1,
60 | type: 'House',
61 | name: 'House 1',
62 | description:
63 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
64 | image: House1,
65 | imageLg: House1Lg,
66 | country: 'United States',
67 | address: '7240C Argyle St. Lawndale, CA 90260',
68 | bedrooms: '6',
69 | bathrooms: '3',
70 | surface: '4200 sq ft',
71 | year: '2016',
72 | price: '110000',
73 | agent: {
74 | image: Agent1,
75 | name: 'Patricia Tullert',
76 | phone: '0123 456 78910',
77 | },
78 | },
79 | {
80 | id: 2,
81 | type: 'House',
82 | name: 'House 2',
83 | description:
84 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
85 | image: House2,
86 | imageLg: House2Lg,
87 | country: 'Canada',
88 | address: '798 Talbot St. Bridgewater, NJ 08807',
89 | bedrooms: '6',
90 | bathrooms: '3',
91 | surface: '4200 sq ft',
92 | year: '2016',
93 | price: '140000',
94 | agent: {
95 | image: Agent2,
96 | name: 'Daryl Hawker',
97 | phone: '0123 456 78910',
98 | },
99 | },
100 | {
101 | id: 3,
102 | type: 'House',
103 | name: 'House 3',
104 | description:
105 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
106 | image: House3,
107 | imageLg: House3Lg,
108 | country: 'United States',
109 | address: '2 Glen Creek St. Alexandria, VA 22304',
110 | bedrooms: '6',
111 | bathrooms: '3',
112 | surface: '4200 sq ft',
113 | year: '2016',
114 | price: '170000',
115 | agent: {
116 | image: Agent3,
117 | name: 'Amado Smith',
118 | phone: '0123 456 78910',
119 | },
120 | },
121 | {
122 | id: 4,
123 | type: 'House',
124 | name: 'House 4',
125 | description:
126 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
127 | image: House4,
128 | imageLg: House4Lg,
129 | country: 'Canada',
130 | address: '84 Woodland St. Cocoa, FL 32927',
131 | bedrooms: '6',
132 | bathrooms: '3',
133 | surface: '4200 sq ft',
134 | year: '2016',
135 | price: '200000',
136 | agent: {
137 | image: Agent4,
138 | name: 'Kaitlyn Gonzalez',
139 | phone: '0123 456 78910',
140 | },
141 | },
142 | {
143 | id: 5,
144 | type: 'House',
145 | name: 'House 5',
146 | description:
147 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
148 | image: House5,
149 | imageLg: House5Lg,
150 | country: 'United States',
151 | address: '28 Westport Dr. Warminster, PA 18974',
152 | bedrooms: '5',
153 | bathrooms: '3',
154 | surface: '4200 sq ft',
155 | year: '2015',
156 | price: '210000',
157 | agent: {
158 | image: Agent5,
159 | name: 'Grover Robinson',
160 | phone: '0123 456 78910',
161 | },
162 | },
163 | {
164 | id: 6,
165 | type: 'House',
166 | name: 'House 6',
167 | description:
168 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
169 | image: House6,
170 | imageLg: House6Lg,
171 | country: 'Canada',
172 | address: '32 Pawnee Street Butte, MT 59701',
173 | bedrooms: '6',
174 | bathrooms: '3',
175 | surface: '6200 sq ft',
176 | year: '2014',
177 | price: '220000',
178 | agent: {
179 | image: Agent6,
180 | name: 'Karen Sorensen',
181 | phone: '0123 456 78910',
182 | },
183 | },
184 | {
185 | id: 7,
186 | type: 'Apartament',
187 | name: 'Apartament 1',
188 | description:
189 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
190 | image: Apartment1,
191 | imageLg: Apartment1Lg,
192 | country: 'Canada',
193 | address: '32 Pawnee Street Butte, MT 59701',
194 | bedrooms: '2',
195 | bathrooms: '1',
196 | surface: '1200 sq ft',
197 | year: '2012',
198 | price: '20000',
199 | agent: {
200 | image: Agent7,
201 | name: 'Jawhar Shamil Naser',
202 | phone: '0123 456 78910',
203 | },
204 | },
205 | {
206 | id: 8,
207 | type: 'Apartament',
208 | name: 'Apartament 2',
209 | description:
210 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
211 | image: Apartment2,
212 | imageLg: Apartment2Lg,
213 | country: 'United States',
214 | address: '28 Westport Dr. Warminster, PA 18974',
215 | bedrooms: '3',
216 | bathrooms: '1',
217 | surface: '1000 sq ft',
218 | year: '2011',
219 | price: '30000',
220 | agent: {
221 | image: Agent8,
222 | name: 'Juana Douglass',
223 | phone: '0123 456 78910',
224 | },
225 | },
226 | {
227 | id: 9,
228 | type: 'Apartament',
229 | name: 'Apartament 3',
230 | description:
231 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
232 | image: Apartment3,
233 | imageLg: Apartment3Lg,
234 | country: 'United States',
235 | address: '84 Woodland St. Cocoa, FL 32927',
236 | bedrooms: '2',
237 | bathrooms: '1',
238 | surface: '1100 sq ft',
239 | year: '2011',
240 | price: '40000',
241 | agent: {
242 | image: Agent9,
243 | name: 'Jerry Schenck',
244 | phone: '0123 456 78910',
245 | },
246 | },
247 | {
248 | id: 10,
249 | type: 'House',
250 | name: 'House 7',
251 | description:
252 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
253 | image: House7,
254 | imageLg: House7Lg,
255 | country: 'Canada',
256 | address: '7240C Argyle St. Lawndale, CA 90260',
257 | bedrooms: '5',
258 | bathrooms: '3',
259 | surface: '3200 sq ft',
260 | year: '2015',
261 | price: '117000',
262 | agent: {
263 | image: Agent10,
264 | name: 'Vera Levesque',
265 | phone: '0123 456 78910',
266 | },
267 | },
268 | {
269 | id: 11,
270 | type: 'House',
271 | name: 'House 8',
272 | description:
273 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
274 | image: House8,
275 | imageLg: House8Lg,
276 | country: 'Canada',
277 | address: '798 Talbot St. Bridgewater, NJ 08807',
278 | bedrooms: '7',
279 | bathrooms: '2',
280 | surface: '2200 sq ft',
281 | year: '2019',
282 | price: '145000',
283 | agent: {
284 | image: Agent11,
285 | name: 'Sofia Gomes',
286 | phone: '0123 456 78910',
287 | },
288 | },
289 | {
290 | id: 12,
291 | type: 'House',
292 | name: 'House 9',
293 | description:
294 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
295 | image: House9,
296 | imageLg: House9Lg,
297 | country: 'United States',
298 | address: '2 Glen Creek St. Alexandria, VA 22304',
299 | bedrooms: '4',
300 | bathrooms: '4',
301 | surface: '4600 sq ft',
302 | year: '2015',
303 | price: '139000',
304 | agent: {
305 | image: Agent12,
306 | name: 'Raymond Hood',
307 | phone: '0123 456 78910',
308 | },
309 | },
310 | {
311 | id: 13,
312 | type: 'House',
313 | name: 'House 10',
314 | description:
315 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
316 | image: House10,
317 | imageLg: House10Lg,
318 | country: 'Canada',
319 | address: '84 Woodland St. Cocoa, FL 32927',
320 | bedrooms: '5',
321 | bathrooms: '2',
322 | surface: '5200 sq ft',
323 | year: '2014',
324 | price: '180000',
325 | agent: {
326 | image: Agent1,
327 | name: 'Patricia Tullert',
328 | phone: '0123 456 78910',
329 | },
330 | },
331 | {
332 | id: 14,
333 | type: 'House',
334 | name: 'House 11',
335 | description:
336 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
337 | image: House11,
338 | imageLg: House11Lg,
339 | country: 'United States',
340 | address: '28 Westport Dr. Warminster, PA 18974',
341 | bedrooms: '5',
342 | bathrooms: '2',
343 | surface: '3200 sq ft',
344 | year: '2011',
345 | price: '213000',
346 | agent: {
347 | image: Agent2,
348 | name: 'Daryl Hawker',
349 | phone: '0123 456 78910',
350 | },
351 | },
352 | {
353 | id: 15,
354 | type: 'House',
355 | name: 'House 12',
356 | description:
357 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
358 | image: House12,
359 | imageLg: House12Lg,
360 | country: 'Canada',
361 | address: '32 Pawnee Street Butte, MT 59701',
362 | bedrooms: '4',
363 | bathrooms: '3',
364 | surface: '5200 sq ft',
365 | year: '2013',
366 | price: '221000',
367 | agent: {
368 | image: Agent3,
369 | name: 'Amado Smith',
370 | phone: '0123 456 78910',
371 | },
372 | },
373 | {
374 | id: 16,
375 | type: 'Apartament',
376 | name: 'Apartament 16',
377 | description:
378 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
379 | image: Apartment4,
380 | imageLg: Apartment4Lg,
381 | country: 'Canada',
382 | address: '32 Pawnee Street Butte, MT 59701',
383 | bedrooms: '2',
384 | bathrooms: '1',
385 | surface: '1300 sq ft',
386 | year: '2011',
387 | price: '21000',
388 | agent: {
389 | image: Agent4,
390 | name: 'Kaitlyn Gonzalez',
391 | phone: '0123 456 78910',
392 | },
393 | },
394 | {
395 | id: 17,
396 | type: 'Apartament',
397 | name: 'Apartament 17',
398 | description:
399 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
400 | image: Apartment5,
401 | imageLg: Apartment5Lg,
402 | country: 'United States',
403 | address: '28 Westport Dr. Warminster, PA 18974',
404 | bedrooms: '3',
405 | bathrooms: '1',
406 | surface: '1000 sq ft',
407 | year: '2012',
408 | price: '32000',
409 | agent: {
410 | image: Agent5,
411 | name: 'Grover Robinson',
412 | phone: '0123 456 78910',
413 | },
414 | },
415 | {
416 | id: 18,
417 | type: 'Apartament',
418 | name: 'Apartament 18',
419 | description:
420 | 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Amet, illoat. Repudiandae ratione impedit delectus consectetur. Aspernaturvero obcaecati placeat ab distinctio unde ipsam molestias atqueratione delectus blanditiis nemo eius dignissimos doloremque quaealiquid maiores id tempore consequatur, quod pariatur saepe.',
421 | image: Apartment6,
422 | imageLg: Apartment6Lg,
423 | country: 'Canada',
424 | address: '84 Woodland St. Cocoa, FL 32927',
425 | bedrooms: '3',
426 | bathrooms: '1',
427 | surface: '1200 sq ft',
428 | year: '2010',
429 | price: '38000',
430 | agent: {
431 | image: Agent6,
432 | name: 'Karen Sorensen',
433 | phone: '0123 456 78910',
434 | },
435 | },
436 | ];
437 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | [type="text"],
7 | [type="email"],
8 | [type="url"],
9 | [type="password"],
10 | [type="number"],
11 | [type="date"],
12 | [type="datetime-local"],
13 | [type="month"],
14 | [type="search"],
15 | [type="tel"],
16 | [type="time"],
17 | [type="week"],
18 | [multiple],
19 | textarea,
20 | select {
21 | @apply w-full border-[#edd5da] rounded-lg shadow-sm focus:border-[#fe2856] focus:ring-[#fe2856];
22 | }
23 | [type="checkbox"],
24 | [type="radio"] {
25 | @apply border-[#f1d4da] rounded text-[#fe2856] focus:ring-[#fe2856];
26 | }
27 | [type="file"] {
28 | @apply border-[#f1d4da] rounded-lg shadow-sm focus:border-[#fe2856] focus:ring-[#fe2856];
29 | }
30 | body {
31 | @apply font-primary bg-white;
32 | }
33 | .dropdown {
34 | @apply w-full lg:max-w-[296px] cursor-pointer;
35 | }
36 | .dropdown-btn {
37 | @apply flex h-[64px] items-center px-[18px] border rounded-lg;
38 | }
39 | .dropdown-icon-primary {
40 | @apply text-2xl mr-[18px] text-rose-700;
41 | }
42 | .dropdown-icon-secondary {
43 | @apply text-2xl text-rose-700 ml-auto;
44 | }
45 | .dropdown-menu {
46 | @apply px-6 py-8 text-[15px] pl-10 space-y-6 shadow-md bg-white absolute w-full z-[1000] list-none rounded-b-lg max-h-[11rem] overflow-y-auto;
47 | }
48 | .alegreya {
49 | @apply font-secondary;
50 | }
51 | }
52 |
53 | * {
54 | margin: 0px;
55 | padding: 0px;
56 | box-sizing: border-box;
57 | font-family: "Poppins", sans-serif;
58 | /* use poppins or alegreya */
59 | font-family: "Alegreya", serif;
60 | }
61 |
62 | /* scroll bar */
63 | ::-webkit-scrollbar {
64 | width: 5px;
65 | height: 4px;
66 | }
67 | ::-webkit-scrollbar-track {
68 | background-color: #adadad;
69 | border-radius: 8px;
70 | }
71 | ::-webkit-scrollbar-thumb {
72 | background: #fe2856;
73 | border-radius: 8px;
74 | }
75 | ::-webkit-scrollbar-thumb:hover {
76 | background: #fe2856;
77 | }
78 | /* scroll bar */
79 |
80 | /* slider style */
81 | .mySwiper .swiper-button-next:after {
82 | font-size: 1rem;
83 | background-color: #fe2856;
84 | padding: 8px 9px 8px 12px;
85 | border-radius: 50%;
86 | color: white;
87 | }
88 | .mySwiper .swiper-button-next:hover:after {
89 | transform: scale(1.2);
90 | }
91 | .mySwiper .swiper-button-prev:after {
92 | font-size: 1rem;
93 | background-color: #fe2856;
94 | padding: 8px 12px 8px 9px;
95 | border-radius: 50%;
96 | color: white;
97 | }
98 | .mySwiper .swiper-button-prev:hover:after {
99 | transform: scale(1.2);
100 | }
101 | /* slider style */
102 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import { store } from './statemanagement/storage/index'
4 | import { Provider } from 'react-redux'
5 | import { ToastContainer } from "react-toastify";
6 | import "react-toastify/dist/ReactToastify.css";
7 | import './index.css';
8 | import App from './App';
9 | import { BrowserRouter as Router } from 'react-router-dom';
10 |
11 | const root = ReactDOM.createRoot(document.getElementById('root'));
12 | root.render(
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/src/pages/AllProduct.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProductList from '../components/ProductList';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { getAllShoe } from '../statemanagement/slice/ShoeSlice';
5 | import Search from '../components/filterProduct/Search';
6 |
7 | function ProductPage() {
8 | const dispatch = useDispatch();
9 | const { shoeData, loading, error, runningData, loungingData, everydayData } = useSelector((state) => state.shoeDetails);
10 | const { page, limit, sort, brand, category, price } = useSelector((state) => state.filterShoes);
11 | React.useEffect(() => {
12 | dispatch(getAllShoe({ page, limit, sort, brand, category, price }));
13 | }, [dispatch, page, limit, sort, brand, category, price]);
14 | const style = {
15 | textAlign: 'left',
16 | marginLeft: '10px',
17 | };
18 | return (
19 |
23 | );
24 | }
25 |
26 | export default ProductPage;
27 |
--------------------------------------------------------------------------------
/src/pages/Home.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | // import components
4 | import ProductList from '../components/ProductList';
5 | import Banner from '../components/Banner';
6 | import TopProduct from './topProduct';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import { getAllShoe } from '../statemanagement/slice/ShoeSlice';
9 | import Search from '../components/filterProduct/Search';
10 | const Home = () => {
11 | const dispatch = useDispatch();
12 | const { shoeData, loading, error } = useSelector((state) => state.shoeDetails);
13 | const { page, limit, sort, brand, category, price } = useSelector((state) => state.filterShoes);
14 | React.useEffect(() => {
15 | dispatch(getAllShoe({ page, limit, sort, brand, category, price }));
16 | }, [dispatch, page, limit, sort, brand, category, price]);
17 | return (
18 |
24 | );
25 | };
26 |
27 | export default Home;
28 |
--------------------------------------------------------------------------------
/src/pages/ProductDetails.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | // useParams
3 | import { useParams } from 'react-router-dom';
4 | // import link
5 | import { Link } from 'react-router-dom';
6 | import { useDispatch, useSelector } from 'react-redux';
7 | import { getShoeById, getShoeByIdOnPageLoad } from '../statemanagement/slice/ShoeSlice/index';
8 | import { decodeToken } from 'react-jwt';
9 | import { addCarts, CartisOpen, checkoutAct } from '../statemanagement/slice/cartSlice';
10 | import { LoadingSinglePage } from '../toastify';
11 |
12 | const ProductDetails = () => {
13 | const { id } = useParams();
14 | const token = localStorage.getItem('authenticate');
15 | const decodeData = decodeToken(token);
16 | const dispatch = useDispatch();
17 | const addToCart = async ({ product, shoeId }) => {
18 | await dispatch(addCarts({ product, shoeId }));
19 | await dispatch(CartisOpen(true));
20 | }
21 | useEffect(() => {
22 | dispatch(getShoeById(id));
23 | dispatch(getShoeByIdOnPageLoad(id));
24 | }, [id, dispatch]);
25 | async function CheckoutBtn(data) {
26 | await dispatch(addCarts({ product: data, shoeId: data?._id, notification: false }));
27 | await dispatch(checkoutAct(data?.price))
28 | }
29 | const { singleShoeData, loading } = useSelector((state) => state.shoeDetails);
30 | if (loading || !singleShoeData) return
31 | return (
32 |
33 |
34 | {singleShoeData?.title}
35 |
36 |
37 | {singleShoeData?.shoeFor?.map((shoeF, index) => {
38 | return (
39 |
40 | {shoeF}
41 | );
42 | }
43 | ).splice(0, 1)}
44 |
45 |
46 | Available: {singleShoeData?.quantity}
47 |
48 |
49 | Rs.{singleShoeData?.price}
50 |
51 |
52 |
53 | Rs.{singleShoeData?.price}
54 |
55 |
56 |
57 |
58 | {singleShoeData?.category?.map((category, index) => {
59 | return (
60 |
61 | {category}
62 | );
63 | }
64 | ).splice(0, 1)}
65 |
66 |
67 | Available: {singleShoeData?.quantity}
68 |
69 |
70 |
71 |
72 |
73 |
{singleShoeData?.description}
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | {singleShoeData?.title}
83 |
84 |
85 | Order Now
86 |
87 |
88 |
89 |
115 |
116 | CheckoutBtn(singleShoeData)}
119 | >
120 | Order Now
121 |
122 | addToCart(
123 | { product: singleShoeData, shoeId: id }
124 | )}>
125 | Add to Cart
126 |
127 |
128 |
129 |
130 |
131 | );
132 | };
133 |
134 | export default ProductDetails;
135 |
--------------------------------------------------------------------------------
/src/pages/UserEmailVerification.js:
--------------------------------------------------------------------------------
1 | // react
2 | import React from "react";
3 | import { useDispatch } from "react-redux";
4 | import { useNavigate } from "react-router-dom";
5 | import { useParams } from "react-router-dom";
6 | import { VerifyaUser } from "../statemanagement/slice/AuthenticationSlice";
7 |
8 | const UserVerification = () => {
9 | const params = useParams();
10 | const navigate = useNavigate();
11 | const dispatch = useDispatch();
12 | const [message, setMessage] = React.useState('');
13 | const verify = () => {
14 | dispatch(VerifyaUser({ params, navigate, setMessage }));
15 | };
16 |
17 | return (
18 | // user verification page
19 |
20 |
21 |
User Verification
22 |
Click on the button to verify your account
23 |
24 |
25 |
Verify
40 |
{
41 | message.includes('//') ?
42 | message.split('//')[1]
43 | : message
44 | }
45 |
46 |
47 | );
48 | }
49 | export default UserVerification;
--------------------------------------------------------------------------------
/src/pages/topProduct.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BsEyeFill } from 'react-icons/bs';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { getTopShoe } from '../statemanagement/slice/ShoeSlice';
5 | import { Swiper, SwiperSlide } from "swiper/react";
6 | import "swiper/css";
7 | import "swiper/css/pagination";
8 | import "swiper/css/navigation";
9 | import { Pagination, Navigation } from "swiper";
10 | import { Link } from 'react-router-dom';
11 | const Carousel = () => {
12 | const dispatch = useDispatch();
13 | const { topShoeData, loading } = useSelector((state) => state.shoeDetails);
14 | React.useEffect(() => {
15 | dispatch(getTopShoe());
16 | }, [dispatch]);
17 | const windowWidth = window.innerWidth <= 768 ? 1 : window.innerWidth <= 1024 ? 2 : window.innerWidth <= 1280 ? 3 : 4;
18 | if (loading) return
;
19 | return (
20 |
21 |
22 |
23 |
Top Sales
26 |
Add our products to weekly lineup
27 |
28 |
29 |
46 |
47 | {topShoeData?.map((item, index) => {
48 | return (
49 |
50 |
51 |
53 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {item?.shoeFor?.map((shoeF, index) => {
63 | const IndexStyle = index === 0 ? 'bg-yellow-400' : 'bg-rose-600';
64 | return (
65 | {shoeF}
66 | );
67 | }
68 | ).splice(0, 2)}
69 |
70 |
71 |
72 | {item?.title.split(" ").slice(0, 6).join(" ")}
73 |
74 |
75 | Rs. {item?.price}
76 |
77 |
78 |
79 |
80 | );
81 | })}
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default Carousel;
89 |
--------------------------------------------------------------------------------
/src/pages/wishlist.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ProductList from '../components/ProductList';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import Search from '../components/filterProduct/Search';
5 | import { getAllWishList } from '../statemanagement/slice/WishList';
6 | const Wishlist = () => {
7 | const dispatch = useDispatch();
8 | const { wishListData, loading, error } = useSelector((state) => state.wishList);
9 | const { page, limit, sort, brand, category, price } = useSelector((state) => state.filterShoes);
10 | React.useEffect(() => {
11 | dispatch(getAllWishList({ page, limit, sort, brand, category, price }));
12 | }, [dispatch, page, limit, sort, brand, category, price]);
13 | return (
14 |
18 | );
19 | };
20 |
21 | export default Wishlist;
22 |
--------------------------------------------------------------------------------
/src/statemanagement/api/AuthenticationApi.js:
--------------------------------------------------------------------------------
1 | import API from './index';
2 |
3 | // Register a new user
4 | export const registeraUser = (registerData) => API.post('/user/signup', registerData);
5 | // Login a user
6 | export const loginaUser = (loginData) => API.post('/user/signin', loginData);
7 | // verify a user
8 | export const verifyUser = (verifyData) => API.get(`/user/${verifyData.userId}/verify/${verifyData.verifyId}`);
9 | // Logout a user
10 | export const logoutUser = () => API.get('/user/signout');
--------------------------------------------------------------------------------
/src/statemanagement/api/ShoeApi.js:
--------------------------------------------------------------------------------
1 | import API from "./index";
2 |
3 | export const GetAllShoeAPI = ({ page, limit, sort, brand, category, price }) => API.get("/shoesPage", {
4 | params: {
5 | page,
6 | limit,
7 | sort,
8 | brand,
9 | category,
10 | price
11 | }
12 | });
13 | export const GetTopShoeAPI = () => API.get("/shoesPage/top");
14 | export const GetShoeByIdAPI = (id) => API.get(`/shoesPage/${id}`);
15 | export const CreateShoeAPI = (AddProductData) => API.post("/shoesPage", AddProductData);
16 | export const GetFilterData = () => API.get("/shoesPage/filter");
--------------------------------------------------------------------------------
/src/statemanagement/api/WishListApi.js:
--------------------------------------------------------------------------------
1 | import API from "./index";
2 |
3 | export const GetAllWishListAPI = ({ page, limit, sort, brand, category, price }) => API.get("/user/wishlist", {
4 | params: {
5 | page,
6 | limit,
7 | sort,
8 | brand,
9 | category,
10 | price
11 | }
12 | });
13 | export const AddWishListAPI = (id) => API.post(`/user/wishlist/${id}`);
14 | export const DeleteWishListAPI = (id) => API.delete(`/user/wishlist/${id}`);
--------------------------------------------------------------------------------
/src/statemanagement/api/cartApi.js:
--------------------------------------------------------------------------------
1 | import API from "./index";
2 |
3 | export const GetAllCartAPI = () => API.get("/user/cart");
4 | export const AddCartAPI = (id) => API.post(`/user/cart/${id}`);
5 | export const DeleteCartAPI = (id) => API.delete(`/user/cart/${id}`);
6 | export const CartQuantityAPI = (id, status) => API.post(`/user/cart/${id}/quantity`, { status });
7 | export const checkoutAPI = (total) => API.post("/user/checkout", { total });
--------------------------------------------------------------------------------
/src/statemanagement/api/index.js:
--------------------------------------------------------------------------------
1 | // fetch data from api
2 | import axios from 'axios'
3 | let baseURL;
4 | // check if not production
5 | if (process.env.NODE_ENV !== 'production') {
6 | baseURL = process.env.REACT_APP_BASE_URL_LOCAL;
7 | } else {
8 | baseURL = process.env.REACT_APP_BASE_URL;
9 | }
10 | axios.defaults.withCredentials = true;
11 | const API = axios.create({ baseURL });
12 | API.interceptors.request.use(req => {
13 | const token = localStorage.getItem('authenticate');
14 | req.headers.Authorization = `Bearer ${token}`;
15 | return req;
16 | }, error => {
17 | return Promise.reject(error.message);
18 | }
19 | );
20 | export default API;
--------------------------------------------------------------------------------
/src/statemanagement/slice/AuthenticationSlice/index.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../api/AuthenticationApi';
2 | import { createAsyncThunk } from '@reduxjs/toolkit'
3 | import { NotifyError, NotifySuccess, NotifyWarning } from '../../../toastify';
4 |
5 | export const registeraUser = createAsyncThunk('User/registeraUser', async ({ authData, navigate, closeModal, closeModalDropDown }, { rejectWithValue }) => {
6 | try {
7 | const { data: { message } } = await api.registeraUser(authData);
8 | closeModal();
9 | window.innerWidth < 768 && closeModalDropDown();
10 | NotifySuccess(message);
11 | navigate("/");
12 | return;
13 | } catch (error) {
14 | if (error?.response?.status >= 300 && error?.response?.status <= 500) {
15 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
16 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
17 | } else {
18 | NotifyError(error.message)
19 | return rejectWithValue(error.message)
20 | }
21 | }
22 | }
23 | );
24 |
25 | export const loginaUser = createAsyncThunk('User/loginaUser', async ({ authData, navigate, closeModal, closeModalDropDown }, { rejectWithValue }) => {
26 | try {
27 | const { data: { message, token } } = await api.loginaUser(authData);
28 | // get cookie from server
29 | const cookie = document.cookies;
30 | console.log(cookie, "cookie");
31 | closeModal();
32 | window.innerWidth < 768 && closeModalDropDown();
33 | if (token) {
34 | localStorage.setItem('authenticate', token);
35 | }
36 | NotifySuccess(message);
37 | navigate("/");
38 | return;
39 | } catch (error) {
40 | if (error?.response?.status >= 300 && error?.response?.status <= 500) {
41 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
42 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
43 | } else {
44 | NotifyError(error.message)
45 | return rejectWithValue(error.message)
46 | }
47 | }
48 | }
49 | );
50 |
51 | export const VerifyaUser = createAsyncThunk('User/VerifyUser', async ({ params, navigate, setMessage }, { rejectWithValue }) => {
52 | try {
53 | const { data: { message, token } } = await api.verifyUser(params);
54 | setMessage(message);
55 | if (token) {
56 | localStorage.setItem('authenticate', token)
57 | }
58 | NotifySuccess(message);
59 | setTimeout(() => {
60 | navigate('/');
61 | }, 3000);
62 | return;
63 | } catch (error) {
64 | if (error?.response?.status >= 300 && error?.response?.status <= 500) {
65 | setMessage(error?.response?.data?.message || "Error please reload page")
66 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
67 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
68 | } else {
69 | setMessage(error.message)
70 | NotifyError(error.message)
71 | return rejectWithValue(error.message)
72 | }
73 | }
74 | }
75 | );
76 |
77 | export const logoutUser = createAsyncThunk('User/logoutUser', async ({ navigate }) => {
78 | try {
79 | const { data: { message } } = await api.logoutUser();
80 | NotifySuccess(message);
81 | localStorage.clear();
82 | navigate('/');
83 | return;
84 | } catch (error) {
85 | console.log(error);
86 | }
87 | })
88 |
--------------------------------------------------------------------------------
/src/statemanagement/slice/ShoeSlice/Shoe.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { initialState, getAllShoe, getShoeById, getShoeByIdOnPageLoad, getTopShoe } from "./index";
3 |
4 | const shoeSlice = createSlice({
5 | name: "Shoe",
6 | initialState,
7 | reducers: {},
8 | extraReducers: {
9 | [getAllShoe.pending]: (state) => {
10 | state.loading = true;
11 | }
12 | , [getAllShoe.fulfilled]: (state, action) => {
13 | state.loading = false;
14 | state.shoeData = action.payload.data;
15 | state.runningData = action.payload.runnning;
16 | state.loungingData = action.payload.lounging;
17 | state.everydayData = action.payload.everyday;
18 | }
19 | , [getAllShoe.rejected]: (state, action) => {
20 | state.loading = false;
21 | state.error = true;
22 | }
23 | , [getTopShoe.pending]: (state) => {
24 | state.loading = true;
25 | }
26 | , [getTopShoe.fulfilled]: (state, action) => {
27 | state.loading = false;
28 | state.topShoeData = action.payload;
29 | }
30 | , [getTopShoe.rejected]: (state, action) => {
31 | state.loading = false;
32 | state.error = true;
33 | }
34 | , [getShoeByIdOnPageLoad.pending]: (state) => {
35 | state.loading = true;
36 | }
37 | , [getShoeByIdOnPageLoad.fulfilled]: (state, action) => {
38 | state.loading = false;
39 | state.singleShoeData = action.payload;
40 | }
41 | , [getShoeByIdOnPageLoad.rejected]: (state, action) => {
42 | state.loading = false;
43 | state.error = action.payload;
44 | }
45 | , [getShoeById.pending]: (state) => {
46 | state.loading = true;
47 | }
48 | , [getShoeById.fulfilled]: (state, action) => {
49 | state.loading = false;
50 | state.singleShoeData = state.shoeData.filter((item) => item._id === action.payload)[0];
51 | }
52 | , [getShoeById.rejected]: (state, action) => {
53 | state.loading = false;
54 | state.error = action.payload;
55 | }
56 | }
57 | });
58 |
59 | export const shoeReducer = shoeSlice.reducer;
60 |
61 |
--------------------------------------------------------------------------------
/src/statemanagement/slice/ShoeSlice/index.js:
--------------------------------------------------------------------------------
1 | import * as api from '../../api/ShoeApi';
2 | import { createAsyncThunk } from '@reduxjs/toolkit'
3 | import { NotifyError, NotifySuccess, NotifyWarning } from '../../../toastify';
4 | export const initialState = {
5 | error: null,
6 | shoeData: [],
7 | loungingData: [],
8 | everydayData: [],
9 | runningData: [],
10 | singleShoeData: '',
11 | topShoeData: [],
12 | loading: false
13 | }
14 | export const getAllShoe = createAsyncThunk('Shoe/getAllShoe', async ({ page, limit, sort, brand, category, price }, { rejectWithValue }) => {
15 | try {
16 | const { data: { data, runnning, lounging, everyday } } = await api.GetAllShoeAPI({ page, limit, sort, brand, category, price });
17 | return { data, runnning, lounging, everyday };
18 | } catch (error) {
19 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
20 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
21 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
22 | } else {
23 | NotifyError(error.message)
24 | return rejectWithValue(error.message)
25 | }
26 | }
27 | }
28 | );
29 |
30 | export const getTopShoe = createAsyncThunk('Shoe/getTopShoe', async (rejectWithValue) => {
31 | try {
32 | const { data: { data } } = await api.GetTopShoeAPI();
33 | return data;
34 | } catch (error) {
35 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
36 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
37 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
38 | } else {
39 | NotifyError(error.message)
40 | return rejectWithValue(error.message)
41 | }
42 | }
43 | }
44 | );
45 |
46 | export const getShoeById = createAsyncThunk('Shoe/getShoeById', async (shoeId) => {
47 | return shoeId;
48 | });
49 | export const getShoeByIdOnPageLoad = createAsyncThunk('Shoe/getShoeByIdOnPageLoad', async (shoeId, { rejectWithValue }) => {
50 | try {
51 | const { data: { data, message } } = await api.GetShoeByIdAPI(shoeId);
52 | NotifySuccess(message);
53 | return data;
54 | } catch (error) {
55 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
56 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
57 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
58 | } else {
59 | NotifyError(error.message)
60 | return rejectWithValue(error.message)
61 | }
62 | }
63 | }
64 | );
65 |
66 | export const createShoe = createAsyncThunk('Shoe/createShoe', async ({ closeModal
67 | , AddProductData }, { rejectWithValue }) => {
68 | try {
69 | const { data: { message } } = await api.CreateShoeAPI(AddProductData);
70 | closeModal();
71 | NotifySuccess(message);
72 | return;
73 | } catch (error) {
74 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
75 | NotifyWarning(error?.response?.data?.message || "Error please reload page")
76 | return rejectWithValue(error?.response?.data?.message || "Error please reload page");
77 | } else {
78 | NotifyError(error.message)
79 | return rejectWithValue(error.message)
80 | }
81 | }
82 | }
83 | );
84 |
--------------------------------------------------------------------------------
/src/statemanagement/slice/WishList/index.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { createAsyncThunk } from '@reduxjs/toolkit'
3 | import { NotifyError, NotifySuccess, NotifyWarning } from "../../../toastify";
4 | import * as api from '../../api/WishListApi';
5 | import { decodeToken } from "react-jwt";
6 | export const initialState = {
7 | error: null,
8 | wishListIDs: localStorage.getItem('authenticate') ? decodeToken(localStorage.getItem('authenticate')).wishlist ? decodeToken(localStorage.getItem('authenticate')).wishlist : [] : [],
9 | wishListData: [],
10 | loading: false
11 | }
12 |
13 | export const getAllWishList = createAsyncThunk('WishList/getAllWishList', async ({ page, limit, sort, brand, category, price }, { rejectWithValue }) => {
14 | try {
15 | const { data: { data, token } } = await api.GetAllWishListAPI({ page, limit, sort, brand, category, price });
16 | localStorage.setItem('authenticate', token)
17 | return data;
18 | } catch (error) {
19 | if (error.response.status >= 400 && error.response.status <= 500) {
20 | NotifyWarning(error.response.data.message)
21 | return rejectWithValue(error.response.data.message);
22 | } else {
23 | NotifyError(error.message)
24 | return rejectWithValue(error.message)
25 | }
26 | }
27 | }
28 | );
29 |
30 | export const addWishList = createAsyncThunk('WishList/addWishList', async ({ shoeId, product }, { rejectWithValue }) => {
31 | try {
32 | const { data: { message, data, token } } = await api.AddWishListAPI(shoeId);
33 | localStorage.setItem('authenticate', token)
34 | NotifySuccess(message);
35 | return { data, product };
36 | } catch (error) {
37 | if (error.response.status >= 400 && error.response.status <= 500) {
38 | NotifyWarning(error.response.data.message)
39 | return rejectWithValue(error.response.data.message);
40 | } else {
41 | NotifyError(error.message)
42 | return rejectWithValue(error.message)
43 | }
44 | }
45 | }
46 | );
47 |
48 | export const deleteWishList = createAsyncThunk('WishList/deleteWishList', async (shoeId, { rejectWithValue }) => {
49 | try {
50 | const { data: { message, data, token } } = await api.DeleteWishListAPI(shoeId);
51 | localStorage.setItem('authenticate', token)
52 | NotifyWarning(message);
53 | return { data, shoeId };
54 | } catch (error) {
55 | if (error.response.status >= 400 && error.response.status <= 500) {
56 | NotifyWarning(error.response.data.message)
57 | return rejectWithValue(error.response.data.message);
58 | } else {
59 | NotifyError(error.message)
60 | return rejectWithValue(error.message)
61 | }
62 | }
63 | }
64 | );
65 |
66 | const wishListSlice = createSlice({
67 | name: "WishList",
68 | initialState,
69 | reducers: {},
70 | extraReducers: {
71 | [getAllWishList.pending]: (state) => {
72 | state.loading = true
73 | },
74 | [getAllWishList.fulfilled]: (state, action) => {
75 | state.loading = false
76 | state.wishListData = action.payload
77 | },
78 | [getAllWishList.rejected]: (state, action) => {
79 | state.loading = false
80 | state.error = true
81 | },
82 | [addWishList.pending]: (state) => {
83 | state.loading = true
84 | },
85 | [addWishList.fulfilled]: (state, action) => {
86 | state.loading = false
87 | state.wishListIDs = action.payload.data
88 | state.wishListData = [...state.wishListData, action.payload.product]
89 | },
90 | [addWishList.rejected]: (state, action) => {
91 | state.loading = false
92 | state.error = action.payload
93 | },
94 | [deleteWishList.pending]: (state) => {
95 | state.loading = true
96 | },
97 | [deleteWishList.fulfilled]: (state, action) => {
98 | state.loading = false
99 | state.wishListIDs = action.payload.data
100 | state.wishListData = state.wishListData.filter((item) => item._id !== action.payload.shoeId)
101 | },
102 | [deleteWishList.rejected]: (state, action) => {
103 | state.loading = false
104 | state.error = action.payload
105 | }
106 | }
107 | });
108 |
109 | export const wishListReducer = wishListSlice.reducer;
--------------------------------------------------------------------------------
/src/statemanagement/slice/cartSlice/index.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import { decodeToken } from "react-jwt";
3 | import { NotifyError, NotifySuccess, NotifyWarning } from "../../../toastify";
4 | import * as api from '../../api/cartApi';
5 |
6 | const Status = Object.freeze({
7 | IDLE: 'idle',
8 | LOADING: 'loading',
9 | FAILED: 'failed',
10 | Increment: 'increment',
11 | });
12 |
13 | export const initialState = {
14 | cartIds: localStorage.getItem('authenticate') ? decodeToken(localStorage.getItem('authenticate')).cart ? decodeToken(localStorage.getItem('authenticate')).cart : [] : [],
15 | cartData: [],
16 | status: Status.IDLE,
17 | isOpenCart: false,
18 | }
19 |
20 | export const getCarts = () => async (dispatch) => {
21 | dispatch(setStatus(Status.LOADING));
22 | try {
23 | const { data: { data, token } } = await api.GetAllCartAPI();
24 | dispatch(getAllCartData(data));
25 | localStorage.setItem('authenticate', token)
26 | dispatch(setStatus(Status.IDLE));
27 | } catch (error) {
28 | if (error.response.status >= 400 && error.response.status <= 500) {
29 | NotifyWarning(error.response.data.message)
30 | } else {
31 | NotifyError(error.message)
32 | }
33 | }
34 | }
35 |
36 | export const addCarts = ({ product, shoeId, notification }) => async (dispatch) => {
37 | dispatch(setStatus(Status.LOADING));
38 | try {
39 | const { data: { data, token, message } } = await api.AddCartAPI(shoeId);
40 | localStorage.setItem('authenticate', token)
41 | dispatch(addCartData(product));
42 | dispatch(addCartIds(data));
43 | dispatch(isOpenCart(true));
44 | if (notification !== false) {
45 | NotifySuccess(message);
46 | }
47 | dispatch(setStatus(Status.IDLE));
48 | } catch (error) {
49 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
50 | if (notification !== false) {
51 | NotifyWarning(error?.response?.data?.message || 'Something went wrong')
52 | }
53 | return dispatch(setStatus(Status.FAILED));
54 | } else {
55 | NotifyError(error?.message)
56 | return dispatch(setStatus(Status.FAILED));
57 | }
58 | }
59 | }
60 |
61 | export const CartisOpen = (open) => async (dispatch) => {
62 | dispatch(isOpenCart(open));
63 | }
64 |
65 | export const deleteCarts = (id) => async (dispatch) => {
66 | dispatch(setStatus(Status.LOADING));
67 | try {
68 | const { data: { message, data, token } } = await api.DeleteCartAPI(id);
69 | localStorage.setItem('authenticate', token)
70 | dispatch(deleteCartData(id));
71 | dispatch(addCartIds(data));
72 | NotifySuccess(message);
73 | dispatch(setStatus(Status.IDLE));
74 | } catch (error) {
75 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
76 | NotifyWarning(error?.response?.data?.message || 'Something went wrong')
77 | return dispatch(setStatus(Status.FAILED));
78 | } else {
79 | NotifyError(error?.message)
80 | return dispatch(setStatus(Status.FAILED));
81 | }
82 | }
83 | }
84 |
85 | export const cartQuantity = ({ status, shoeId }) => async (dispatch) => {
86 | dispatch(setStatus(Status.Increment));
87 | try {
88 | const { data: { message, token, data } } = await api.CartQuantityAPI(shoeId, status);
89 | dispatch(addCartIds(data));
90 | localStorage.setItem('authenticate', token)
91 | NotifySuccess(message);
92 | dispatch(setStatus(Status.IDLE));
93 | } catch (error) {
94 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
95 | NotifyWarning(error?.response?.data?.message || 'Something went wrong')
96 | return dispatch(setStatus(Status.FAILED));
97 | } else {
98 | NotifyError(error?.message)
99 | return dispatch(setStatus(Status.FAILED));
100 | }
101 | }
102 | }
103 |
104 | export const checkoutAct = (total) => async (dispatch) => {
105 | dispatch(setStatus(Status.LOADING));
106 | try {
107 | const { data: { message, token } } = await api.checkoutAPI(total);
108 | localStorage.setItem('authenticate', token)
109 | dispatch(checkoutCart());
110 | NotifySuccess(message);
111 | dispatch(setStatus(Status.IDLE));
112 | } catch (error) {
113 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
114 | NotifyWarning(error?.response?.data?.message || 'Something went wrong')
115 | return dispatch(setStatus(Status.FAILED));
116 | } else {
117 | NotifyError(error?.message)
118 | return dispatch(setStatus(Status.FAILED));
119 | }
120 | }
121 | }
122 |
123 | const cartSlice = createSlice({
124 | name: "Cart",
125 | initialState,
126 | reducers: {
127 | setStatus: (state, action) => {
128 | state.status = action.payload;
129 | },
130 | getAllCartData: (state, action) => {
131 | state.cartData = action.payload;
132 | },
133 | addCartData: (state, action) => {
134 | state.cartData = [...state.cartData, action.payload];
135 | },
136 | addCartIds: (state, action) => {
137 | state.cartIds = action.payload
138 | },
139 | deleteCartData: (state, action) => {
140 | state.cartData = state.cartData.filter((item) => item._id !== action.payload);
141 | },
142 | checkoutCart: (state) => {
143 | state.cartData = [];
144 | state.cartIds = [];
145 | },
146 | isOpenCart: (state, action) => {
147 | state.isOpenCart = action.payload;
148 | },
149 | },
150 | });
151 |
152 | export const { setStatus, addCartData, addCartIds, getAllCartData, deleteCartData, checkoutCart, isOpenCart } = cartSlice.actions;
153 | export const cartReducer = cartSlice.reducer;
--------------------------------------------------------------------------------
/src/statemanagement/slice/filterShoes.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 | import { NotifyError, NotifyWarning } from '../../toastify';
3 | import * as api from '../api/ShoeApi';
4 |
5 | const Status = Object.freeze({
6 | IDLE: 'idle',
7 | LOADING: 'loading',
8 | FAILED: 'failed'
9 | });
10 |
11 | const initialState = {
12 | page: 1,
13 | limit: 8,
14 | sort: 'createdAt',
15 | brand: '',
16 | category: '',
17 | price: '',
18 | brandData: [],
19 | categoryData: [],
20 | pageData: [],
21 | status: Status.IDLE,
22 | }
23 |
24 | export const getAllFilterData = () => async (dispatch, getState) => {
25 | dispatch(setStatus(Status.LOADING));
26 | try {
27 | const { data: { data } } = await api.GetFilterData();
28 | dispatch(setBrandData(data?.brand));
29 | dispatch(setCategoryData(data?.category));
30 | dispatch(setPageData(data?.pageNumbers));
31 | dispatch(setStatus(Status.IDLE));
32 | } catch (error) {
33 | if (error?.response?.status >= 400 && error?.response?.status <= 500) {
34 | NotifyWarning(error?.response?.data?.message || 'Something went wrong')
35 | return dispatch(setStatus(Status.FAILED));
36 | } else {
37 | NotifyError(error?.message)
38 | return dispatch(setStatus(Status.FAILED));
39 | }
40 | }
41 | }
42 |
43 | export const filterShoes = createSlice({
44 | name: 'filterShoes',
45 | initialState,
46 | reducers: {
47 | setPage: (state, action) => {
48 | state.page = action.payload;
49 | },
50 | setLimit: (state, action) => {
51 | state.limit = action.payload;
52 | },
53 | setSort: (state, action) => {
54 | state.sort = action.payload;
55 | },
56 | setBrandValue: (state, action) => {
57 | state.brand = action.payload;
58 | },
59 | setCategoryValue: (state, action) => {
60 | state.category = action.payload;
61 | },
62 | setPriceValue: (state, action) => {
63 | state.price = action.payload;
64 | },
65 | setPageValue: (state, action) => {
66 | state.page = action.payload;
67 | },
68 | setBrandData: (state, action) => {
69 | state.brandData = ['All Brands', ...action.payload];
70 | },
71 | setCategoryData: (state, action) => {
72 | state.categoryData = ['All Categories', ...action.payload];
73 | },
74 | setPageData: (state, action) => {
75 | state.pageData = ["All Pages", ...action.payload];
76 | },
77 | setStatus: (state, action) => {
78 | state.status = action.payload;
79 | },
80 | },
81 | });
82 |
83 | export const { setPage, setLimit, setSort, setBrandValue, setCategoryValue, setPriceValue, setBrandData, setCategoryData, setPageData, setPageValue, setStatus } = filterShoes.actions;
84 | export const filterReducer = filterShoes.reducer;
--------------------------------------------------------------------------------
/src/statemanagement/storage/index.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit'
2 | import { shoeReducer } from '../slice/ShoeSlice/Shoe'
3 | import { wishListReducer } from '../slice/WishList'
4 | import { cartReducer } from '../slice/cartSlice'
5 | import { filterReducer } from '../slice/filterShoes'
6 | export const store = configureStore({
7 | reducer: {
8 | shoeDetails: shoeReducer,
9 | wishList: wishListReducer,
10 | filterShoes: filterReducer,
11 | cart: cartReducer
12 | }
13 | })
--------------------------------------------------------------------------------
/src/toastify.js:
--------------------------------------------------------------------------------
1 | import { toast } from "react-toastify";
2 |
3 | export const NotifySuccess = message => {
4 | toast.success(message, {
5 | hideProgressBar: true,
6 | closeOnClick: true,
7 | })
8 | }
9 |
10 | export const NotifyError = message => {
11 | toast.error(`${message}`);
12 | };
13 |
14 | export const NotifyWarning = message => {
15 | toast.warning(`${message}`);
16 | };
17 |
18 | export const NotifyInfo = message => {
19 | toast.info(`${message}`);
20 | };
21 |
22 | export const LoadingCircle = () => {
23 | return (
24 |
30 |
31 |
33 |
34 |
35 | )
36 | }
37 |
38 | export const LoadingBtn = ({ color, width }) => {
39 | return (
40 |
42 |
43 |
45 |
46 |
47 | )
48 | }
49 |
50 | export const LoadingCard = () => {
51 | return (
52 |
53 | {[1, 2, 3, 4].map((index) => (
54 |
55 |
57 |
58 | {["", ""].map((shoeF, index) => {
59 | const IndexStyle = index === 0 ? 'bg-yellow-100' : 'bg-rose-100';
60 | return (
61 | {shoeF}
62 | );
63 | }
64 | ).splice(0, 2)}
65 |
66 |
67 |
68 | ))}
69 |
70 | )
71 | }
72 |
73 | export const LoadingSinglePage = () => {
74 | return (
75 |
80 | )
81 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ['./src/**/*.{js,jsx,ts,tsx}'],
3 | theme: {
4 | fontFamily: {
5 | primary: 'Poppins',
6 | secondary: 'Alegreya',
7 | },
8 | container: {
9 | padding: {
10 | DEFAULT: '1rem',
11 | lg: '2rem',
12 | },
13 | },
14 | screens: {
15 | sm: '640px',
16 | md: '768px',
17 | lg: '1024px',
18 | xl: '1234px',
19 | },
20 | extend: {
21 | colors: {
22 | primary: '#101828',
23 | secondary: '#7F56D9',
24 | },
25 | boxShadow: {
26 | 1: '0px 4px 30px rgba(0, 0, 0, 0.08)',
27 | },
28 | },
29 | keyframes: {
30 | shimmer: {
31 | '100%': {
32 | transform: 'translateX(100%)',
33 | },
34 | },
35 | spin: {
36 | '100%': {
37 | transform: 'rotate(360deg)',
38 | },
39 | },
40 | pulse: {
41 | '0%, 100%': {
42 | opacity: 1,
43 | },
44 | '50%': {
45 | opacity: 0.5,
46 | },
47 | },
48 | },
49 | animation: {
50 | spin: 'spin 1s linear infinite',
51 | pulse: 'pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite',
52 | }
53 | },
54 | plugins: [
55 | require('@tailwindcss/forms'),
56 | ],
57 | };
58 |
--------------------------------------------------------------------------------