├── public ├── favicon.ico ├── vercel.svg ├── thirteen.svg └── next.svg ├── postcss.config.js ├── jsconfig.json ├── utils ├── error.js ├── db.js └── Store.js ├── next.config.js ├── tailwind.config.js ├── pages ├── api │ ├── keys │ │ └── paypal.js │ ├── admin │ │ ├── cloudinary-sign.js │ │ ├── users │ │ │ ├── index.js │ │ │ └── [id].js │ │ ├── orders │ │ │ ├── index.js │ │ │ └── [id] │ │ │ │ └── deliver.js │ │ ├── summary.js │ │ └── products │ │ │ ├── index.js │ │ │ └── [id].js │ ├── orders │ │ ├── [id] │ │ │ ├── index.js │ │ │ └── pay.js │ │ ├── history.js │ │ └── index.js │ ├── auth │ │ ├── signup.js │ │ ├── update.js │ │ └── [...nextauth].js │ └── products │ │ └── [id].js ├── unauthorized.js ├── _app.js ├── index.js ├── payment.js ├── login.js ├── order-history.js ├── product │ └── [slug].js ├── shipping.js ├── register.js ├── cart.js ├── profile.js ├── admin │ ├── orders.js │ ├── dashboard.js │ ├── users.js │ ├── products.js │ └── product │ │ └── [id].js ├── placeorder.js ├── search.js └── order │ └── [id].js ├── components ├── DropdownLink.js ├── CheckoutWizard.js ├── ProductItem.js └── Layout.js ├── .gitignore ├── models ├── User.js ├── Product.js └── Order.js ├── styles └── globals.css ├── package.json └── README.md /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wildhunter4137/E-commerce-next/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["./*"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /utils/error.js: -------------------------------------------------------------------------------- 1 | export const getError = (error) => 2 | error.response && error.response.data && error.response.data.message 3 | ? error.response.data.message 4 | : error.message; 5 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ["res.cloudinary.com", "www.w3.org"], 6 | }, 7 | }; 8 | 9 | module.exports = nextConfig; 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./pages/**/*.{js,ts,jsx,tsx}", 5 | "./components/**/*.{js,ts,jsx,tsx}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [], 11 | }; 12 | -------------------------------------------------------------------------------- /pages/api/keys/paypal.js: -------------------------------------------------------------------------------- 1 | import { getSession } from "next-auth/react"; 2 | 3 | const handler = async (req, res) => { 4 | const session = await getSession({ req }); 5 | if (!session) { 6 | return res.status(401).send("Signin required"); 7 | } 8 | 9 | res.send(process.env.PAYPAL_CLIENT_ID || "sb"); 10 | }; 11 | 12 | export default handler; 13 | -------------------------------------------------------------------------------- /pages/api/admin/cloudinary-sign.js: -------------------------------------------------------------------------------- 1 | const cloudinary = require("cloudinary").v2; 2 | 3 | export default function signature(req, res) { 4 | const timestamp = Math.round(new Date().getTime() / 1000); 5 | const signature = cloudinary.utils.api_sign_request( 6 | { 7 | timestamp: timestamp, 8 | }, 9 | process.env.CLOUDINARY_SECRET 10 | ); 11 | res.statsCode = 200; 12 | res.json({ 13 | signature, 14 | timestamp, 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /components/DropdownLink.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React from "react"; 3 | 4 | function DropdownLink(props) { 5 | const { href, logout, children, ...rest } = props; 6 | 7 | return ( 8 | { 12 | if (logout) logout(); 13 | }} 14 | > 15 | {children} 16 | 17 | ); 18 | } 19 | 20 | export default DropdownLink; 21 | -------------------------------------------------------------------------------- /pages/api/orders/[id]/index.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session) { 8 | res.status(401).send("Signin required"); 9 | } 10 | await db.connect(); 11 | const order = await Order.findById(req.query.id); 12 | await db.disconnect(); 13 | res.send(order); 14 | }; 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /.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 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # env files 32 | .env 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /pages/api/admin/users/index.js: -------------------------------------------------------------------------------- 1 | import User from "@/models/User"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | await db.connect(); 11 | const users = await User.find({}); 12 | await db.disconnect(); 13 | res.send(users); 14 | }; 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /pages/unauthorized.js: -------------------------------------------------------------------------------- 1 | import Layout from '@/components/Layout'; 2 | import { useRouter } from 'next/router'; 3 | import React from 'react'; 4 | 5 | function Unauthorized() { 6 | const router = useRouter(); 7 | const { message } = router.query; 8 | 9 | return ( 10 | 11 |

Access Denied

12 |
{message &&
{message}
}
13 |
14 | ); 15 | } 16 | 17 | export default Unauthorized; 18 | -------------------------------------------------------------------------------- /pages/api/orders/history.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session) { 8 | res.status(401).send("Signin required"); 9 | } 10 | const { user } = session; 11 | await db.connect(); 12 | const orders = await Order.find({ 13 | user: user._id, 14 | }); 15 | await db.disconnect(); 16 | res.send(orders); 17 | }; 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/User.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const userSchema = new 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 | required: true, 17 | }, 18 | isAdmin: { 19 | type: Boolean, 20 | required: true, 21 | default: false, 22 | }, 23 | }, 24 | { 25 | timestamps: true, 26 | } 27 | ); 28 | 29 | const User = mongoose.models.User || mongoose.model("User", userSchema); 30 | export default User; 31 | -------------------------------------------------------------------------------- /components/CheckoutWizard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function CheckoutWizard({ activeStep = 0 }) { 4 | return ( 5 |
6 | {["User Login", "Shipping Address", "Payment Method", "Place Order"].map( 7 | (step, index) => ( 8 |
16 | {step} 17 |
18 | ) 19 | )} 20 |
21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /pages/api/admin/orders/index.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | if (req.method === "GET") { 11 | await db.connect(); 12 | const orders = await Order.find({}).populate("user", "name"); 13 | await db.disconnect(); 14 | console.log(orders); 15 | res.send(orders); 16 | } else { 17 | return res.status(400).send({ message: "Method not allowed" }); 18 | } 19 | }; 20 | 21 | export default handler; 22 | -------------------------------------------------------------------------------- /pages/api/orders/index.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session) { 8 | res.status(401).send("Signin required"); 9 | } 10 | const { user } = session; 11 | await db.connect(); 12 | const newOrder = new Order({ 13 | ...req.body, 14 | isPaid: false, 15 | paidAt: Date.now(), 16 | paymentResult: { 17 | id: "", 18 | status: "", 19 | email_address: "", 20 | }, 21 | user: user._id, 22 | }); 23 | const order = await newOrder.save(); 24 | await db.disconnect(); 25 | res.status(201).send(order); 26 | }; 27 | 28 | export default handler; 29 | -------------------------------------------------------------------------------- /pages/api/admin/orders/[id]/deliver.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | 11 | await db.connect(); 12 | const order = await Order.findById(req.query.id); 13 | if (order) { 14 | order.isDelivered = true; 15 | order.deliveredAt = Date.now(); 16 | const devliveredOrder = await order.save(); 17 | await db.disconnect(); 18 | res.send({ 19 | message: "Order delivered successfully", 20 | order: devliveredOrder, 21 | }); 22 | } else { 23 | await db.disconnect(); 24 | res.status(404).send({ 25 | message: "Error: order not found", 26 | }); 27 | } 28 | }; 29 | 30 | export default handler; 31 | -------------------------------------------------------------------------------- /pages/api/admin/users/[id].js: -------------------------------------------------------------------------------- 1 | import User from "@/models/User"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | if (req.method === "DELETE") { 11 | return deleteHandler(req, res); 12 | } else { 13 | return res.status(400).send({ message: "Method not allowed" }); 14 | } 15 | }; 16 | 17 | const deleteHandler = async (req, res) => { 18 | await db.connect(); 19 | const user = await User.findById(req.query.id); 20 | if (user) { 21 | await user.remove(); 22 | await db.disconnect(); 23 | return res.send({ message: "User deleted successfully" }); 24 | } else { 25 | await db.disconnect(); 26 | return res.status(404).send({ message: "User Not Found" }); 27 | } 28 | }; 29 | 30 | export default handler; 31 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @import url('https://fonts.googleapis.com/css2?family=Fredoka+One&display=swap'); 6 | 7 | @layer base { 8 | html { 9 | font-family: 'Fredoka One', cursive; 10 | } 11 | } 12 | 13 | .card{ 14 | @apply mb-5 block rounded-lg border border-gray-200 shadow-md 15 | } 16 | 17 | .primary-button{ 18 | @apply rounded bg-blue-500 py-2 shadow-lg outline-none hover:bg-blue-700 active:bg-blue-800 p-2 19 | } 20 | 21 | .default-button{ 22 | @apply rounded bg-gray-100 py-2 shadow outline-none hover:bg-gray-100 active:bg-gray-300 p-2 23 | } 24 | 25 | input, 26 | select, 27 | textarea{ 28 | @apply rounded border p-2 outline-none ring-indigo-400 focus:ring 29 | } 30 | 31 | .dropdown-link{ 32 | @apply flex p-2 hover:bg-gray-400; 33 | } 34 | 35 | .alert-error{ 36 | @apply my-3 rounded-lg bg-red-100 p-3 text-red-700; 37 | } 38 | 39 | .alert-success{ 40 | @apply my-3 rounded-lg bg-green-100 p-3 text-green-700; 41 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-shop", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.11", 13 | "@heroicons/react": "^2.0.15", 14 | "@next/font": "13.1.6", 15 | "@paypal/react-paypal-js": "^7.8.2", 16 | "axios": "^1.3.3", 17 | "bcryptjs": "^2.4.3", 18 | "chart.js": "^4.2.1", 19 | "cloudinary": "^1.34.0", 20 | "js-cookie": "^3.0.1", 21 | "mongoose": "^6.9.1", 22 | "next": "13.1.6", 23 | "next-auth": "^4.19.2", 24 | "react": "18.2.0", 25 | "react-chartjs-2": "^5.2.0", 26 | "react-dom": "18.2.0", 27 | "react-hook-form": "^7.43.1", 28 | "react-rating-stars-component": "^2.2.0", 29 | "react-responsive-carousel": "^3.2.23", 30 | "react-toastify": "^9.1.1" 31 | }, 32 | "devDependencies": { 33 | "autoprefixer": "^10.4.13", 34 | "postcss": "^8.4.21", 35 | "tailwindcss": "^3.2.6" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/db.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const connection = {}; 4 | 5 | async function connect() { 6 | if (connection.isConnected) { 7 | return; 8 | } 9 | if (mongoose.connections.length > 0) { 10 | connection.isConnected = mongoose.connections[0].readyState; 11 | if (connection.isConnected === 1) { 12 | return; 13 | } 14 | await mongoose.disconnect(); 15 | } 16 | const db = await mongoose.connect(process.env.MONGODO_URL); 17 | connection.isConnected = db.connections[0].readyState; 18 | } 19 | 20 | async function disconnect() { 21 | if (connection.isConnected) { 22 | if (process.env.NODE_ENV === "production") { 23 | await mongoose.disconnect(); 24 | connection.isConnected = false; 25 | } else { 26 | console.log("not disconnected"); 27 | } 28 | } 29 | } 30 | 31 | function convertDocToObj(doc) { 32 | doc._id = doc._id.toString(); 33 | doc.createdAt = doc.createdAt.toString(); 34 | doc.updatedAt = doc.updatedAt.toString(); 35 | return doc; 36 | } 37 | 38 | const db = { connect, disconnect, convertDocToObj }; 39 | export default db; 40 | -------------------------------------------------------------------------------- /pages/api/orders/[id]/pay.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session) { 8 | res.status(401).send("Signin required"); 9 | } 10 | await db.connect(); 11 | const order = await Order.findById(req.query.id); 12 | if (order) { 13 | if (order.isPaid) { 14 | return res.status(400).send({ message: "Error: order is already paid" }); 15 | } 16 | order.isPaid = true; 17 | order.paidAt = Date.now(); 18 | order.paymentResult = { 19 | id: req.body.id, 20 | status: req.body.status, 21 | email_address: req.body.email_address, 22 | }; 23 | 24 | const paidOrder = await order.save(); 25 | await db.disconnect(); 26 | res.send({ 27 | message: "Order paid successfully", 28 | order: paidOrder, 29 | }); 30 | } else { 31 | await db.disconnect(); 32 | res.status(404).send({ 33 | message: "Error: order not found", 34 | }); 35 | } 36 | }; 37 | 38 | export default handler; 39 | -------------------------------------------------------------------------------- /pages/api/auth/signup.js: -------------------------------------------------------------------------------- 1 | import User from "@/models/User"; 2 | import db from "@/utils/db"; 3 | import bcryptjs from "bcryptjs"; 4 | 5 | const handler = async (req, res) => { 6 | if (req.method !== "POST") { 7 | return; 8 | } 9 | 10 | const { name, email, password } = req.body; 11 | 12 | if ( 13 | !name || 14 | !email || 15 | !email.includes("@") || 16 | !password || 17 | password.trim().length < 5 18 | ) { 19 | res.status(422).json({ 20 | message: "Validation error", 21 | }); 22 | return; 23 | } 24 | await db.connect(); 25 | const existingUser = await User.findOne({ email: email }); 26 | 27 | if (existingUser) { 28 | res.status(422).json({ 29 | message: "User exists already", 30 | }); 31 | await db.disconnect(); 32 | return; 33 | } 34 | 35 | const newUser = await User({ 36 | name, 37 | email, 38 | password: bcryptjs.hashSync(password), 39 | isAdmin: false, 40 | }); 41 | 42 | const user = await newUser.save(); 43 | 44 | await db.disconnect(); 45 | 46 | res.status(201).send({ 47 | message: "Created user", 48 | _id: user._id, 49 | name: user.name, 50 | email: user.email, 51 | isAdmin: user.isAdmin, 52 | }); 53 | }; 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /pages/api/auth/update.js: -------------------------------------------------------------------------------- 1 | import bcryptjs from "bcryptjs"; 2 | import User from "@/models/User"; 3 | import db from "@/utils/db"; 4 | import { getSession } from "next-auth/react"; 5 | 6 | const handler = async (req, res) => { 7 | if (req.method != "PUT") { 8 | return res.status(400).send({ 9 | message: `${req.method} not supported`, 10 | }); 11 | } 12 | 13 | const session = await getSession({ req }); 14 | if (!session) { 15 | res.status(401).send("Signin required"); 16 | } 17 | const { user } = session; 18 | const { name, email, password } = req.body; 19 | 20 | if ( 21 | !name || 22 | !email || 23 | !email.includes("@") || 24 | !password || 25 | password.trim().length < 5 26 | ) { 27 | res.status(422).json({ 28 | message: "Validation error", 29 | }); 30 | return; 31 | } 32 | 33 | await db.connect(); 34 | 35 | const toUpdateUser = await User.findById(user._id); 36 | toUpdateUser.name = name; 37 | toUpdateUser.email = email; 38 | if (password) { 39 | toUpdateUser.password = bcryptjs.hashSync(password); 40 | } 41 | 42 | await toUpdateUser.save(); 43 | 44 | await db.disconnect(); 45 | 46 | res.send({ 47 | message: "User updated", 48 | }); 49 | }; 50 | 51 | export default handler; 52 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { StoreProvide } from "@/utils/Store"; 3 | import { PayPalScriptProvider } from "@paypal/react-paypal-js"; 4 | import { SessionProvider, useSession } from "next-auth/react"; 5 | import { useRouter } from "next/router"; 6 | 7 | export default function App({ 8 | Component, 9 | pageProps: { session, ...pageProps }, 10 | }) { 11 | return ( 12 | 13 | 14 | 15 | {Component.auth ? ( 16 | 17 | 18 | 19 | ) : ( 20 | 21 | )} 22 | 23 | 24 | 25 | ); 26 | } 27 | 28 | function Auth({ children, adminOnly }) { 29 | const router = useRouter(); 30 | const { status, data: session } = useSession({ 31 | required: true, 32 | onUnauthenticated() { 33 | router.push("/unauthorized?message=loging required"); 34 | }, 35 | }); 36 | 37 | if (status === "loading") { 38 | return
Loading...
; 39 | } 40 | 41 | if (adminOnly && !session.user.isAdmin) { 42 | router.push("/unauthorized?message=admin login required"); 43 | } 44 | 45 | return children; 46 | } 47 | -------------------------------------------------------------------------------- /components/ProductItem.js: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import ReactStars from "react-rating-stars-component"; 5 | 6 | export default function ProductItem({ product, addToCartHandler }) { 7 | const ratingChanged = () => {}; 8 | 9 | return ( 10 |
11 | 12 | 13 | {product.name} 20 | 21 | 22 |
23 | 24 |

{product.name}

25 | 26 |

{product.brand}

27 | 35 |

{product.price}₹

36 | 43 |
44 |
45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /pages/api/admin/summary.js: -------------------------------------------------------------------------------- 1 | import Order from "@/models/Order"; 2 | import Product from "@/models/Product"; 3 | import User from "@/models/User"; 4 | import db from "@/utils/db"; 5 | import { getSession } from "next-auth/react"; 6 | 7 | const handler = async (req, res) => { 8 | const session = await getSession({ req }); 9 | if (!session || (session && !session.user.isAdmin)) { 10 | res.status(401).send("Signin required"); 11 | } 12 | 13 | await db.connect(); 14 | 15 | const ordersCount = await Order.countDocuments(); 16 | const productsCount = await Product.countDocuments(); 17 | const usersCount = await User.countDocuments(); 18 | 19 | const ordersPriceGroup = await Order.aggregate([ 20 | { 21 | $group: { 22 | _id: null, 23 | sales: { $sum: "$totalPrice" }, 24 | }, 25 | }, 26 | ]); 27 | 28 | const ordersPrice = 29 | ordersPriceGroup.length > 0 ? ordersPriceGroup[0].sales : 0; 30 | 31 | const salesDate = await Order.aggregate([ 32 | { 33 | $group: { 34 | _id: { 35 | $dateToString: { 36 | format: "%Y-%m", 37 | date: "$createdAt", 38 | }, 39 | }, 40 | totalSales: { $sum: "$totalPrice" }, 41 | }, 42 | }, 43 | ]); 44 | 45 | await db.disconnect(); 46 | 47 | res.send({ 48 | ordersPrice, 49 | ordersCount, 50 | productsCount, 51 | usersCount, 52 | salesDate, 53 | }); 54 | }; 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /pages/api/admin/products/index.js: -------------------------------------------------------------------------------- 1 | import Product from "@/models/Product"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | if (req.method === "GET") { 11 | await db.connect(); 12 | const products = await Product.find({}); 13 | await db.disconnect(); 14 | res.send(products); 15 | } 16 | if (req.method === "POST") { 17 | return postHandler(req, res); 18 | } else { 19 | return res.status(400).send({ message: "Method not allowed" }); 20 | } 21 | }; 22 | 23 | const postHandler = async (req, res) => { 24 | await db.connect(); 25 | const newProduct = new Product({ 26 | name: "sample name", 27 | slug: "sample-name-" + Math.random(), 28 | image: "/image/default-image.svg", 29 | price: 0, 30 | category: "sample category", 31 | brand: "sample brand", 32 | countInStock: 0, 33 | description: "sample description", 34 | rating: 0, 35 | ratings: [0, 0, 0, 0, 0], 36 | totalRatings: 0, 37 | numReviews: 0, 38 | reviews: [], 39 | isFeatured: false, 40 | banner: "", 41 | }); 42 | 43 | console.log("post called...."); 44 | console.log(newProduct); 45 | 46 | const product = await newProduct.save(); 47 | await db.disconnect(); 48 | res.send({ 49 | message: "Product created succssfully", 50 | product, 51 | }); 52 | }; 53 | 54 | export default handler; 55 | -------------------------------------------------------------------------------- /models/Product.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const productSchema = new mongoose.Schema( 4 | { 5 | name: { 6 | type: String, 7 | required: true, 8 | }, 9 | slug: { 10 | type: String, 11 | required: true, 12 | unique: true, 13 | }, 14 | category: { 15 | type: String, 16 | required: true, 17 | }, 18 | image: { 19 | type: String, 20 | required: true, 21 | }, 22 | price: { 23 | type: Number, 24 | required: true, 25 | }, 26 | brand: { 27 | type: String, 28 | required: true, 29 | }, 30 | rating: { 31 | type: Number, 32 | required: true, 33 | default: 0, 34 | }, 35 | ratings: [ 36 | { 37 | type: Number, 38 | required: true, 39 | default: 0, 40 | }, 41 | ], 42 | totalRatings: { 43 | type: Number, 44 | required: true, 45 | default: 0, 46 | }, 47 | numReviews: { 48 | type: Number, 49 | required: true, 50 | default: 0, 51 | }, 52 | reviews: [ 53 | { 54 | type: String, 55 | required: true, 56 | }, 57 | ], 58 | countInStock: { 59 | type: Number, 60 | required: true, 61 | default: 0, 62 | }, 63 | description: { 64 | type: String, 65 | required: true, 66 | }, 67 | isFeatured: { 68 | type: Boolean, 69 | default: false, 70 | }, 71 | banner: String, 72 | }, 73 | { 74 | timestamps: true, 75 | } 76 | ); 77 | 78 | const Product = 79 | mongoose.models.Product || mongoose.model("Product", productSchema); 80 | export default Product; 81 | -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].js: -------------------------------------------------------------------------------- 1 | import User from "@/models/User"; 2 | import db from "@/utils/db"; 3 | import NextAuth from "next-auth/next"; 4 | import CredentialsProvider from "next-auth/providers/credentials"; 5 | import bcryptjs from "bcryptjs"; 6 | 7 | export default NextAuth({ 8 | session: { 9 | strategy: "jwt", 10 | }, 11 | callbacks: { 12 | async jwt({ token, user }) { 13 | if (user?._id) { 14 | token._id = user._id; 15 | } 16 | 17 | if (user?.isAdmin) { 18 | token.isAdmin = user.isAdmin; 19 | } 20 | 21 | return token; 22 | }, 23 | async session({ session, token }) { 24 | if (token?._id) { 25 | session.user._id = token._id; 26 | } 27 | 28 | if (token?.isAdmin) { 29 | session.user.isAdmin = token.isAdmin; 30 | } 31 | 32 | return session; 33 | }, 34 | }, 35 | providers: [ 36 | CredentialsProvider({ 37 | async authorize(creadentials) { 38 | console.log("authorize called"); 39 | console.log("connect running..."); 40 | await db.connect(); 41 | 42 | console.log("findOne called"); 43 | console.log("findOne running..."); 44 | const user = await User.findOne({ 45 | email: creadentials.email, 46 | }); 47 | 48 | console.log("user is:-", user); 49 | 50 | console.log("disconnect called"); 51 | console.log("disconnect running..."); 52 | await db.disconnect(); 53 | 54 | console.log("retruning..."); 55 | if ( 56 | user && 57 | bcryptjs.compareSync(creadentials.password, user.password) 58 | ) { 59 | return { 60 | _id: user._id, 61 | name: user.name, 62 | email: user.email, 63 | image: "image", 64 | isAdmin: user.isAdmin, 65 | }; 66 | } 67 | 68 | throw new Error("Invalid email or password"); 69 | }, 70 | }), 71 | ], 72 | }); 73 | -------------------------------------------------------------------------------- /pages/api/admin/products/[id].js: -------------------------------------------------------------------------------- 1 | import Product from "@/models/Product"; 2 | import db from "@/utils/db"; 3 | import { getSession } from "next-auth/react"; 4 | 5 | const handler = async (req, res) => { 6 | const session = await getSession({ req }); 7 | if (!session || (session && !session.user.isAdmin)) { 8 | res.status(401).send("Admin signin required"); 9 | } 10 | if (req.method === "GET") { 11 | return getHandler(req, res); 12 | } else if (req.method === "PUT") { 13 | return putHandler(req, res); 14 | } else if (req.method === "DELETE") { 15 | return deleteHandler(req, res); 16 | } else { 17 | return res.status(400).send({ message: "Method not allowed" }); 18 | } 19 | }; 20 | 21 | const deleteHandler = async (req, res) => { 22 | await db.connect(); 23 | const product = await Product.findById(req.query.id); 24 | if (product) { 25 | await product.remove(); 26 | await db.disconnect(); 27 | return res.send({ message: "Product deleted successfully" }); 28 | } else { 29 | await db.disconnect(); 30 | return res.status(404).send({ message: "Product not found" }); 31 | } 32 | }; 33 | 34 | const getHandler = async (req, res) => { 35 | await db.connect(); 36 | const product = await Product.findById(req.query.id); 37 | await db.disconnect(); 38 | res.send(product); 39 | }; 40 | 41 | const putHandler = async (req, res) => { 42 | await db.connect(); 43 | const product = await Product.findById(req.query.id); 44 | if (product) { 45 | product.name = req.body.name; 46 | product.slug = req.body.slug; 47 | product.price = req.body.price; 48 | product.category = req.body.category; 49 | product.image = req.body.image; 50 | product.brand = req.body.brand; 51 | product.countInStock = req.body.countInStock; 52 | product.description = req.body.description; 53 | await product.save(); 54 | await db.disconnect(); 55 | return res.send({ message: "Product updated successfully" }); 56 | } else { 57 | await db.disconnect(); 58 | return res.status(404).send({ message: "Product not found" }); 59 | } 60 | }; 61 | 62 | export default handler; 63 | -------------------------------------------------------------------------------- /pages/api/products/[id].js: -------------------------------------------------------------------------------- 1 | import Product from "@/models/Product"; 2 | import db from "@/utils/db"; 3 | 4 | const handler = async (req, res) => { 5 | if (req.method === "GET") { 6 | return getHandler(req, res); 7 | } else if (req.method === "PUT") { 8 | return putHandler(req, res); 9 | } else { 10 | return res.status(400).send({ message: "Method not allowed" }); 11 | } 12 | }; 13 | 14 | const getHandler = async (req, res) => { 15 | await db.connect(); 16 | const product = await Product.findById(req.query.id); 17 | await db.disconnect(); 18 | res.send(product); 19 | }; 20 | 21 | function calculateRatings(star5, star4, star3, star2, star1) { 22 | return ( 23 | (5 * star5 + 4 * star4 + 3 * star3 + 2 * star2 + 1 * star1) / 24 | (star5 + star4 + star3 + star2 + star1) 25 | ); 26 | } 27 | 28 | const putHandler = async (req, res) => { 29 | await db.connect(); 30 | console.log("put called...."); 31 | const product = await Product.findById(req.query.id); 32 | console.log("product is:-", product); 33 | if (product) { 34 | const star5 = product.ratings[0]; 35 | const star4 = product.ratings[1]; 36 | const star3 = product.ratings[2]; 37 | const star2 = product.ratings[3]; 38 | const star1 = product.ratings[4]; 39 | 40 | if (req.body.rating === 5) { 41 | product.ratings[0] = star5 + 1; 42 | } else if (req.body.rating === 4) { 43 | product.ratings[1] = star4 + 1; 44 | } else if (req.body.rating === 3) { 45 | product.ratings[2] = star3 + 1; 46 | } else if (req.body.rating === 2) { 47 | product.ratings[3] = star2 + 1; 48 | } else { 49 | product.ratings[4] = star1 + 1; 50 | } 51 | 52 | product.totalRatings = product.totalRatings + 1; 53 | 54 | product.rating = calculateRatings( 55 | product.ratings[0], 56 | product.ratings[1], 57 | product.ratings[2], 58 | product.ratings[3], 59 | product.ratings[4] 60 | ); 61 | 62 | console.log("final product", product); 63 | await product.save(); 64 | await db.disconnect(); 65 | return res.send({ message: "Ratings updated successfully" }); 66 | } else { 67 | await db.disconnect(); 68 | return res.status(404).send({ message: "Product not found" }); 69 | } 70 | }; 71 | 72 | export default handler; 73 | -------------------------------------------------------------------------------- /models/Order.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const orderSchema = new mongoose.Schema( 4 | { 5 | user: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "User", 8 | required: true, 9 | }, 10 | orderItems: [ 11 | { 12 | name: { 13 | type: String, 14 | required: true, 15 | }, 16 | quantity: { 17 | type: Number, 18 | required: true, 19 | }, 20 | image: { 21 | type: String, 22 | required: true, 23 | }, 24 | price: { 25 | type: Number, 26 | required: true, 27 | }, 28 | }, 29 | ], 30 | shippingAddress: { 31 | fullName: { 32 | type: String, 33 | required: true, 34 | }, 35 | address: { 36 | type: String, 37 | required: true, 38 | }, 39 | city: { 40 | type: String, 41 | required: true, 42 | }, 43 | postalCode: { 44 | type: String, 45 | required: true, 46 | }, 47 | country: { 48 | type: String, 49 | required: true, 50 | }, 51 | }, 52 | paymentMethod: { 53 | type: String, 54 | required: true, 55 | }, 56 | paymentResult: { 57 | id: String, 58 | status: String, 59 | email_address: String, 60 | }, 61 | itemsPrice: { 62 | type: Number, 63 | required: true, 64 | }, 65 | shippingPrice: { 66 | type: Number, 67 | required: true, 68 | }, 69 | taxPrice: { 70 | type: Number, 71 | required: true, 72 | }, 73 | totalPrice: { 74 | type: Number, 75 | required: true, 76 | }, 77 | isPaid: { 78 | type: Boolean, 79 | required: true, 80 | default: false, 81 | }, 82 | isDelivered: { 83 | type: Boolean, 84 | required: true, 85 | default: false, 86 | }, 87 | paidAt: { 88 | type: Date, 89 | }, 90 | deliveredAt: { 91 | type: Date, 92 | }, 93 | }, 94 | { 95 | timestamps: true, 96 | } 97 | ); 98 | 99 | const Order = mongoose.models.Order || mongoose.model("Order", orderSchema); 100 | export default Order; 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## :star2: About the Project 2 | 3 | MyShop is an Ecommerce web application built with Next.js and MongoDB. 4 | 5 | ## LIVE DEMO 💥 6 | 7 | 8 | 9 | ### :camera: Screenshots 10 | 11 | ![Screenshot (79)](https://user-images.githubusercontent.com/87040096/219966144-f29fe874-400c-44bd-8708-0026616c8c09.png) 12 | 13 | ## 14 | 15 | ![Screenshot (82)](https://user-images.githubusercontent.com/87040096/219966155-35a608b3-0c48-447b-b86d-2f8c21e153cc.png) 16 | 17 | ## 18 | 19 | ![Screenshot (89)](https://user-images.githubusercontent.com/87040096/219966164-ec2ee88d-fce1-4004-a8bd-fb655523b8db.png) 20 | 21 | ## 22 | 23 | ![Screenshot (90)](https://user-images.githubusercontent.com/87040096/219966172-ca106a05-4eee-4a73-813a-90b7ac8de21c.png) 24 | 25 | ### :space_invader: Tech Stack 26 | 27 |
28 | Client 29 | 33 |
34 | 35 |
36 | Database 37 | 40 |
41 |
42 | 43 | 44 | 45 | 48 | 51 | 54 | 57 | 58 |
46 | 47 | 49 | 50 | 52 | 53 | 55 | 56 |
59 | 60 | # 🏃‍♀️ Running 61 | 62 | - Clone repo Run `git clone https://github.com/wildhunter4137/E-commerce-next.git` 63 | - Run `npm install` 64 | - Run `npm run dev` 65 | - See `http://localhost:3000` 66 | 67 | 68 | 69 | ## Contact 70 | 71 | Project Link: https://github.com/wildhunter4137/E-commerce-next 72 | -------------------------------------------------------------------------------- /utils/Store.js: -------------------------------------------------------------------------------- 1 | import Cookies from "js-cookie"; 2 | const { createContext, useReducer } = require("react"); 3 | export const Store = createContext(); 4 | 5 | const initialStore = { 6 | cart: Cookies.get("cart") 7 | ? JSON.parse(Cookies.get("cart")) 8 | : { cartItems: [], shippingAddress: {}, paymentMethod: "" }, 9 | }; 10 | 11 | function reducer(state, action) { 12 | switch (action.type) { 13 | case "CART_ADD_ITEM": { 14 | const newItem = action.payload; 15 | const existItem = state.cart.cartItems.find( 16 | (item) => item.slug === newItem.slug 17 | ); 18 | const cartItems = existItem 19 | ? state.cart.cartItems.map((item) => 20 | item.name === existItem.name ? newItem : item 21 | ) 22 | : [...state.cart.cartItems, newItem]; 23 | 24 | Cookies.set("cart", JSON.stringify({ ...state.cart, cartItems })); 25 | 26 | return { ...state, cart: { ...state.cart, cartItems } }; 27 | } 28 | case "CART_REMOVE_ITEM": { 29 | const cartItems = state.cart.cartItems.filter( 30 | (item) => item.slug != action.payload.slug 31 | ); 32 | 33 | Cookies.set("cart", JSON.stringify({ ...state.cart, cartItems })); 34 | 35 | return { ...state, cart: { ...state.cart, cartItems } }; 36 | } 37 | case "CART_RESET": { 38 | return { 39 | ...state, 40 | cart: { 41 | cartItems: [], 42 | shippingAddress: { 43 | location: {}, 44 | }, 45 | paymentMethod: "", 46 | }, 47 | }; 48 | } 49 | case "SAVE_SHIPPING_ADDRESS": { 50 | return { 51 | ...state, 52 | cart: { 53 | ...state.cart, 54 | shippingAddress: { 55 | ...state.cart.shippingAddress, 56 | ...action.payload, 57 | }, 58 | }, 59 | }; 60 | } 61 | case "SAVE_PAYMENT_METHOD": { 62 | return { 63 | ...state, 64 | cart: { 65 | ...state.cart, 66 | paymentMethod: action.payload, 67 | }, 68 | }; 69 | } 70 | case "CART_CLEAR_ITEMS": { 71 | return { ...state, cart: { ...state.cart, cartItems: [] } }; 72 | } 73 | default: 74 | return state; 75 | } 76 | } 77 | 78 | export function StoreProvide({ children }) { 79 | const [state, dispatch] = useReducer(reducer, initialStore); 80 | const value = { state, dispatch }; 81 | return {children}; 82 | } 83 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import ProductItem from "@/components/ProductItem"; 3 | import Product from "@/models/Product"; 4 | import db from "@/utils/db"; 5 | import { Store } from "@/utils/Store"; 6 | import axios from "axios"; 7 | import Link from "next/link"; 8 | import { useContext } from "react"; 9 | import "react-responsive-carousel/lib/styles/carousel.min.css"; 10 | import { Carousel } from "react-responsive-carousel"; 11 | import { toast } from "react-toastify"; 12 | 13 | export default function Home({ featuredProducts, products }) { 14 | const { state, dispatch } = useContext(Store); 15 | const { cart } = state; 16 | 17 | const addToCartHandler = async (product) => { 18 | const existItem = cart.cartItems.find((item) => item.slug === product.slug); 19 | const quantity = existItem ? existItem.quantity + 1 : 1; 20 | 21 | const { data } = await axios.get(`/api/products/${product._id}`); 22 | 23 | if (data.countInStock < quantity) { 24 | toast.error("Sorry. Product is out of stock"); 25 | return; 26 | } 27 | 28 | dispatch({ 29 | type: "CART_ADD_ITEM", 30 | payload: { ...product, quantity: quantity }, 31 | }); 32 | 33 | toast.success("Product added to the cart"); 34 | }; 35 | 36 | return ( 37 | 38 |
39 | 40 | {featuredProducts.map((product) => ( 41 |
42 | 43 |
44 | {product.name} 45 |
46 | 47 |
48 | ))} 49 |
50 |
51 |

Latest Products

52 |
53 | {products.map((product) => ( 54 | 59 | ))} 60 |
61 |
62 | ); 63 | } 64 | 65 | export async function getServerSideProps() { 66 | await db.connect(); 67 | const products = await Product.find().lean(); 68 | const featuredProducts = products.filter( 69 | (product) => product.isFeatured === true 70 | ); 71 | return { 72 | props: { 73 | featuredProducts: featuredProducts.map(db.convertDocToObj), 74 | products: products.map(db.convertDocToObj), 75 | }, 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /pages/payment.js: -------------------------------------------------------------------------------- 1 | import CheckoutWizard from "@/components/CheckoutWizard"; 2 | import Layout from "@/components/Layout"; 3 | import { Store } from "@/utils/Store"; 4 | import Cookies from "js-cookie"; 5 | import { useRouter } from "next/router"; 6 | import React, { useContext, useEffect, useState } from "react"; 7 | import { toast } from "react-toastify"; 8 | 9 | export default function Payment() { 10 | const router = useRouter(); 11 | 12 | const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(""); 13 | 14 | const { state, dispatch } = useContext(Store); 15 | const { cart } = state; 16 | const { shippingAddress, paymentMethod } = cart; 17 | 18 | useEffect(() => { 19 | if (!shippingAddress.address) { 20 | router.push("/shipping"); 21 | } 22 | setSelectedPaymentMethod(paymentMethod || ""); 23 | }, [paymentMethod, router, shippingAddress.address]); 24 | 25 | const submitHandler = (e) => { 26 | e.preventDefault(); 27 | if (!selectedPaymentMethod) { 28 | return toast.error("Payment method is required"); 29 | } 30 | 31 | dispatch({ type: "SAVE_PAYMENT_METHOD", payload: selectedPaymentMethod }); 32 | 33 | Cookies.set( 34 | "cart", 35 | JSON.stringify({ 36 | ...cart, 37 | paymentMethod: selectedPaymentMethod, 38 | }) 39 | ); 40 | 41 | router.push("/placeorder"); 42 | }; 43 | 44 | return ( 45 | 46 | 47 |
48 |

Payment Method

49 | {["PayPal", "Stripe", "CashOnDelivery"].map((payment) => ( 50 |
51 | setSelectedPaymentMethod(payment)} 58 | /> 59 | 60 | 63 |
64 | ))} 65 |
66 | 73 | 74 |
75 |
76 |
77 | ); 78 | } 79 | 80 | Payment.auth = true; 81 | -------------------------------------------------------------------------------- /pages/login.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import Link from "next/link"; 3 | import { signIn, useSession } from "next-auth/react"; 4 | import React, { useEffect } from "react"; 5 | import { useForm } from "react-hook-form"; 6 | import { getError } from "@/utils/error"; 7 | import { toast } from "react-toastify"; 8 | import { useRouter } from "next/router"; 9 | 10 | export default function Login() { 11 | const { data: session } = useSession(); 12 | const router = useRouter(); 13 | const { redirect } = router.query; 14 | 15 | useEffect(() => { 16 | if (session?.user) { 17 | router.push(redirect || "/"); 18 | } 19 | }, [router, session, redirect]); 20 | 21 | const { 22 | register, 23 | handleSubmit, 24 | watch, 25 | formState: { errors }, 26 | } = useForm(); 27 | 28 | const submitHandler = async ({ email, password }) => { 29 | try { 30 | const result = await signIn("credentials", { 31 | redirect: false, 32 | email, 33 | password, 34 | }); 35 | 36 | if (result.error) { 37 | toast.error(result.error); 38 | } 39 | } catch (error) { 40 | toast.error(getError(error)); 41 | } 42 | }; 43 | 44 | return ( 45 | 46 |
50 |

Login

51 |
52 | 53 | 67 | {errors.email && ( 68 |
{errors.email.message}
69 | )} 70 |
71 |
72 | 73 | 85 | {errors.password && ( 86 |
{errors.password.message}
87 | )} 88 |
89 |
90 | 91 |
92 |
93 | Don't have an account?   94 | Register 95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /pages/order-history.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import Link from "next/link"; 5 | import React, { useEffect, useReducer } from "react"; 6 | 7 | function reducer(state, action) { 8 | switch (action.type) { 9 | case "FETCH_REQUEST": { 10 | return { ...state, loading: true, error: "" }; 11 | } 12 | case "FETCH_SUCCESS": { 13 | return { ...state, loading: false, error: "", orders: action.payload }; 14 | } 15 | case "FETCH_FAIL": { 16 | return { ...state, loading: false, error: action.payload }; 17 | } 18 | default: { 19 | return state; 20 | } 21 | } 22 | } 23 | 24 | export default function OrderHistory() { 25 | const [{ loading, error, orders }, dispatch] = useReducer(reducer, { 26 | loading: true, 27 | error: "", 28 | orders: [], 29 | }); 30 | 31 | useEffect(() => { 32 | const fetchOrders = async () => { 33 | try { 34 | dispatch({ type: "FETCH_REQUEST" }); 35 | const { data } = await axios.get("api/orders/history"); 36 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 37 | } catch (error) { 38 | dispatchEvent({ type: "FETCH_FAIL", payload: getError(error) }); 39 | } 40 | }; 41 | fetchOrders(); 42 | }, []); 43 | 44 | return ( 45 | 46 |

Order History

47 | {loading ? ( 48 |
Loading...
49 | ) : error ? ( 50 |
{error}
51 | ) : ( 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | {orders.map((order) => ( 66 | 67 | 68 | 69 | 70 | 75 | 80 | 85 | 86 | ))} 87 | 88 |
IDDATETOTALPAIDDELIVEREDACTION
{order._id.substring(20, 24)}{order.createdAt.substring(0, 10)}{order.totalPrice} ₹ 71 | {order.isPaid 72 | ? `${order.paidAt.substring(0, 10)}` 73 | : "not paid"} 74 | 76 | {order.isDelivered 77 | ? `${order.deliveredAt.substring(0, 10)}` 78 | : "not delivered"} 79 | 81 | 82 | Details 83 | 84 |
89 |
90 | )} 91 |
92 | ); 93 | } 94 | 95 | OrderHistory.auth = true; 96 | -------------------------------------------------------------------------------- /pages/product/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from '@/components/Layout'; 2 | import Product from '@/models/Product'; 3 | import db from '@/utils/db'; 4 | import { getError } from '@/utils/error'; 5 | import { Store } from '@/utils/Store'; 6 | import axios from 'axios'; 7 | import Image from 'next/image'; 8 | import Link from 'next/link'; 9 | import { useRouter } from 'next/router'; 10 | import React, { useContext, useState } from 'react'; 11 | import ReactStars from 'react-rating-stars-component'; 12 | import { toast } from 'react-toastify'; 13 | 14 | export default function ProductDetail(props) { 15 | const { product } = props; 16 | 17 | const { state, dispatch } = useContext(Store); 18 | 19 | const router = useRouter(); 20 | 21 | if (!product) { 22 | return ( 23 | 24 |
Product Not Found
25 |
26 | ); 27 | } 28 | 29 | const addToCartHandler = async () => { 30 | const existItem = state.cart.cartItems.find((item) => item.slug === product.slug); 31 | const quantity = existItem ? existItem.quantity + 1 : 1; 32 | 33 | const { data } = await axios.get(`/api/products/${product._id}`); 34 | 35 | if (data.countInStock < quantity) { 36 | toast.error('Sorry. Product is out of stock'); 37 | return; 38 | } 39 | 40 | dispatch({ 41 | type: 'CART_ADD_ITEM', 42 | payload: { ...product, quantity: quantity }, 43 | }); 44 | 45 | router.push('/cart'); 46 | }; 47 | 48 | const ratingChanged = async (productId, count) => { 49 | try { 50 | await axios.put(`/api/products/${productId}`, { 51 | rating: count, 52 | }); 53 | toast.success('We really appreciate you taking the time to share your rating with us'); 54 | } catch (error) { 55 | dispatch({ type: 'UPDATE_FAUL', payload: getError(error) }); 56 | toast.error(getError(error)); 57 | } 58 | }; 59 | 60 | return ( 61 | 62 |
63 |
64 | {product.name} 65 |
66 |
67 |
    68 |
  • 69 |

    {product.name}

    70 |
  • 71 |
  • Category: {product.category}
  • 72 |
  • Brand: {product.brand}
  • 73 |
  • 74 |
    75 | 76 | {product.totalRatings} Ratings 77 |
    78 |
  • 79 |
  • Description: {product.description}
  • 80 |
    81 |
  • 82 | Rate Product: 83 | { 86 | ratingChanged(product._id, count); 87 | }} 88 | size={24} 89 | activeColor='#ffd700' 90 | edit={true} 91 | /> 92 |
  • 93 |
94 |
95 |
96 |
97 |
98 |
Price
99 |
{product.price} ₹
100 |
101 |
102 |
Status
103 |
{product.countInStock > 0 ? 'In Stock' : 'Unavailable'}
104 |
105 | 108 |
109 |
110 |
111 |
112 | ); 113 | } 114 | 115 | export async function getServerSideProps(context) { 116 | const { params } = context; 117 | const { slug } = params; 118 | await db.connect(); 119 | const product = await Product.findOne({ 120 | slug: slug, 121 | }).lean(); 122 | await db.disconnect(); 123 | return { 124 | props: { 125 | product: product ? db.convertDocToObj(product) : null, 126 | }, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /pages/shipping.js: -------------------------------------------------------------------------------- 1 | import CheckoutWizard from '@/components/CheckoutWizard'; 2 | import Layout from '@/components/Layout'; 3 | import { Store } from '@/utils/Store'; 4 | import Cookies from 'js-cookie'; 5 | import { useRouter } from 'next/router'; 6 | import React, { useContext, useEffect } from 'react'; 7 | import { useForm } from 'react-hook-form'; 8 | 9 | export default function Shipping() { 10 | const { 11 | register, 12 | handleSubmit, 13 | setValue, 14 | getValues, 15 | formState: { errors }, 16 | } = useForm(); 17 | 18 | const { state, dispatch } = useContext(Store); 19 | const { cart } = state; 20 | const { shippingAddress } = cart; 21 | 22 | useEffect(() => { 23 | setValue('fullName', shippingAddress.fullName); 24 | setValue('address', shippingAddress.address); 25 | setValue('city', shippingAddress.city); 26 | setValue('postalCode', shippingAddress.postalCode); 27 | setValue('country', shippingAddress.country); 28 | }, [setValue, shippingAddress]); 29 | 30 | const router = useRouter(); 31 | 32 | const submitHandler = ({ fullName, address, city, postalCode, country }) => { 33 | dispatch({ 34 | type: 'SAVE_SHIPPING_ADDRESS', 35 | payload: { 36 | fullName, 37 | address, 38 | city, 39 | postalCode, 40 | country, 41 | }, 42 | }); 43 | 44 | Cookies.set( 45 | 'cart', 46 | JSON.stringify({ 47 | ...cart, 48 | shippingAddress: { 49 | fullName, 50 | address, 51 | city, 52 | postalCode, 53 | country, 54 | }, 55 | }) 56 | ); 57 | 58 | router.push('/payment'); 59 | }; 60 | 61 | return ( 62 | 63 | 64 |
65 |

Shipping Mall

66 | 67 |
68 | 69 | 78 | {errors.fullName &&
{errors.fullName.message}
} 79 |
80 | 81 |
82 | 83 | 95 | {errors.address &&
{errors.address.message}
} 96 |
97 | 98 |
99 | 100 | 108 | {errors.city &&
{errors.city.message}
} 109 |
110 | 111 |
112 | 113 | 121 | {errors.postalCode &&
{errors.postalCode.message}
} 122 |
123 | 124 |
125 | 126 | 134 | {errors.country &&
{errors.country.message}
} 135 |
136 | 137 |
138 | 139 |
140 |
141 |
142 | ); 143 | } 144 | 145 | Shipping.auth = true; 146 | -------------------------------------------------------------------------------- /pages/register.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import { signIn, useSession } from "next-auth/react"; 5 | import Link from "next/link"; 6 | import { useRouter } from "next/router"; 7 | import React, { useEffect } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { toast } from "react-toastify"; 10 | 11 | export default function Register() { 12 | const { data: session } = useSession(); 13 | const router = useRouter(); 14 | const { redirect } = router.query; 15 | 16 | useEffect(() => { 17 | if (session?.user) { 18 | router.push(redirect || "/"); 19 | } 20 | }, [router, session, redirect]); 21 | 22 | const { 23 | register, 24 | handleSubmit, 25 | watch, 26 | getValues, 27 | formState: { errors }, 28 | } = useForm(); 29 | 30 | const submitHandler = async ({ name, email, password }) => { 31 | try { 32 | await axios.post("/api/auth/signup", { 33 | redirect: false, 34 | name, 35 | email, 36 | password, 37 | }); 38 | 39 | const result = await signIn("credentials", { 40 | redirect: false, 41 | email, 42 | password, 43 | }); 44 | 45 | if (result.error) { 46 | toast.error(result.error); 47 | } 48 | } catch (error) { 49 | toast.error(getError(error)); 50 | } 51 | }; 52 | 53 | return ( 54 | 55 |
59 |

Create Account

60 |
61 | 62 | 71 | {errors.name && ( 72 |
{errors.name.message}
73 | )} 74 |
75 |
76 | 77 | 90 | {errors.email && ( 91 |
{errors.email.message}
92 | )} 93 |
94 |
95 | 96 | 108 | {errors.password && ( 109 |
{errors.password.message}
110 | )} 111 |
112 |
113 | 114 | value === getValues("password"), 119 | minLength: { 120 | value: 6, 121 | message: "confirm password is more than 5 chars", 122 | }, 123 | })} 124 | className="w-full" 125 | id="confirmPassword" 126 | > 127 | 128 | {errors.confirmPassword && ( 129 |
{errors.confirmPassword.message}
130 | )} 131 | 132 | {errors.confirmPassword && 133 | errors.confirmPassword.type === "validate" && ( 134 |
Password do not match
135 | )} 136 |
137 |
138 | 139 |
140 |
141 | Already have an account?   142 | Login 143 |
144 |
145 |
146 | ); 147 | } 148 | -------------------------------------------------------------------------------- /pages/cart.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { XCircleIcon } from "@heroicons/react/24/outline"; 3 | import { Store } from "@/utils/Store"; 4 | import Link from "next/link"; 5 | import React, { useContext } from "react"; 6 | import Image from "next/image"; 7 | import { useRouter } from "next/router"; 8 | import dynamic from "next/dynamic"; 9 | import axios from "axios"; 10 | import { toast } from "react-toastify"; 11 | 12 | function Cart() { 13 | const router = useRouter(); 14 | const { state, dispatch } = useContext(Store); 15 | 16 | const { 17 | cart: { cartItems }, 18 | } = state; 19 | 20 | const removeItemHandler = (item) => { 21 | dispatch({ type: "CART_REMOVE_ITEM", payload: item }); 22 | }; 23 | 24 | const updateCartHandler = async (item, qty) => { 25 | const quantity = Number(qty); 26 | const { data } = await axios.get(`/api/products/${item._id}`); 27 | 28 | if (data.countInStock < quantity) { 29 | toast.error("Sorry. Product is out of stock"); 30 | return; 31 | } 32 | 33 | dispatch({ 34 | type: "CART_ADD_ITEM", 35 | payload: { ...item, quantity: quantity }, 36 | }); 37 | 38 | toast.success("Product updated in the cart"); 39 | }; 40 | 41 | return ( 42 | 43 |

Shopping Cart

44 | {cartItems.length === 0 ? ( 45 |
46 | Cart is empty. Go shopping 47 |
48 | ) : ( 49 |
50 |
51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | {cartItems.map((item) => ( 62 | 63 | 77 | 91 | 92 | 97 | 98 | ))} 99 | 100 |
ItemQuantityPriceAction
64 | 65 | 66 | 73 |  {item.name} 74 | 75 | 76 | 78 | 90 | {item.price}₹ 93 | 96 |
101 |
102 |
103 |
    104 |
  • 105 |
    106 | Subtotal ( 107 | {cartItems.reduce((total, item) => total + item.quantity, 0)}){" "} 108 | :{" "} 109 | {cartItems.reduce( 110 | (total, item) => total + item.quantity * item.price, 111 | 0 112 | )}{" "} 113 | ₹ 114 |
    115 |
  • 116 |
  • 117 | 123 |
  • 124 |
125 |
126 |
127 | )} 128 |
129 | ); 130 | } 131 | 132 | export default dynamic(() => Promise.resolve(Cart), { ssr: false }); 133 | -------------------------------------------------------------------------------- /pages/profile.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import { Store } from "@/utils/Store"; 4 | import axios from "axios"; 5 | import Cookies from "js-cookie"; 6 | import { signIn, signOut, useSession } from "next-auth/react"; 7 | import React, { useContext, useEffect } from "react"; 8 | import { useForm } from "react-hook-form"; 9 | import { toast } from "react-toastify"; 10 | 11 | export default function Profile() { 12 | const { data: session } = useSession(); 13 | 14 | const { 15 | handleSubmit, 16 | register, 17 | getValues, 18 | setValue, 19 | formState: { errors }, 20 | } = useForm(); 21 | 22 | useEffect(() => { 23 | setValue("name", session.user.name); 24 | setValue("email", session.user.email); 25 | }, []); 26 | 27 | const submitHandler = async ({ name, email, password }) => { 28 | try { 29 | await axios.put("/api/auth/update", { 30 | name, 31 | email, 32 | password, 33 | }); 34 | 35 | const result = await signIn("credentials", { 36 | redirect: false, 37 | email, 38 | password, 39 | }); 40 | 41 | toast.success("Profile updated successfully"); 42 | 43 | logoutClickHandler(); 44 | 45 | if (result.error) { 46 | toast.error(result.error); 47 | } 48 | } catch (error) { 49 | toast.error(getError(error)); 50 | } 51 | }; 52 | 53 | const { state, dispatch } = useContext(Store); 54 | 55 | const logoutClickHandler = () => { 56 | Cookies.remove("cart"); 57 | dispatch({ type: "CART_RESET" }); 58 | signOut({ callbackUrl: "/login" }); 59 | }; 60 | 61 | return ( 62 | 63 |
67 |

Updated Profile

68 | 69 |
70 | 71 | 80 | {errors.name && ( 81 |
{errors.name.message}
82 | )} 83 |
84 | 85 |
86 | 87 | 100 | {errors.email && ( 101 |
{errors.email.message}
102 | )} 103 |
104 | 105 |
106 | 107 | 119 | {errors.password && ( 120 |
{errors.password.message}
121 | )} 122 |
123 | 124 |
125 | 126 | value === getValues("password"), 131 | minLength: { 132 | value: 6, 133 | message: "confirm password is more than 5 chars", 134 | }, 135 | })} 136 | className="w-full" 137 | id="confirmPassword" 138 | > 139 | 140 | {errors.confirmPassword && ( 141 |
{errors.confirmPassword.message}
142 | )} 143 | 144 | {errors.confirmPassword && 145 | errors.confirmPassword.type === "validate" && ( 146 |
Password do not match
147 | )} 148 |
149 | 150 |
151 | 152 |
153 |
154 |
155 | ); 156 | } 157 | 158 | Profile.auth = true; 159 | -------------------------------------------------------------------------------- /pages/admin/orders.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import Link from "next/link"; 5 | import React, { useEffect, useReducer } from "react"; 6 | 7 | function reducer(state, action) { 8 | switch (action.type) { 9 | case "FETCH_REQUEST": { 10 | return { ...state, loading: true, error: "" }; 11 | } 12 | case "FETCH_SUCCESS": { 13 | return { ...state, loading: false, orders: action.payload, error: "" }; 14 | } 15 | case "FETCH_FAIL": { 16 | return { ...state, loading: false, error: action.payload }; 17 | } 18 | default: { 19 | return state; 20 | } 21 | } 22 | } 23 | 24 | export default function Orders() { 25 | const [{ loading, error, orders }, dispatch] = useReducer(reducer, { 26 | loading: true, 27 | orders: [], 28 | error: "", 29 | }); 30 | 31 | useEffect(() => { 32 | const fetchData = async () => { 33 | try { 34 | dispatch({ type: "FETCH_REQUEST" }); 35 | const { data } = await axios.get(`/api/admin/orders`); 36 | console.log(data); 37 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 38 | } catch (err) { 39 | dispatch({ type: "FETCH_FAIL", payload: getError(err) }); 40 | } 41 | }; 42 | fetchData(); 43 | }, []); 44 | 45 | console.log(orders); 46 | 47 | return ( 48 | 49 |
50 |
51 |
    52 |
  • 53 | Dashboard 54 |
  • 55 |
  • 56 | 57 | Orders 58 | 59 | {""} 60 | 68 | 73 | 74 |
  • 75 |
  • 76 | Products 77 |
  • 78 |
  • 79 | Users 80 |
  • 81 |
82 |
83 |
84 |

Admin Dashboard

85 | {loading ? ( 86 |
Loading...
87 | ) : error ? ( 88 |
{error}
89 | ) : ( 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | {orders.map((order) => ( 105 | 106 | 107 | 108 | 111 | 112 | 115 | 116 | 121 | 126 | 131 | 132 | ))} 133 | 134 |
IDUSERDATETOTALPAIDDELIVEREDACTION
{order._id.substring(20, 24)} 109 | {order.user ? order.user.name : "DELETED USER"} 110 | 113 | {order.createdAt.substring(0, 10)} 114 | {order.totalPrice} ₹ 117 | {order.isPaid 118 | ? `${order.paidAt.substring(0, 10)}` 119 | : "not paid"} 120 | 122 | {order.isDelivered 123 | ? `${order.deliveredAt.substring(0, 10)}` 124 | : "not delivered"} 125 | 127 | 128 | Details 129 | 130 |
135 |
136 | )} 137 |
138 |
139 |
140 | ); 141 | } 142 | 143 | Orders.auth = { adminOnly: true }; 144 | -------------------------------------------------------------------------------- /pages/admin/dashboard.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import { 5 | BarElement, 6 | CategoryScale, 7 | Chart as ChartJS, 8 | Legend, 9 | LinearScale, 10 | Title, 11 | Tooltip, 12 | } from "chart.js"; 13 | import Link from "next/link"; 14 | import React, { useEffect, useReducer } from "react"; 15 | import { Bar } from "react-chartjs-2"; 16 | 17 | ChartJS.register( 18 | CategoryScale, 19 | LinearScale, 20 | BarElement, 21 | Title, 22 | Tooltip, 23 | Legend 24 | ); 25 | 26 | export const options = { 27 | responsive: true, 28 | plugins: { 29 | legend: { 30 | postion: "top", 31 | }, 32 | }, 33 | }; 34 | 35 | function reducer(state, action) { 36 | switch (action.type) { 37 | case "FETCH_REQUEST": { 38 | return { ...state, loading: true, error: "" }; 39 | } 40 | case "FETCH_SUCCESS": { 41 | return { ...state, loading: false, summay: action.payload, error: "" }; 42 | } 43 | case "FETCH_FAIL": { 44 | return { ...state, loading: false, error: action.payload }; 45 | } 46 | default: { 47 | return state; 48 | } 49 | } 50 | } 51 | 52 | export default function Dashboard() { 53 | const [{ loading, error, summay }, dispatch] = useReducer(reducer, { 54 | loading: true, 55 | summay: { salesDate: [] }, 56 | error: "", 57 | }); 58 | 59 | useEffect(() => { 60 | const fetchData = async () => { 61 | try { 62 | dispatch({ type: "FETCH_REQUEST" }); 63 | const { data } = await axios.get("/api/admin/summary"); 64 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 65 | } catch (error) { 66 | dispatch({ type: "FETCH_FAIL", payload: getError(error) }); 67 | } 68 | }; 69 | fetchData(); 70 | }, []); 71 | 72 | const data = { 73 | labels: summay.salesDate.map((yearAndMont) => yearAndMont._id), 74 | datasets: [ 75 | { 76 | label: "Sales", 77 | backgroundColor: "rgba(162,222,208,1)", 78 | data: summay.salesDate.map((yearAndMont) => yearAndMont.totalSales), 79 | }, 80 | ], 81 | }; 82 | 83 | return ( 84 | 85 |
86 |
87 |
    88 |
  • 89 | Dashboard 90 | {""} 91 | 99 | 104 | 105 |
  • 106 |
  • 107 | Orders 108 |
  • 109 |
  • 110 | Products 111 |
  • 112 |
  • 113 | Users 114 |
  • 115 |
116 |
117 |
118 |

Admin Dashboard

119 | {loading ? ( 120 |
Loading...
121 | ) : error ? ( 122 |
{error}
123 | ) : ( 124 |
125 |
126 |
127 |

{summay.ordersPrice} ₹

128 |

Sales

129 | View sales 130 |
131 | 132 |
133 |

{summay.ordersCount}

134 |

Orders

135 | View orders 136 |
137 | 138 |
139 |

{summay.productsCount}

140 |

Products

141 | View products 142 |
143 | 144 |
145 |

{summay.usersCount}

146 |

Users

147 | View users 148 |
149 |
150 |

Sales Report

151 | 157 |
158 | )} 159 |
160 |
161 |
162 | ); 163 | } 164 | 165 | Dashboard.auth = { adminOnly: true }; 166 | -------------------------------------------------------------------------------- /pages/admin/users.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/router"; 6 | import React, { useEffect, useReducer } from "react"; 7 | import { toast } from "react-toastify"; 8 | 9 | function reducer(state, action) { 10 | switch (action.type) { 11 | case "FETCH_REQUEST": { 12 | return { ...state, loading: true, error: "" }; 13 | } 14 | case "FETCH_SUCCESS": { 15 | return { ...state, loading: false, users: action.payload, error: "" }; 16 | } 17 | case "FETCH_FAIL": { 18 | return { ...state, loading: false, error: action.payload }; 19 | } 20 | 21 | case "DELETE_REQUEST": { 22 | return { ...state, loadingDelete: true }; 23 | } 24 | case "DELETE_SUCCESS": { 25 | return { ...state, loadingDelete: false, successDelete: true }; 26 | } 27 | case "DELETE_FAIL": { 28 | return { ...state, loadingDelete: false }; 29 | } 30 | case "DELETE_RESET": { 31 | return { ...state, loadingDelete: false, successDelete: false }; 32 | } 33 | 34 | default: { 35 | return state; 36 | } 37 | } 38 | } 39 | 40 | export default function Users() { 41 | const [{ loading, error, users, loadingDelete, successDelete }, dispatch] = 42 | useReducer(reducer, { 43 | loading: true, 44 | users: [], 45 | error: "", 46 | }); 47 | 48 | useEffect(() => { 49 | const fetchData = async () => { 50 | try { 51 | dispatch({ type: "FETCH_REQUEST" }); 52 | const { data } = await axios.get(`/api/admin/users`); 53 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 54 | } catch (err) { 55 | dispatch({ type: "FETCH_FAIL", payload: getError(err) }); 56 | } 57 | }; 58 | 59 | if (successDelete) { 60 | dispatch({ type: "DELETE_RESET" }); 61 | } else { 62 | fetchData(); 63 | } 64 | }, [successDelete]); 65 | 66 | const deletHandler = async (userId) => { 67 | if (!window.confirm("Are you sure?")) { 68 | return; 69 | } 70 | try { 71 | dispatch({ type: "DELETE_REQUEST" }); 72 | await axios.delete(`/api/admin/users/${userId}`); 73 | dispatch({ type: "DELETE_SUCCESS" }); 74 | toast.success("User deleted succssfully"); 75 | } catch (error) { 76 | dispatch({ type: "DELETE_FAIL" }); 77 | toast.error(getError(error)); 78 | } 79 | }; 80 | 81 | return ( 82 | 83 |
84 |
85 |
    86 |
  • 87 | Dashboard 88 |
  • 89 |
  • 90 | Orders 91 |
  • 92 |
  • 93 | Products 94 |
  • 95 |
  • 96 | 97 | Users 98 | 99 | {""} 100 | 108 | 113 | 114 |
  • 115 |
116 |
117 |
118 |

Users

119 | {loadingDelete &&
Deleting...
} 120 | {loading ? ( 121 |
Loading...
122 | ) : error ? ( 123 |
{error}
124 | ) : ( 125 |
126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | {users.map((user) => ( 138 | 139 | 140 | 141 | 142 | 143 | 158 | 159 | ))} 160 | 161 |
IDNAMEEMAILADMINACTION
{user._id.substring(20, 24)}{user.name}{user.email}{user.isAdmin ? "YES" : "NO"} 144 | 148 | Edit 149 | 150 |   151 | 157 |
162 |
163 | )} 164 |
165 |
166 |
167 | ); 168 | } 169 | 170 | Users.auth = { adminOnly: true }; 171 | -------------------------------------------------------------------------------- /pages/placeorder.js: -------------------------------------------------------------------------------- 1 | import CheckoutWizard from "@/components/CheckoutWizard"; 2 | import Layout from "@/components/Layout"; 3 | import { getError } from "@/utils/error"; 4 | import { Store } from "@/utils/Store"; 5 | import axios from "axios"; 6 | import Cookies from "js-cookie"; 7 | import Image from "next/image"; 8 | import Link from "next/link"; 9 | import { useRouter } from "next/router"; 10 | import React, { useContext, useEffect, useState } from "react"; 11 | import { toast } from "react-toastify"; 12 | 13 | export default function PlaceOrder() { 14 | const router = useRouter(); 15 | 16 | const { state, dispatch } = useContext(Store); 17 | const { cart } = state; 18 | const { cartItems, shippingAddress, paymentMethod } = cart; 19 | 20 | const itemsPrice = Number( 21 | cartItems 22 | .reduce((total, item) => total + item.quantity * item.price, 0) 23 | .toFixed(2) 24 | ); 25 | 26 | const shippingPrice = itemsPrice < 400 ? 0 : 120; 27 | 28 | const taxPrice = Number((itemsPrice * 0.05).toFixed(2)); 29 | 30 | const totalPrice = Number((itemsPrice + shippingPrice + taxPrice).toFixed(2)); 31 | 32 | useEffect(() => { 33 | if (!paymentMethod) { 34 | router.push("/payment"); 35 | } 36 | }, [paymentMethod, router]); 37 | 38 | const [loading, setLoading] = useState(false); 39 | 40 | const placeOrderHandler = async () => { 41 | try { 42 | setLoading(true); 43 | const { data } = await axios.post("/api/orders", { 44 | orderItems: cartItems, 45 | shippingAddress, 46 | paymentMethod, 47 | itemsPrice, 48 | shippingPrice, 49 | taxPrice, 50 | totalPrice, 51 | }); 52 | 53 | dispatch({ type: "CART_CLEAR_ITEMS" }); 54 | 55 | Cookies.set( 56 | "cart", 57 | JSON.stringify({ 58 | ...cart, 59 | cartItems: [], 60 | }) 61 | ); 62 | 63 | setLoading(false); 64 | router.push(`/order/${data._id}`); 65 | } catch (error) { 66 | setLoading(false); 67 | toast.error(getError(error)); 68 | } 69 | }; 70 | 71 | return ( 72 | 73 | 74 |

Place Order

75 | {cartItems.length === 0 ? ( 76 |
77 | Cart is empty. Go Shopping 78 |
79 | ) : ( 80 |
81 |
82 |
83 |

Shipping Address

84 |
85 | {shippingAddress.fullName},{shippingAddress.address},{" "} 86 | {shippingAddress.city},{shippingAddress.postalCode},{" "} 87 | {shippingAddress.country} 88 |
89 |
90 | Edit 91 |
92 |
93 | 94 |
95 |

Payment Method

96 |
{paymentMethod}
97 |
98 | Edit 99 |
100 |
101 | 102 |
103 |

Order Items

104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {cartItems.map((item) => ( 115 | 116 | 130 | 131 | 132 | 135 | 136 | ))} 137 | 138 |
ItemQuantityPriceAction
117 | 118 | 119 | 126 |  {item.name} 127 | 128 | 129 | {item.quantity}{item.price}₹ 133 | {item.quantity * item.price}₹ 134 |
139 |
140 | Edit 141 |
142 |
143 |
144 |
145 |

Order Summary

146 |
    147 |
  • 148 |
    149 |
    Items
    150 |
    {itemsPrice}₹
    151 |
    152 |
  • 153 |
  • 154 |
    155 |
    Tax
    156 |
    {taxPrice}₹
    157 |
    158 |
  • 159 |
  • 160 |
    161 |
    Shipping
    162 |
    {shippingPrice}₹
    163 |
    164 |
  • 165 |
  • 166 |
    167 |
    Total
    168 |
    {totalPrice}₹
    169 |
    170 |
  • 171 |
  • 172 | 179 |
  • 180 |
181 |
182 |
183 | )} 184 |
185 | ); 186 | } 187 | 188 | PlaceOrder.auth = true; 189 | -------------------------------------------------------------------------------- /pages/admin/products.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/router"; 6 | import React, { useEffect, useReducer } from "react"; 7 | import { toast } from "react-toastify"; 8 | 9 | function reducer(state, action) { 10 | switch (action.type) { 11 | case "FETCH_REQUEST": { 12 | return { ...state, loading: true, error: "" }; 13 | } 14 | case "FETCH_SUCCESS": { 15 | return { ...state, loading: false, products: action.payload, error: "" }; 16 | } 17 | case "FETCH_FAIL": { 18 | return { ...state, loading: false, error: action.payload }; 19 | } 20 | 21 | case "CREATE_REQUEST": { 22 | return { ...state, loadingCreate: true }; 23 | } 24 | case "CREATE_SUCCESS": { 25 | return { ...state, loadingCreate: false }; 26 | } 27 | case "CREATE_FAIL": { 28 | return { ...state, loadingCreate: false }; 29 | } 30 | 31 | case "DELETE_REQUEST": { 32 | return { ...state, loadingDelete: true }; 33 | } 34 | case "DELETE_SUCCESS": { 35 | return { ...state, loadingDelete: false, successDelete: true }; 36 | } 37 | case "DELETE_FAIL": { 38 | return { ...state, loadingDelete: false }; 39 | } 40 | case "DELETE_RESET": { 41 | return { ...state, loadingDelete: false, successDelete: false }; 42 | } 43 | 44 | default: { 45 | return state; 46 | } 47 | } 48 | } 49 | 50 | export default function Products() { 51 | const [ 52 | { loading, error, products, loadingCreate, loadingDelete, successDelete }, 53 | dispatch, 54 | ] = useReducer(reducer, { 55 | loading: true, 56 | products: [], 57 | error: "", 58 | successDelete: false, 59 | }); 60 | 61 | const router = useRouter(); 62 | 63 | useEffect(() => { 64 | const fetchData = async () => { 65 | try { 66 | dispatch({ type: "FETCH_REQUEST" }); 67 | const { data } = await axios.get(`/api/admin/products`); 68 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 69 | } catch (err) { 70 | dispatch({ type: "FETCH_FAIL", payload: getError(err) }); 71 | } 72 | }; 73 | 74 | if (successDelete) { 75 | dispatch({ type: "DELETE_RESET" }); 76 | } else { 77 | fetchData(); 78 | } 79 | }, [successDelete]); 80 | 81 | const createHandler = async () => { 82 | if (!window.confirm("Are you sure")) { 83 | return; 84 | } 85 | try { 86 | dispatch({ type: "CREATE_REQUEST" }); 87 | const { data } = await axios.post(`/api/admin/products`); 88 | dispatch({ type: "CREATE_SUCCESS" }); 89 | toast.success("Product created succssfully"); 90 | router.push(`/admin/product/${data.product._id}`); 91 | } catch (error) { 92 | dispatch({ type: "CREATE_FAIL" }); 93 | toast.error(getError(error)); 94 | } 95 | }; 96 | 97 | const deletHandler = async (productId) => { 98 | if (!window.confirm("Are you sure?")) { 99 | return; 100 | } 101 | try { 102 | dispatch({ type: "DELETE_REQUEST" }); 103 | await axios.delete(`/api/admin/products/${productId}`); 104 | dispatch({ type: "DELETE_SUCCESS" }); 105 | toast.success("Product deleted succssfully"); 106 | } catch (error) { 107 | dispatch({ type: "DELETE_FAIL" }); 108 | toast.error(getError(error)); 109 | } 110 | }; 111 | 112 | return ( 113 | 114 |
115 |
116 |
    117 |
  • 118 | Dashboard 119 |
  • 120 |
  • 121 | Orders 122 |
  • 123 |
  • 124 | 125 | Products 126 | 127 | {""} 128 | 136 | 141 | 142 |
  • 143 |
  • 144 | Users 145 |
  • 146 |
147 |
148 |
149 |
150 |

Admin Dashboard

151 | {loadingDelete &&
Deleting item...
} 152 | 159 |
160 | 161 | {loading ? ( 162 |
Loading...
163 | ) : error ? ( 164 |
{error}
165 | ) : ( 166 |
167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | {products.map((product) => ( 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 204 | 205 | ))} 206 | 207 |
IDNAMEPRICECATEGORYCOUNTRATINGACTION
{product._id.substring(20, 24)}{product.name}{product.price}{product.category} ₹{product.countInStock}{product.rating} 189 | 193 | Edit 194 | 195 |   196 | 203 |
208 |
209 | )} 210 |
211 |
212 |
213 | ); 214 | } 215 | 216 | Products.auth = { adminOnly: true }; 217 | -------------------------------------------------------------------------------- /pages/search.js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import Product from "@/models/Product"; 3 | import db from "@/utils/db"; 4 | import { Store } from "@/utils/Store"; 5 | import axios from "axios"; 6 | import { useRouter } from "next/router"; 7 | import React, { useContext } from "react"; 8 | import { toast } from "react-toastify"; 9 | import { XCircleIcon } from "@heroicons/react/24/outline"; 10 | import ProductItem from "@/components/ProductItem"; 11 | 12 | const PAGE_SIZE = 3; 13 | 14 | const prices = [ 15 | { 16 | name: "200₹ to 600₹", 17 | value: "200-600", 18 | }, 19 | { 20 | name: "601₹ to 1200₹", 21 | value: "601-1200", 22 | }, 23 | { 24 | name: "1201₹ to 1600₹", 25 | value: "1201-1600", 26 | }, 27 | ]; 28 | 29 | const ratings = [1, 2, 3, 4, 5]; 30 | 31 | export default function Search(props) { 32 | const { countProducts, products, categories, brands, pages } = props; 33 | 34 | const router = useRouter(); 35 | 36 | const { 37 | query = "all", 38 | category = "all", 39 | brand = "all", 40 | price = "all", 41 | rating = "all", 42 | sort = "featured", 43 | page = 1, 44 | } = router.query; 45 | 46 | const { state, dispatch } = useContext(Store); 47 | const { cart } = state; 48 | 49 | const addToCartHandler = async (product) => { 50 | const existItem = cart.cartItems.find((item) => item.slug === product.slug); 51 | const quantity = existItem ? existItem.quantity + 1 : 1; 52 | 53 | const { data } = await axios.get(`/api/products/${product._id}`); 54 | 55 | if (data.countInStock < quantity) { 56 | toast.error("Sorry. Product is out of stock"); 57 | return; 58 | } 59 | 60 | dispatch({ 61 | type: "CART_ADD_ITEM", 62 | payload: { ...product, quantity: quantity }, 63 | }); 64 | 65 | toast.success("Product added to the cart"); 66 | }; 67 | 68 | const filterSearch = ({ 69 | category, 70 | page, 71 | brand, 72 | sort, 73 | min, 74 | max, 75 | searchQuery, 76 | price, 77 | rating, 78 | }) => { 79 | const { query } = router; 80 | if (page) query.page = page; 81 | if (searchQuery) query.searchQuery = searchQuery; 82 | if (sort) query.sort = sort; 83 | if (category) query.category = category; 84 | if (brand) query.brand = brand; 85 | if (price) query.price = price; 86 | if (rating) query.rating = rating; 87 | if (min) query.min ? query.min : query.min === 0 ? 0 : min; 88 | if (max) query.max ? query.max : query.max === 0 ? 0 : max; 89 | 90 | router.push({ 91 | pathname: router.pathname, 92 | query: query, 93 | }); 94 | }; 95 | 96 | const categoryHandler = (e) => { 97 | filterSearch({ category: e.target.value }); 98 | }; 99 | 100 | const pageHandler = (page) => { 101 | filterSearch({ page: page }); 102 | }; 103 | 104 | const brandHandler = (e) => { 105 | filterSearch({ brand: e.target.value }); 106 | }; 107 | 108 | const sortHandler = (e) => { 109 | filterSearch({ sort: e.target.value }); 110 | }; 111 | 112 | const priceHandler = (e) => { 113 | filterSearch({ price: e.target.value }); 114 | }; 115 | 116 | const ratingHandler = (e) => { 117 | filterSearch({ rating: e.target.value }); 118 | }; 119 | 120 | return ( 121 | 122 |
123 |
124 |
125 |

Categories

126 | 139 |
140 | 141 |
142 |

Brands

143 | 152 |
153 | 154 |
155 |

Prices

156 | 165 |
166 | 167 |
168 |

Rating

169 | 178 |
179 |
180 |
181 |
182 |
183 | {products.length === 0 ? "No" : countProducts} 184 | Results 185 | {query !== "all" && query !== "" && " : " + query} 186 | {category !== "all" && category !== "" && " : " + category} 187 | {brand !== "all" && brand !== "" && " : " + brand} 188 | {price !== "all" && price !== "" && " : " + price} 189 | {rating !== "all" && rating !== "" && " : " + rating} 190 |   191 | {(query !== "all" && query !== "") || 192 | category !== "all" || 193 | brand !== "all" || 194 | rating !== "all" || 195 | price !== "all" ? ( 196 | 199 | ) : null} 200 |
201 |
202 | Sort by{" "} 203 | 210 |
211 |
212 |
213 |
214 | {products.map((product) => ( 215 | 220 | ))} 221 |
222 |
    223 | {products.length > 0 && 224 | [...Array(pages).keys()].map((pageNumber) => ( 225 |
  • 226 | 234 |
  • 235 | ))} 236 |
237 |
238 |
239 |
240 |
241 | ); 242 | } 243 | 244 | export async function getServerSideProps({ query }) { 245 | const pageSize = query.pageSize || PAGE_SIZE; 246 | const page = query.page || 1; 247 | const category = query.category || ""; 248 | const brand = query.brand || ""; 249 | const price = query.price || ""; 250 | const rating = query.rating || ""; 251 | const sort = query.sort || ""; 252 | const searchQuery = query.query || ""; 253 | 254 | const queryFilter = 255 | searchQuery && searchQuery !== "all" 256 | ? { 257 | name: { 258 | $regex: searchQuery, 259 | $options: "i", 260 | }, 261 | } 262 | : {}; 263 | const categoryFilter = 264 | category && category !== "all" 265 | ? { 266 | category, 267 | } 268 | : {}; 269 | 270 | const brandFilter = 271 | brand && brand !== "all" 272 | ? { 273 | brand, 274 | } 275 | : {}; 276 | 277 | const ratingFilter = 278 | rating && rating !== "all" 279 | ? { 280 | rating: { 281 | $gte: Number(rating), 282 | }, 283 | } 284 | : {}; 285 | 286 | const priceFilter = 287 | price && price !== "all" 288 | ? { 289 | price: { 290 | $gte: Number(price.split("-")[0]), 291 | $lte: Number(price.split("-")[1]), 292 | }, 293 | } 294 | : {}; 295 | 296 | const order = 297 | sort === "featured" 298 | ? { isFeatured: -1 } 299 | : sort === "lowest" 300 | ? { price: 1 } 301 | : sort === "highest" 302 | ? { price: -1 } 303 | : sort === "toprated" 304 | ? { rating: -1 } 305 | : sort === "newest" 306 | ? { createdAt: -1 } 307 | : { _id: -1 }; 308 | 309 | await db.connect(); 310 | const categories = await Product.find().distinct("category"); 311 | const brands = await Product.find().distinct("brand"); 312 | const productDocs = await Product.find({ 313 | ...queryFilter, 314 | ...categoryFilter, 315 | ...priceFilter, 316 | ...brandFilter, 317 | ...ratingFilter, 318 | }) 319 | .sort(order) 320 | .skip(pageSize * (page - 1)) 321 | .limit(pageSize) 322 | .lean(); 323 | 324 | const countProducts = await Product.countDocuments({ 325 | ...queryFilter, 326 | ...categoryFilter, 327 | ...priceFilter, 328 | ...brandFilter, 329 | ...ratingFilter, 330 | }); 331 | 332 | const products = productDocs.map(db.convertDocToObj); 333 | 334 | await db.disconnect(); 335 | 336 | return { 337 | props: { 338 | products, 339 | countProducts, 340 | page, 341 | pages: Math.ceil(countProducts / pageSize), 342 | categories, 343 | brands, 344 | }, 345 | }; 346 | } 347 | -------------------------------------------------------------------------------- /pages/order/[id].js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import { PayPalButtons, usePayPalScriptReducer } from "@paypal/react-paypal-js"; 4 | import axios from "axios"; 5 | import { useSession } from "next-auth/react"; 6 | import Image from "next/image"; 7 | import Link from "next/link"; 8 | import { useRouter } from "next/router"; 9 | import React, { useEffect, useReducer } from "react"; 10 | import { toast } from "react-toastify"; 11 | 12 | function reducer(state, action) { 13 | switch (action.type) { 14 | case "FETCH_REQUEST": { 15 | return { ...state, loading: true, error: "" }; 16 | } 17 | case "FETCH_SUCCESS": { 18 | return { ...state, loading: false, order: action.payload, error: "" }; 19 | } 20 | case "FETCH_FAIL": { 21 | return { ...state, loading: false, error: action.payload }; 22 | } 23 | 24 | case "PAY_REQUEST": { 25 | return { ...state, loadingPay: true }; 26 | } 27 | case "PAY_SUCCESS": { 28 | return { ...state, loadingPay: false, successPay: true }; 29 | } 30 | case "PAY_FAIL": { 31 | return { ...state, loadingPay: false, errorPay: action.payload }; 32 | } 33 | case "PAY_RESET": { 34 | return { ...state, loadingPay: false, errorPay: "" }; 35 | } 36 | 37 | case "DELIVER_REQUEST": { 38 | return { ...state, loadingDeliver: true, successDeliver: false }; 39 | } 40 | case "DELIVER_SUCCESS": { 41 | return { ...state, loadingDeliver: false, successDeliver: true }; 42 | } 43 | case "DELIVER_FAIL": { 44 | return { ...state, loadingDeliver: false }; 45 | } 46 | case "DELIVER_RESET": { 47 | return { ...state, loadingDeliver: false, successDeliver: false }; 48 | } 49 | 50 | default: { 51 | return state; 52 | } 53 | } 54 | } 55 | 56 | export default function Order() { 57 | const [{ isPending }, paypalDispatch] = usePayPalScriptReducer(); 58 | 59 | const { data: session } = useSession(); 60 | 61 | const { query } = useRouter(); 62 | const orderId = query.id; 63 | 64 | const [ 65 | { 66 | loading, 67 | error, 68 | order, 69 | successPay, 70 | loadingPay, 71 | errorPay, 72 | loadingDeliver, 73 | successDeliver, 74 | }, 75 | dispatch, 76 | ] = useReducer(reducer, { 77 | loading: true, 78 | order: {}, 79 | error: "", 80 | }); 81 | 82 | useEffect(() => { 83 | const fetchOrder = async () => { 84 | try { 85 | dispatch({ type: "FETCH_REQUEST" }); 86 | const { data } = await axios.get(`/api/orders/${orderId}`); 87 | dispatch({ type: "FETCH_SUCCESS", payload: data }); 88 | } catch (error) { 89 | dispatch({ type: "FETCH_FAIL", payload: getError(error) }); 90 | } 91 | }; 92 | 93 | if ( 94 | !order._id || 95 | successPay || 96 | successDeliver || 97 | (order._id && order._id != orderId) 98 | ) { 99 | fetchOrder(); 100 | if (successPay) { 101 | dispatch({ type: "PAY_RESET" }); 102 | } 103 | if (successDeliver) { 104 | dispatch({ type: "DELIVER_RESET" }); 105 | } 106 | } else { 107 | const loadPaypalScript = async () => { 108 | const { data: clientId } = await axios.get("/api/keys/paypal"); 109 | 110 | paypalDispatch({ 111 | type: "resetOptions", 112 | value: { 113 | "client-id": clientId, 114 | currency: "USD", 115 | }, 116 | }); 117 | 118 | paypalDispatch({ 119 | type: "setLoadingStatus", 120 | value: "pending", 121 | }); 122 | }; 123 | loadPaypalScript(); 124 | } 125 | }, [order, orderId, paypalDispatch, successPay, successDeliver]); 126 | 127 | const { 128 | shippingAddress, 129 | paymentMethod, 130 | orderItems, 131 | itemsPrice, 132 | taxPrice, 133 | shippingPrice, 134 | totalPrice, 135 | isPaid, 136 | paidAt, 137 | isDelivered, 138 | deliveredAt, 139 | } = order; 140 | 141 | const createOrder = (data, actions) => { 142 | return actions.order 143 | .create({ 144 | purchase_units: [ 145 | { 146 | amount: { value: totalPrice }, 147 | }, 148 | ], 149 | }) 150 | .then((orderId) => { 151 | return orderId; 152 | }); 153 | }; 154 | 155 | const onApprove = (data, actions) => { 156 | return actions.order.capture().then(async function (details) { 157 | try { 158 | dispatch({ type: "PAY_REQUEST" }); 159 | const { data } = await axios.put( 160 | `api/orders/${order._id}/pay`, 161 | details 162 | ); 163 | 164 | dispatch({ type: "PAY_SUCCESS", payload: data }); 165 | toast.success("Order is paid successfully"); 166 | } catch (error) { 167 | dispatch({ type: "PAY_FAIL", payload: getError(error) }); 168 | toast.error(getError(error)); 169 | } 170 | }); 171 | }; 172 | 173 | const onError = (error) => { 174 | toast.error(getError(error)); 175 | }; 176 | 177 | const deliverOrderHandler = async () => { 178 | try { 179 | dispatch({ type: "DELIVER_REQUEST" }); 180 | const { data } = await axios.put( 181 | `/api/admin/orders/${order._id}/deliver`, 182 | {} 183 | ); 184 | dispatch({ type: "DELIVER_SUCCESS", payload: data }); 185 | toast.success("Order is delivered"); 186 | } catch (error) { 187 | dispatch({ type: "DELIVER_FAIL", payload: getError(error) }); 188 | toast.error(getError(error)); 189 | } 190 | }; 191 | 192 | return ( 193 | 194 |

{`Order ${orderId}`}

195 | {loading ? ( 196 |
Loading...
197 | ) : error ? ( 198 |
{error}
199 | ) : ( 200 |
201 |
202 |
203 |

Shipping Address

204 |
205 | {shippingAddress.fullName},{shippingAddress.address},{" "} 206 | {shippingAddress.city},{shippingAddress.postalCode},{" "} 207 | {shippingAddress.country} 208 |
209 | {isDelivered ? ( 210 |
Delivered at {deliveredAt}
211 | ) : ( 212 |
Not Delivered
213 | )} 214 |
215 | 216 |
217 |

Payment Method

218 |
{paymentMethod}
219 | 220 | {isPaid ? ( 221 |
Paid at {paidAt}
222 | ) : ( 223 |
Not Paid
224 | )} 225 |
226 | 227 |
228 |

Order Items

229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | {orderItems.map((item) => ( 240 | 241 | 254 | 255 | 256 | 259 | 260 | ))} 261 | 262 |
ItemQuantityPriceAction
242 | 243 | 244 | 250 |  {item.name} 251 | 252 | 253 | {item.quantity}{item.price}₹ 257 | {item.quantity * item.price}₹ 258 |
263 |
264 |
265 |
266 |
267 |

Order Summary

268 |
    269 |
  • 270 |
    271 |
    Items
    272 |
    {itemsPrice}₹
    273 |
    274 |
  • 275 |
  • 276 |
    277 |
    Tax
    278 |
    {taxPrice}₹
    279 |
    280 |
  • 281 |
  • 282 |
    283 |
    Shipping
    284 |
    {shippingPrice}₹
    285 |
    286 |
  • 287 |
  • 288 |
    289 |
    Total
    290 |
    {totalPrice}₹
    291 |
    292 |
  • 293 | {!isPaid && ( 294 |
  • 295 | {isPending ? ( 296 |
    Loading...
    297 | ) : ( 298 |
    299 | 304 |
    305 | )} 306 | {loadingPay &&
    Loading...
    } 307 |
  • 308 | )} 309 | {session.user.isAdmin && order.isPaid && !order.isDelivered && ( 310 |
  • 311 | {loadingDeliver &&
    Loading...
    } 312 | 318 |
  • 319 | )} 320 |
321 |
322 |
323 |
324 | )} 325 |
326 | ); 327 | } 328 | 329 | Order.auth = true; 330 | -------------------------------------------------------------------------------- /pages/admin/product/[id].js: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/Layout"; 2 | import { getError } from "@/utils/error"; 3 | import axios from "axios"; 4 | import Link from "next/link"; 5 | import { useRouter } from "next/router"; 6 | import React, { useEffect, useReducer } from "react"; 7 | import { useForm } from "react-hook-form"; 8 | import { toast } from "react-toastify"; 9 | 10 | function reducer(state, action) { 11 | switch (action.type) { 12 | case "FETCH_REQUEST": { 13 | return { ...state, loading: true, error: "" }; 14 | } 15 | case "FETCH_SUCCESS": { 16 | return { ...state, loading: false, error: "" }; 17 | } 18 | case "FETCH_FAIL": { 19 | return { ...state, loading: false, error: action.payload }; 20 | } 21 | 22 | case "UPDATE_REQUEST": { 23 | return { ...state, loadingUpdate: true, errorUpdate: "" }; 24 | } 25 | case "UPDATE_SUCCESS": { 26 | return { ...state, loadingUpdate: false, errorUpdate: "" }; 27 | } 28 | case "UPDATE_FAIL": { 29 | return { ...state, loadingUpdate: false, errorUpdate: action.payload }; 30 | } 31 | 32 | case "UPLOAD_REQUEST": { 33 | return { ...state, loadingUpload: true, errorUpload: "" }; 34 | } 35 | case "UPLOAD_SUCCESS": { 36 | return { ...state, loadingUpload: false, errorUpload: "" }; 37 | } 38 | case "UPLOAD_FAIL": { 39 | return { ...state, loadingUpload: false, errorUpload: action.payload }; 40 | } 41 | 42 | default: { 43 | return state; 44 | } 45 | } 46 | } 47 | 48 | export default function ProductEditScreen() { 49 | const { query } = useRouter(); 50 | const productId = query.id; 51 | const [{ loading, error, loadingUpdate, loadingUpload }, dispatch] = 52 | useReducer(reducer, { 53 | loading: true, 54 | error: "", 55 | }); 56 | 57 | const { 58 | register, 59 | handleSubmit, 60 | setValue, 61 | formState: { errors }, 62 | } = useForm(); 63 | 64 | useEffect(() => { 65 | const fetchData = async () => { 66 | try { 67 | dispatch({ type: "FETCH_REQUEST" }); 68 | const { data } = await axios.get(`/api/admin/products/${productId}`); 69 | dispatch({ type: "FETCH_SUCCESS" }); 70 | setValue("name", data.name); 71 | setValue("slug", data.slug); 72 | setValue("price", data.price); 73 | setValue("image", data.image); 74 | setValue("category", data.category); 75 | setValue("brand", data.brand); 76 | setValue("countInStock", data.countInStock); 77 | setValue("description", data.description); 78 | } catch (error) { 79 | dispatch({ type: "FETCH_FAIL", payload: getError(error) }); 80 | } 81 | }; 82 | fetchData(); 83 | }, [productId, setValue]); 84 | 85 | const router = useRouter(); 86 | 87 | const submitHandler = async ({ 88 | name, 89 | slug, 90 | price, 91 | category, 92 | image, 93 | brand, 94 | countInStock, 95 | description, 96 | }) => { 97 | try { 98 | dispatch({ type: "UPDATE_REQUEST" }); 99 | await axios.put(`/api/admin/products/${productId}`, { 100 | name, 101 | slug, 102 | price, 103 | category, 104 | image, 105 | brand, 106 | countInStock, 107 | description, 108 | }); 109 | dispatch({ type: "UPDATE_SUCCESS" }); 110 | toast.success("Product updated successfully"); 111 | router.push("/admin/products"); 112 | } catch (error) { 113 | dispatch({ type: "UPDATE_FAUL", payload: getError(error) }); 114 | toast.error(getError(error)); 115 | } 116 | }; 117 | 118 | const uploadHandler = async (e, imageField = "image") => { 119 | const url = `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/upload`; 120 | 121 | try { 122 | dispatch({ type: "UPLOAD_REQUEST" }); 123 | const { 124 | data: { signature, timestamp }, 125 | } = await axios("/api/admin/cloudinary-sign"); 126 | 127 | const file = e.target.files[0]; 128 | 129 | const formData = new FormData(); 130 | 131 | formData.append("file", file); 132 | formData.append("signature", signature); 133 | formData.append("timestamp", timestamp); 134 | formData.append("api_key", process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY); 135 | 136 | const { data } = await axios.post(url, formData); 137 | 138 | dispatch({ type: "UPLOAD_SUCCESS" }); 139 | 140 | setValue(imageField, data.secure_url); 141 | 142 | console.log("image url is:-", data.secure_url); 143 | 144 | toast.success("File uploaded successfully"); 145 | } catch (error) { 146 | dispatch({ type: "UPLOAD_FAIL", payload: getError(error) }); 147 | toast.error(getError(error)); 148 | } 149 | }; 150 | 151 | return ( 152 | 153 |
154 |
155 |
    156 |
  • 157 | Dashboard 158 |
  • 159 |
  • 160 | Orders 161 |
  • 162 |
  • 163 | 164 | Products 165 | 166 | {""} 167 | 175 | 180 | 181 |
  • 182 |
  • 183 | Users 184 |
  • 185 |
186 |
187 |
188 | {loading ? ( 189 |
Loading...
190 | ) : error ? ( 191 |
{error}
192 | ) : ( 193 |
197 |

{`Edit Product ${productId}`}

198 | 199 |
200 | 201 | 210 | {errors.name && ( 211 |
{errors.name.message}
212 | )} 213 |
214 | 215 |
216 | 217 | 225 | {errors.slug && ( 226 |
{errors.slug.message}
227 | )} 228 |
229 | 230 |
231 | 232 | 240 | {errors.price && ( 241 |
{errors.price.message}
242 | )} 243 |
244 | 245 |
246 | 247 | 253 | 254 | {loadingUpload &&
Uploading....
} 255 |
256 | 257 |
258 | 259 | 267 | {errors.category && ( 268 |
{errors.category.message}
269 | )} 270 |
271 | 272 |
273 | 274 | 282 | {errors.brand && ( 283 |
{errors.brand.message}
284 | )} 285 |
286 | 287 |
288 | 289 | 297 | {errors.countInStock && ( 298 |
299 | {errors.countInStock.message} 300 |
301 | )} 302 |
303 | 304 |
305 | 306 | 314 | {errors.description && ( 315 |
316 | {errors.description.message} 317 |
318 | )} 319 |
320 |
321 | 324 |
325 |
326 | Back 327 |
328 |
329 | )} 330 |
331 |
332 |
333 | ); 334 | } 335 | 336 | ProductEditScreen.auth = { adminOnly: true }; 337 | -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import { Store } from "@/utils/Store"; 2 | import { Menu } from "@headlessui/react"; 3 | import Cookies from "js-cookie"; 4 | import { signOut, useSession } from "next-auth/react"; 5 | import Head from "next/head"; 6 | import Link from "next/link"; 7 | import { useRouter } from "next/router"; 8 | import React, { useContext, useEffect, useState } from "react"; 9 | import { ToastContainer } from "react-toastify"; 10 | import "react-toastify/dist/ReactToastify.css"; 11 | import DropdownLink from "./DropdownLink"; 12 | 13 | function Layout({ title, children }) { 14 | const router = useRouter(); 15 | const { status, data: session } = useSession(); 16 | 17 | const { state, dispatch } = useContext(Store); 18 | const { cart } = state; 19 | 20 | const [cardItemsCount, setCardItemsCount] = useState(0); 21 | 22 | useEffect(() => { 23 | setCardItemsCount(cart.cartItems.reduce((a, c) => a + c.quantity, 0)); 24 | }, [cart.cartItems]); 25 | 26 | const logoutClickHandler = () => { 27 | Cookies.remove("cart"); 28 | dispatch({ type: "CART_RESET" }); 29 | signOut({ callbackUrl: "/login" }); 30 | }; 31 | 32 | const [query, setQuery] = useState(""); 33 | 34 | const submitHandler = (e) => { 35 | e.preventDefault(); 36 | router.push(`/search/?query=${query}`); 37 | }; 38 | 39 | const [toggle, setToggle] = useState(false); 40 | 41 | return ( 42 | <> 43 | 44 | {title ? title : "MyShop"} 45 | 46 | 47 | 48 | 49 | 50 |
51 |
52 | 175 |
181 |
185 | setQuery(e.target.value)} 187 | type="text" 188 | className="rounded-tr-none rounded-br-none p-1.5 text-sm focus:ring-0" 189 | placeholder="Search products" 190 | /> 191 | 209 |
210 | 211 |
212 |
213 | 214 | Cart 215 | {cardItemsCount > 0 ? ( 216 | 217 | {cardItemsCount} 218 | 219 | ) : null} 220 | 221 |
222 | {status === "loading" ? ( 223 | "Loading" 224 | ) : session?.user ? ( 225 | 226 | 227 | {session.user.name} 228 | 237 | 242 | 243 | 244 | 245 | 246 | Profile 247 | 248 | 249 | 250 | 251 | Order History 252 | 253 | 254 | 255 | {session.user.isAdmin && ( 256 | 257 | 258 | Admin Dashboard 259 | 260 | 261 | )} 262 | 263 | 264 | 265 | Logout 266 | 267 | 268 | 269 | 270 | ) : ( 271 | 272 | Login 273 | 274 | )} 275 |
276 |
277 |
278 |
279 | {children} 280 |
281 | 284 |
285 | 286 | ); 287 | } 288 | 289 | export default Layout; 290 | --------------------------------------------------------------------------------