├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── (front) │ ├── cart │ │ ├── CartDetails.tsx │ │ └── page.tsx │ ├── layout.tsx │ ├── loading.tsx │ ├── order-history │ │ ├── MyOrders.tsx │ │ └── page.tsx │ ├── order │ │ └── [id] │ │ │ ├── OrderDetails.tsx │ │ │ └── page.tsx │ ├── page.tsx │ ├── payment │ │ ├── Form.tsx │ │ └── page.tsx │ ├── place-order │ │ ├── Form.tsx │ │ └── page.tsx │ ├── product │ │ └── [slug] │ │ │ └── page.tsx │ ├── profile │ │ ├── Form.tsx │ │ └── page.tsx │ ├── register │ │ ├── Form.tsx │ │ └── page.tsx │ ├── search │ │ └── page.tsx │ ├── shipping │ │ ├── Form.tsx │ │ └── page.tsx │ └── signin │ │ ├── Form.tsx │ │ └── page.tsx ├── admin │ ├── dashboard │ │ ├── Dashboard.tsx │ │ └── page.tsx │ ├── orders │ │ ├── Orders.tsx │ │ └── page.tsx │ ├── products │ │ ├── Products.tsx │ │ ├── [id] │ │ │ ├── Form.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ └── users │ │ ├── Users.tsx │ │ ├── [id] │ │ ├── Form.tsx │ │ └── page.tsx │ │ └── page.tsx ├── api │ ├── admin │ │ ├── orders │ │ │ ├── [id] │ │ │ │ └── deliver │ │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── products │ │ │ ├── [id] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── summary │ │ │ └── route.ts │ │ └── users │ │ │ ├── [id] │ │ │ └── route.ts │ │ │ └── route.ts │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.ts │ │ ├── profile │ │ │ └── route.ts │ │ └── register │ │ │ └── route.ts │ ├── cloudinary-sign │ │ └── route.ts │ ├── orders │ │ ├── [id] │ │ │ ├── capture-paypal-order │ │ │ │ └── route.ts │ │ │ ├── create-paypal-order │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── mine │ │ │ └── route.ts │ │ └── route.ts │ └── products │ │ ├── categories │ │ └── route.ts │ │ └── seed │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx └── not-found.tsx ├── components.json ├── components ├── ClientProvider.tsx ├── DrawerButton.tsx ├── Providers.tsx ├── Sidebar.tsx ├── Wrapper.tsx ├── admin │ └── AdminLayout.tsx ├── carousel │ └── carousel.tsx ├── categories │ ├── Categories.tsx │ └── Overlay.tsx ├── checkout │ └── CheckoutSteps.tsx ├── footer │ └── Footer.tsx ├── header │ ├── Header.tsx │ ├── Menu.tsx │ └── SearchBox.tsx ├── icons │ └── Icons.tsx ├── products │ ├── AddToCart.tsx │ ├── ProductItem.tsx │ ├── ProductItems.tsx │ └── Rating.tsx ├── readMore │ ├── ReadMore.tsx │ └── Text.tsx ├── slider │ ├── CardSlider.tsx │ └── Slider.tsx └── ui │ ├── button.tsx │ └── carousel.tsx ├── lib ├── auth.ts ├── data.ts ├── dbConnect.ts ├── hooks │ ├── useCartStore.ts │ └── useLayout.ts ├── models │ ├── OrderModel.ts │ ├── ProductModel.ts │ └── UserModel.ts ├── paypal.ts ├── services │ └── productService.ts └── utils.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prettier.config.js ├── public ├── Logo.svg ├── images │ ├── banner │ │ ├── banner1.webp │ │ └── banner2.webp │ └── categories │ │ ├── Handbags.webp │ │ ├── Pants.webp │ │ └── Shirts.webp ├── next.svg ├── readme │ ├── Fashion-Corner-Fullstack-Next-js-Store-dark.webp │ └── Fashion-Corner-Fullstack-Next-js-Store.webp └── vercel.svg ├── tailwind.config.ts ├── tsconfig.json └── types └── next-auth.d.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "plugins": ["import"], 4 | "rules": { 5 | "import/order": [ 6 | "error", 7 | { 8 | "newlines-between": "always", 9 | "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], 10 | "alphabetize": { 11 | "order": "asc", 12 | "caseInsensitive": true 13 | } 14 | } 15 | ] 16 | } 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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FashionCorner - Next.js 14 Fullstack Ecommerce App 2 | 3 | Features: 4 | 5 | - Tailwind design + DaisyUI, Shadcn/ui 6 | - Typescript 7 | - MongoDB integration 8 | - Cloudinary integration 9 | - NProgress integration 10 | - React Hook Form 11 | - Admin dashboard 12 | - SEO Friendly Application - (SSR) 13 | - Plaiceholder blurred images 14 | - PayPay integration (Stripe will be added soon) 15 | - Server Side Pagination 16 | 17 | ## [Visit project url](https://fashion-corner.vercel.app/) 18 | 19 |

20 | Next.js 14 Fullstack Ecommerce App - Home page light mode 21 |

22 | 23 |

24 | Next.js 14 Fullstack Ecommerce App - Home page dark mode 25 |

26 | -------------------------------------------------------------------------------- /app/(front)/cart/CartDetails.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useEffect, useState } from 'react'; 7 | 8 | import useCartService from '@/lib/hooks/useCartStore'; 9 | 10 | const CartDetails = () => { 11 | const { items, itemsPrice, decrease, increase } = useCartService(); 12 | const [mounted, setMounted] = useState(false); 13 | const router = useRouter(); 14 | 15 | useEffect(() => { 16 | setMounted(true); 17 | }, [items, itemsPrice, decrease, increase]); 18 | 19 | if (!mounted) return <>Loading...; 20 | 21 | return ( 22 |
23 |

Shopping Cart

24 | {items.length === 0 ? ( 25 |
26 |

Cart is empty :(

27 | 28 | Go shopping 29 | 30 |
31 | ) : ( 32 |
33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {items.map((item) => ( 44 | 45 | 59 | 78 | 79 | 80 | ))} 81 | 82 |
ItemQuantityPrice
46 | 50 | {item.name} 56 | 57 | {item.name} 58 | 60 |
61 | 68 | {item.qty} 69 | 76 |
77 |
$ {item.price}
83 |
84 |
85 |
86 |
    87 |
  • 88 | Subtotal: {items.reduce((acc, item) => acc + item.qty, 0)}: 89 |
    $ {itemsPrice} 90 |
  • 91 |
  • 92 | 99 |
  • 100 |
101 |
102 |
103 |
104 | )} 105 |
106 | ); 107 | }; 108 | 109 | export default CartDetails; 110 | -------------------------------------------------------------------------------- /app/(front)/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import CartDetails from './CartDetails'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Shopping Cart', 7 | }; 8 | 9 | const CartPage = () => { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default CartPage; 18 | -------------------------------------------------------------------------------- /app/(front)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function FrontLayout({ 2 | children, 3 | }: Readonly<{ 4 | children: React.ReactNode; 5 | }>) { 6 | return
{children}
; 7 | } 8 | -------------------------------------------------------------------------------- /app/(front)/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Loading = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Loading; 12 | -------------------------------------------------------------------------------- /app/(front)/order-history/MyOrders.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import React from 'react'; 6 | import useSWR from 'swr'; 7 | 8 | import { Order } from '@/lib/models/OrderModel'; 9 | 10 | const MyOrders = () => { 11 | const router = useRouter(); 12 | const { data: orders, error, isLoading } = useSWR('/api/orders/mine'); 13 | 14 | if (error) return <>An error has occurred; 15 | if (isLoading) return <>Loading...; 16 | if (!orders) return <>No orders...; 17 | 18 | return ( 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {orders.map((order: Order) => ( 33 | 34 | 35 | 38 | 39 | 44 | 49 | 54 | 55 | ))} 56 | 57 |
IDDATETOTALPAIDDELIVEREDACTION
{order._id.substring(20, 24)} 36 | {order.createdAt.substring(0, 10)} 37 | ${order.totalPrice} 40 | {order.isPaid && order.paidAt 41 | ? `${order.paidAt.substring(0, 10)}` 42 | : 'not paid'} 43 | 45 | {order.isDelivered && order.deliveredAt 46 | ? `${order.deliveredAt.substring(0, 10)}` 47 | : 'not delivered'} 48 | 50 | 51 | Details 52 | 53 |
58 |
59 | ); 60 | }; 61 | 62 | export default MyOrders; 63 | -------------------------------------------------------------------------------- /app/(front)/order-history/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import React from 'react'; 3 | 4 | import MyOrders from './MyOrders'; 5 | 6 | export const metadata: Metadata = { 7 | title: 'Order History', 8 | }; 9 | 10 | const MyOrderPage = () => { 11 | return ( 12 |
13 |

Order History

14 | 15 |
16 | ); 17 | }; 18 | 19 | export default MyOrderPage; 20 | -------------------------------------------------------------------------------- /app/(front)/order/[id]/OrderDetails.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js'; 4 | import Image from 'next/image'; 5 | import Link from 'next/link'; 6 | import { useSession } from 'next-auth/react'; 7 | import toast from 'react-hot-toast'; 8 | import useSWR from 'swr'; 9 | import useSWRMutation from 'swr/mutation'; 10 | 11 | import { OrderItem } from '@/lib/models/OrderModel'; 12 | 13 | interface IOrderDetails { 14 | orderId: string; 15 | paypalClientId: string; 16 | } 17 | 18 | const OrderDetails = ({ orderId, paypalClientId }: IOrderDetails) => { 19 | const { data: session } = useSession(); 20 | 21 | const { trigger: deliverOrder, isMutating: isDelivering } = useSWRMutation( 22 | `/api/orders/${orderId}`, 23 | async (url) => { 24 | const res = await fetch(`/api/admin/orders/${orderId}/deliver`, { 25 | method: 'PUT', 26 | headers: { 27 | 'Content-Type': 'application/json', 28 | }, 29 | }); 30 | const data = await res.json(); 31 | res.ok 32 | ? toast.success('Order delivered successfully') 33 | : toast.error(data.message); 34 | }, 35 | ); 36 | 37 | function createPayPalOrder() { 38 | return fetch(`/api/orders/${orderId}/create-paypal-order`, { 39 | method: 'POST', 40 | headers: { 41 | 'Content-Type': 'application/json', 42 | }, 43 | }) 44 | .then((response) => response.json()) 45 | .then((order) => order.id); 46 | } 47 | 48 | function onApprovePayPalOrder(data: any) { 49 | return fetch(`/api/orders/${orderId}/capture-paypal-order`, { 50 | method: 'POST', 51 | headers: { 52 | 'Content-Type': 'application/json', 53 | }, 54 | body: JSON.stringify(data), 55 | }) 56 | .then((response) => response.json()) 57 | .then((orderData) => { 58 | toast.success('Order paid successfully'); 59 | }); 60 | } 61 | 62 | const { data, error } = useSWR(`/api/orders/${orderId}`); 63 | 64 | if (error) return error.message; 65 | if (!data) return 'Loading...'; 66 | 67 | const { 68 | paymentMethod, 69 | shippingAddress, 70 | items, 71 | itemsPrice, 72 | taxPrice, 73 | shippingPrice, 74 | totalPrice, 75 | isDelivered, 76 | deliveredAt, 77 | isPaid, 78 | paidAt, 79 | } = data; 80 | 81 | return ( 82 |
83 |

Order {orderId}

84 |
85 |
86 |
87 |
88 |

Shipping Address

89 |

{shippingAddress.fullName}

90 |

91 | {shippingAddress.address}, {shippingAddress.city},{' '} 92 | {shippingAddress.postalCode}, {shippingAddress.country}{' '} 93 |

94 | {isDelivered ? ( 95 |
Delivered at {deliveredAt}
96 | ) : ( 97 |
Not Delivered
98 | )} 99 |
100 |
101 | 102 |
103 |
104 |

Payment Method

105 |

{paymentMethod}

106 | {isPaid ? ( 107 |
Paid at {paidAt}
108 | ) : ( 109 |
Not Paid
110 | )} 111 |
112 |
113 | 114 |
115 |
116 |

Items

117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | {items.map((item: OrderItem) => ( 127 | 128 | 144 | 145 | 146 | 147 | ))} 148 | 149 |
ItemQuantityPrice
129 | 133 | {item.name} 139 | 140 | {item.name} ({item.color} {item.size}) 141 | 142 | 143 | {item.qty}${item.price}
150 |
151 |
152 |
153 | 154 |
155 |
156 |
157 |

Order Summary

158 |
    159 |
  • 160 |
    161 |
    Items
    162 |
    ${itemsPrice}
    163 |
    164 |
  • 165 |
  • 166 |
    167 |
    Tax
    168 |
    ${taxPrice}
    169 |
    170 |
  • 171 |
  • 172 |
    173 |
    Shipping
    174 |
    ${shippingPrice}
    175 |
    176 |
  • 177 |
  • 178 |
    179 |
    Total
    180 |
    ${totalPrice}
    181 |
    182 |
  • 183 | 184 | {!isPaid && paymentMethod === 'PayPal' && ( 185 |
  • 186 | 189 | 193 | 194 |
  • 195 | )} 196 | {session?.user.isAdmin && ( 197 |
  • 198 | 208 |
  • 209 | )} 210 |
211 |
212 |
213 |
214 |
215 |
216 | ); 217 | }; 218 | 219 | export default OrderDetails; 220 | -------------------------------------------------------------------------------- /app/(front)/order/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import OrderDetails from './OrderDetails'; 2 | 3 | export const generateMetadata = ({ params }: { params: { id: string } }) => { 4 | return { 5 | title: `Order ${params.id}`, 6 | }; 7 | }; 8 | 9 | const OrderDetailsPage = ({ params }: { params: { id: string } }) => { 10 | return ( 11 | 15 | ); 16 | }; 17 | 18 | export default OrderDetailsPage; 19 | -------------------------------------------------------------------------------- /app/(front)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import { Suspense } from 'react'; 3 | 4 | import Carousel, { CarouselSkeleton } from '@/components/carousel/carousel'; 5 | import Categories from '@/components/categories/Categories'; 6 | import Icons from '@/components/icons/Icons'; 7 | import ProductItems, { 8 | ProductItemsSkeleton, 9 | } from '@/components/products/ProductItems'; 10 | import ReadMore from '@/components/readMore/ReadMore'; 11 | import Text from '@/components/readMore/Text'; 12 | import Slider from '@/components/slider/Slider'; 13 | 14 | export const metadata: Metadata = { 15 | title: process.env.NEXT_PUBLIC_APP_NAME || 'Fullstack Next.js Store', 16 | description: 17 | process.env.NEXT_PUBLIC_APP_DESC || 18 | 'Fullstack Next.js Store - Server Components, MongoDB, Next Auth, Tailwind, Zustand', 19 | }; 20 | 21 | const HomePage = () => { 22 | return ( 23 |
24 |
25 | }> 26 | 27 | 28 |
29 |
30 |
31 |

32 | Simply Unique/
Simply Better. 33 |

34 |
35 |
36 |
37 | Fashion Corner is a gift & 38 | clothes store based in HCMC,
39 | Vietnam. Est since 2019. 40 |
41 |
42 |
43 | 44 | 45 | 46 | } 48 | > 49 | 50 | 51 | 52 | }> 53 | 54 | 55 | 56 | 57 | 58 | 59 |
60 | ); 61 | }; 62 | 63 | export default HomePage; 64 | -------------------------------------------------------------------------------- /app/(front)/payment/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | import CheckoutSteps from '@/components/checkout/CheckoutSteps'; 7 | import useCartService from '@/lib/hooks/useCartStore'; 8 | 9 | const Form = () => { 10 | const router = useRouter(); 11 | const [selectedPaymentMethod, setSelectedPaymentMethod] = useState(''); 12 | 13 | const { savePaymentMethod, paymentMethod, shippingAddress } = 14 | useCartService(); 15 | 16 | const handleSubmit = (e: React.FormEvent) => { 17 | e.preventDefault(); 18 | savePaymentMethod(selectedPaymentMethod); 19 | router.push('/place-order'); 20 | }; 21 | 22 | useEffect(() => { 23 | if (!shippingAddress) { 24 | return router.push('/shipping'); 25 | } 26 | setSelectedPaymentMethod(paymentMethod || 'PayPal'); 27 | }, [paymentMethod, router, shippingAddress]); 28 | 29 | return ( 30 |
31 | 32 |
33 |
34 |

Payment Method

35 |
36 | {['PayPal', 'Stripe', 'CashOnDelivery'].map((payment) => ( 37 |
38 | 49 |
50 | ))} 51 |
52 | 55 |
56 |
57 | 64 |
65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | export default Form; 72 | -------------------------------------------------------------------------------- /app/(front)/payment/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import Form from './Form'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Payment method', 7 | }; 8 | 9 | const PaymentPage = async () => { 10 | return ( 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default PaymentPage; 18 | -------------------------------------------------------------------------------- /app/(front)/place-order/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Image from 'next/image'; 4 | import Link from 'next/link'; 5 | import { useRouter } from 'next/navigation'; 6 | import { useEffect, useState } from 'react'; 7 | import toast from 'react-hot-toast'; 8 | import useSWRMutation from 'swr/mutation'; 9 | 10 | import CheckoutSteps from '@/components/checkout/CheckoutSteps'; 11 | import useCartService from '@/lib/hooks/useCartStore'; 12 | 13 | const Form = () => { 14 | const router = useRouter(); 15 | const { 16 | paymentMethod, 17 | shippingAddress, 18 | items, 19 | itemsPrice, 20 | taxPrice, 21 | shippingPrice, 22 | totalPrice, 23 | clear, 24 | } = useCartService(); 25 | 26 | // mutate data in the backend by calling trigger function 27 | const { trigger: placeOrder, isMutating: isPlacing } = useSWRMutation( 28 | `/api/orders/mine`, 29 | async (url) => { 30 | const res = await fetch('/api/orders', { 31 | method: 'POST', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | }, 35 | body: JSON.stringify({ 36 | paymentMethod, 37 | shippingAddress, 38 | items, 39 | itemsPrice, 40 | taxPrice, 41 | shippingPrice, 42 | totalPrice, 43 | }), 44 | }); 45 | const data = await res.json(); 46 | if (res.ok) { 47 | clear(); 48 | toast.success('Order placed successfully'); 49 | return router.push(`/order/${data.order._id}`); 50 | } else { 51 | toast.error(data.message); 52 | } 53 | }, 54 | ); 55 | 56 | useEffect(() => { 57 | if (!paymentMethod) { 58 | return router.push('/payment'); 59 | } 60 | if (items.length === 0) { 61 | return router.push('/'); 62 | } 63 | // eslint-disable-next-line react-hooks/exhaustive-deps 64 | }, [paymentMethod, router]); 65 | 66 | const [mounted, setMounted] = useState(false); 67 | 68 | useEffect(() => { 69 | setMounted(true); 70 | }, []); 71 | 72 | if (!mounted) return <>Loading...; 73 | 74 | return ( 75 |
76 | 77 | 78 |
79 |
80 |
81 |
82 |

Shipping Address

83 |

{shippingAddress.fullName}

84 |

85 | {shippingAddress.address}, {shippingAddress.city},{' '} 86 | {shippingAddress.postalCode}, {shippingAddress.country}{' '} 87 |

88 |
89 | 90 | Edit 91 | 92 |
93 |
94 |
95 | 96 |
97 |
98 |

Payment Method

99 |

{paymentMethod}

100 |
101 | 102 | Edit 103 | 104 |
105 |
106 |
107 | 108 |
109 |
110 |

Items

111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | {items.map((item) => ( 121 | 122 | 138 | 141 | 142 | 143 | ))} 144 | 145 |
ItemQuantityPrice
123 | 127 | {item.name} 133 | 134 | {item.name}({item.color} {item.size}) 135 | 136 | 137 | 139 | {item.qty} 140 | ${item.price}
146 |
147 | 148 | Edit 149 | 150 |
151 |
152 |
153 |
154 | 155 |
156 |
157 |
158 |

Order Summary

159 |
    160 |
  • 161 |
    162 |
    Items
    163 |
    ${itemsPrice}
    164 |
    165 |
  • 166 |
  • 167 |
    168 |
    Tax
    169 |
    ${taxPrice}
    170 |
    171 |
  • 172 |
  • 173 |
    174 |
    Shipping
    175 |
    ${shippingPrice}
    176 |
    177 |
  • 178 |
  • 179 |
    180 |
    Total
    181 |
    ${totalPrice}
    182 |
    183 |
  • 184 | 185 |
  • 186 | 196 |
  • 197 |
198 |
199 |
200 |
201 |
202 |
203 | ); 204 | }; 205 | 206 | export default Form; 207 | -------------------------------------------------------------------------------- /app/(front)/place-order/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import Form from './Form'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Place order', 7 | }; 8 | 9 | const PlaceOrderPage = () => { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | }; 16 | 17 | export default PlaceOrderPage; 18 | -------------------------------------------------------------------------------- /app/(front)/product/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { notFound } from 'next/navigation'; 4 | import { getPlaiceholder } from 'plaiceholder'; 5 | 6 | import AddToCart from '@/components/products/AddToCart'; 7 | import { Rating } from '@/components/products/Rating'; 8 | import productService from '@/lib/services/productService'; 9 | import { convertDocToObj } from '@/lib/utils'; 10 | 11 | export const generateMetadata = async ({ 12 | params, 13 | }: { 14 | params: { slug: string }; 15 | }) => { 16 | const product = await productService.getBySlug(params.slug); 17 | 18 | if (!product) { 19 | return notFound(); 20 | } 21 | 22 | return { 23 | title: product.name, 24 | description: product.description, 25 | }; 26 | }; 27 | 28 | const ProductPage = async ({ params }: { params: { slug: string } }) => { 29 | const product = await productService.getBySlug(params.slug); 30 | 31 | if (!product) { 32 | return notFound(); 33 | } 34 | 35 | const buffer = await fetch(product.image).then(async (res) => 36 | Buffer.from(await res.arrayBuffer()), 37 | ); 38 | 39 | const { base64 } = await getPlaiceholder(buffer); 40 | 41 | return ( 42 |
43 |
44 | {`<- Back to Products`} 45 |
46 |
47 |
48 | {product.name} 58 |
59 |
60 |
    61 |
  • 62 |

    {product.name}

    63 |
  • 64 |
  • 65 | 69 |
  • 70 |
  • {product.brand}
  • 71 |
  • 72 |
    73 |
  • 74 |
  • 75 |

    Description: {product.description}

    76 |
  • 77 |
78 |
79 |
80 |
81 |
82 |
83 |
Price
84 |
${product.price}
85 |
86 |
87 |
Status
88 |
89 | {product.countInStock > 0 ? 'In Stock' : 'Unavailable'} 90 |
91 |
92 | {product.countInStock !== 0 && ( 93 |
94 | 102 |
103 | )} 104 |
105 |
106 |
107 |
108 |
109 | ); 110 | }; 111 | 112 | export default ProductPage; 113 | -------------------------------------------------------------------------------- /app/(front)/profile/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { useSession } from 'next-auth/react'; 5 | import { useEffect } from 'react'; 6 | import { SubmitHandler, useForm } from 'react-hook-form'; 7 | import toast from 'react-hot-toast'; 8 | 9 | type Inputs = { 10 | name: string; 11 | email: string; 12 | password: string; 13 | confirmPassword: string; 14 | }; 15 | 16 | const Form = () => { 17 | const { data: session, update } = useSession(); 18 | const router = useRouter(); 19 | 20 | const { 21 | register, 22 | handleSubmit, 23 | getValues, 24 | setValue, 25 | formState: { errors, isSubmitting }, 26 | } = useForm({ 27 | defaultValues: { 28 | name: '', 29 | email: '', 30 | password: '', 31 | }, 32 | }); 33 | 34 | useEffect(() => { 35 | if (session && session.user) { 36 | setValue('name', session.user.name!); 37 | setValue('email', session.user.email!); 38 | } 39 | }, [router, session, setValue]); 40 | 41 | const formSubmit: SubmitHandler = async (form) => { 42 | const { name, email, password } = form; 43 | try { 44 | const res = await fetch('/api/auth/profile', { 45 | method: 'PUT', 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify({ 50 | name, 51 | email, 52 | password, 53 | }), 54 | }); 55 | if (res.status === 200) { 56 | toast.success('Profile updated successfully'); 57 | const newSession = { 58 | ...session, 59 | user: { 60 | ...session?.user, 61 | name, 62 | email, 63 | }, 64 | }; 65 | await update(newSession); 66 | } else { 67 | const data = await res.json(); 68 | toast.error(data.message || 'error'); 69 | } 70 | } catch (err: any) { 71 | const error = 72 | err.response && err.response.data && err.response.data.message 73 | ? err.response.data.message 74 | : err.message; 75 | toast.error(error); 76 | } 77 | }; 78 | 79 | return ( 80 |
81 |
82 |

Profile

83 | 84 |
85 | 88 | 96 | {errors.name?.message && ( 97 |
{errors.name.message}
98 | )} 99 |
100 |
101 | 104 | 116 | {errors.email?.message && ( 117 |
{errors.email.message}
118 | )} 119 |
120 |
121 | 124 | 130 | {errors.password?.message && ( 131 |
{errors.password.message}
132 | )} 133 |
134 |
135 | 138 | { 143 | const { password } = getValues(); 144 | return password === value || 'Passwords should match!'; 145 | }, 146 | })} 147 | className='input input-bordered w-full max-w-sm' 148 | /> 149 | {errors.confirmPassword?.message && ( 150 |
{errors.confirmPassword.message}
151 | )} 152 |
153 | 154 |
155 | 165 |
166 | 167 |
168 |
169 | ); 170 | }; 171 | 172 | export default Form; 173 | -------------------------------------------------------------------------------- /app/(front)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | import React from 'react'; 3 | 4 | import Form from './Form'; 5 | 6 | export const metadata: Metadata = { 7 | title: 'Profile', 8 | }; 9 | 10 | const ProfilePage = async () => { 11 | return ( 12 |
13 |
14 |
15 | ); 16 | }; 17 | 18 | export default ProfilePage; 19 | -------------------------------------------------------------------------------- /app/(front)/register/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter, useSearchParams } from 'next/navigation'; 5 | import { useSession } from 'next-auth/react'; 6 | import { useEffect } from 'react'; 7 | import { SubmitHandler, useForm } from 'react-hook-form'; 8 | import toast from 'react-hot-toast'; 9 | 10 | type Inputs = { 11 | name: string; 12 | email: string; 13 | password: string; 14 | confirmPassword: string; 15 | }; 16 | 17 | const Form = () => { 18 | const { data: session } = useSession(); 19 | 20 | const params = useSearchParams(); 21 | const router = useRouter(); 22 | let callbackUrl = params.get('callbackUrl') || '/'; 23 | 24 | const { 25 | register, 26 | handleSubmit, 27 | getValues, 28 | formState: { errors, isSubmitting }, 29 | } = useForm({ 30 | defaultValues: { 31 | name: '', 32 | email: '', 33 | password: '', 34 | confirmPassword: '', 35 | }, 36 | }); 37 | 38 | useEffect(() => { 39 | if (session && session.user) { 40 | router.push(callbackUrl); 41 | } 42 | }, [callbackUrl, params, router, session]); 43 | 44 | const formSubmit: SubmitHandler = async (form) => { 45 | const { name, email, password } = form; 46 | 47 | try { 48 | const res = await fetch('/api/auth/register', { 49 | method: 'POST', 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | }, 53 | body: JSON.stringify({ 54 | name, 55 | email, 56 | password, 57 | }), 58 | }); 59 | if (res.ok) { 60 | return router.push( 61 | `/signin?callbackUrl=${callbackUrl}&success=Account has been created`, 62 | ); 63 | } else { 64 | const data = await res.json(); 65 | throw new Error(data.message); 66 | } 67 | } catch (err: any) { 68 | const error = 69 | err.message && err.message.indexOf('E11000') === 0 70 | ? 'Email is duplicate' 71 | : err.message; 72 | toast.error(error || 'error'); 73 | } 74 | }; 75 | 76 | return ( 77 |
78 |
79 |

Register

80 | 81 |
82 | 85 | 93 | {errors.name?.message && ( 94 |
{errors.name.message}
95 | )} 96 |
97 |
98 | 101 | 113 | {errors.email?.message && ( 114 |
{errors.email.message}
115 | )} 116 |
117 |
118 | 121 | 129 | {errors.password?.message && ( 130 |
{errors.password.message}
131 | )} 132 |
133 |
134 | 137 | { 143 | const { password } = getValues(); 144 | return password === value || 'Passwords should match!'; 145 | }, 146 | })} 147 | className='input input-bordered w-full max-w-sm' 148 | /> 149 | {errors.confirmPassword?.message && ( 150 |
{errors.confirmPassword.message}
151 | )} 152 |
153 |
154 | 164 |
165 | 166 | 167 |
168 |
169 | Already have an account?{' '} 170 | 171 | Login 172 | 173 |
174 |
175 |
176 | ); 177 | }; 178 | 179 | export default Form; 180 | -------------------------------------------------------------------------------- /app/(front)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import Form from './Form'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Register', 7 | }; 8 | 9 | const RegisterPage = async () => { 10 | return ( 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default RegisterPage; 18 | -------------------------------------------------------------------------------- /app/(front)/search/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import ProductItem from '@/components/products/ProductItem'; 4 | import { Rating } from '@/components/products/Rating'; 5 | import productServices from '@/lib/services/productService'; 6 | 7 | const sortOrders = ['newest', 'lowest', 'highest', 'rating']; 8 | const prices = [ 9 | { 10 | name: '$1 to $50', 11 | value: '1-50', 12 | }, 13 | { 14 | name: '$51 to $200', 15 | value: '51-200', 16 | }, 17 | { 18 | name: '$201 to $1000', 19 | value: '201-1000', 20 | }, 21 | ]; 22 | 23 | const ratings = [5, 4, 3, 2, 1]; 24 | 25 | export async function generateMetadata({ 26 | searchParams: { q = 'all', category = 'all', price = 'all', rating = 'all' }, 27 | }: { 28 | searchParams: { 29 | q: string; 30 | category: string; 31 | price: string; 32 | rating: string; 33 | sort: string; 34 | page: string; 35 | }; 36 | }) { 37 | if ( 38 | (q !== 'all' && q !== '') || 39 | category !== 'all' || 40 | rating !== 'all' || 41 | price !== 'all' 42 | ) { 43 | return { 44 | title: `Search ${q !== 'all' ? q : ''} 45 | ${category !== 'all' ? ` : Category ${category}` : ''} 46 | ${price !== 'all' ? ` : Price ${price}` : ''} 47 | ${rating !== 'all' ? ` : Rating ${rating}` : ''}`, 48 | }; 49 | } else { 50 | return { 51 | title: 'Search Products', 52 | }; 53 | } 54 | } 55 | 56 | export default async function SearchPage({ 57 | searchParams: { 58 | q = 'all', 59 | category = 'all', 60 | price = 'all', 61 | rating = 'all', 62 | sort = 'newest', 63 | page = '1', 64 | }, 65 | }: { 66 | searchParams: { 67 | q: string; 68 | category: string; 69 | price: string; 70 | rating: string; 71 | sort: string; 72 | page: string; 73 | }; 74 | }) { 75 | const getFilterUrl = ({ 76 | c, 77 | s, 78 | p, 79 | r, 80 | pg, 81 | }: { 82 | c?: string; 83 | s?: string; 84 | p?: string; 85 | r?: string; 86 | pg?: string; 87 | }) => { 88 | const params = { q, category, price, rating, sort, page }; 89 | if (c) params.category = c; 90 | if (p) params.price = p; 91 | if (r) params.rating = r; 92 | if (pg) params.page = pg; 93 | if (s) params.sort = s; 94 | return `/search?${new URLSearchParams(params).toString()}`; 95 | }; 96 | const categories = await productServices.getCategories(); 97 | const { countProducts, products, pages } = await productServices.getByQuery({ 98 | category, 99 | q, 100 | price, 101 | rating, 102 | page, 103 | sort, 104 | }); 105 | return ( 106 |
107 |
108 |
Categories
109 |
110 |
    111 |
  • 112 | 118 | Any 119 | 120 |
  • 121 | {categories.map((c: string) => ( 122 |
  • 123 | 129 | {c} 130 | 131 |
  • 132 | ))} 133 |
134 |
135 |
136 |
Price
137 |
    138 |
  • 139 | 145 | Any 146 | 147 |
  • 148 | {prices.map((p) => ( 149 |
  • 150 | 156 | {p.name} 157 | 158 |
  • 159 | ))} 160 |
161 |
162 |
163 |
Customer Review
164 |
    165 |
  • 166 | 172 | Any 173 | 174 |
  • 175 | {ratings.map((r) => ( 176 |
  • 177 | 183 | 184 | 185 |
  • 186 | ))} 187 |
188 |
189 |
190 |
191 |
192 |
193 | {products.length === 0 ? 'No' : countProducts} Results 194 | {q !== 'all' && q !== '' && ' : ' + q} 195 | {category !== 'all' && ' : ' + category} 196 | {price !== 'all' && ' : Price ' + price} 197 | {rating !== 'all' && ' : Rating ' + rating + ' & up'} 198 |   199 | {(q !== 'all' && q !== '') || 200 | category !== 'all' || 201 | rating !== 'all' || 202 | price !== 'all' ? ( 203 | 204 | Clear 205 | 206 | ) : null} 207 |
208 |
209 | Sort by:{' '} 210 | {sortOrders.map((s) => ( 211 | 218 | {s} 219 | 220 | ))} 221 |
222 |
223 | 224 |
225 |
226 | {products.map((product) => ( 227 | 228 | ))} 229 |
230 |
231 | {products.length > 0 && 232 | Array.from(Array(pages).keys()).map((p) => ( 233 | 240 | {p + 1} 241 | 242 | ))} 243 |
244 |
245 |
246 |
247 | ); 248 | } 249 | -------------------------------------------------------------------------------- /app/(front)/shipping/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { useEffect } from 'react'; 5 | import { SubmitHandler, ValidationRule, useForm } from 'react-hook-form'; 6 | 7 | import CheckoutSteps from '@/components/checkout/CheckoutSteps'; 8 | import useCartService from '@/lib/hooks/useCartStore'; 9 | import { ShippingAddress } from '@/lib/models/OrderModel'; 10 | 11 | const Form = () => { 12 | const router = useRouter(); 13 | const { saveShippingAddress, shippingAddress } = useCartService(); 14 | 15 | const { 16 | register, 17 | handleSubmit, 18 | setValue, 19 | formState: { errors, isSubmitting }, 20 | } = useForm({ 21 | defaultValues: { 22 | fullName: '', 23 | address: '', 24 | city: '', 25 | postalCode: '', 26 | country: '', 27 | }, 28 | }); 29 | 30 | useEffect(() => { 31 | setValue('fullName', shippingAddress.fullName); 32 | setValue('address', shippingAddress.address); 33 | setValue('city', shippingAddress.city); 34 | setValue('postalCode', shippingAddress.postalCode); 35 | setValue('country', shippingAddress.country); 36 | }, [setValue, shippingAddress]); 37 | 38 | const formSubmit: SubmitHandler = async (form) => { 39 | saveShippingAddress(form); 40 | router.push('/payment'); 41 | }; 42 | 43 | const FormInput = ({ 44 | id, 45 | name, 46 | required, 47 | pattern, 48 | }: { 49 | id: keyof ShippingAddress; 50 | name: string; 51 | required?: boolean; 52 | pattern?: ValidationRule; 53 | }) => ( 54 |
55 | 58 | 67 | {errors[id]?.message && ( 68 |
{errors[id]?.message}
69 | )} 70 |
71 | ); 72 | 73 | return ( 74 |
75 | 76 |
77 |
78 |

Shipping Address

79 | 80 | 81 | 82 | 83 | 84 | 85 |
86 | 94 |
95 | 96 |
97 |
98 |
99 | ); 100 | }; 101 | 102 | export default Form; 103 | -------------------------------------------------------------------------------- /app/(front)/shipping/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import Form from './Form'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Shipping', 7 | }; 8 | 9 | const ShippingPage = async () => { 10 | return ( 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default ShippingPage; 18 | -------------------------------------------------------------------------------- /app/(front)/signin/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter, useSearchParams } from 'next/navigation'; 5 | import { signIn, useSession } from 'next-auth/react'; 6 | import { useEffect } from 'react'; 7 | import { SubmitHandler, useForm } from 'react-hook-form'; 8 | 9 | type Inputs = { 10 | email: string; 11 | password: string; 12 | }; 13 | 14 | const Form = () => { 15 | const params = useSearchParams(); 16 | const { data: session } = useSession(); 17 | 18 | let callbackUrl = params.get('callbackUrl') || '/'; 19 | const router = useRouter(); 20 | 21 | const { 22 | register, 23 | handleSubmit, 24 | formState: { errors, isSubmitting }, 25 | } = useForm({ 26 | defaultValues: { 27 | email: '', 28 | password: '', 29 | }, 30 | }); 31 | 32 | useEffect(() => { 33 | if (session && session.user) { 34 | router.push(callbackUrl); 35 | } 36 | }, [callbackUrl, router, session, params]); 37 | 38 | const formSubmit: SubmitHandler = async (form) => { 39 | const { email, password } = form; 40 | signIn('credentials', { 41 | email, 42 | password, 43 | }); 44 | }; 45 | 46 | return ( 47 |
48 |
49 |

Sign in

50 | {params.get('error') && ( 51 |
52 | {params.get('error') === 'CredentialsSignin' 53 | ? 'Invalid email or password' 54 | : params.get('error')} 55 |
56 | )} 57 | {params.get('success') && ( 58 |
{params.get('success')}
59 | )} 60 | 61 |
62 | 65 | 77 | {errors.email?.message && ( 78 |
{errors.email.message}
79 | )} 80 |
81 |
82 | 85 | 93 | {errors.password?.message && ( 94 |
{errors.password.message}
95 | )} 96 |
97 |
98 | 108 |
109 | 110 |
111 | Need an account?{' '} 112 | 113 | Register 114 | 115 |
116 |
117 |
118 | ); 119 | }; 120 | 121 | export default Form; 122 | -------------------------------------------------------------------------------- /app/(front)/signin/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from 'next'; 2 | 3 | import Form from './Form'; 4 | 5 | export const metadata: Metadata = { 6 | title: 'Sign in', 7 | }; 8 | 9 | const SignInPage = async () => { 10 | return ( 11 |
12 |
13 |
14 | ); 15 | }; 16 | 17 | export default SignInPage; 18 | -------------------------------------------------------------------------------- /app/admin/dashboard/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Chart as ChartJS, 5 | CategoryScale, 6 | LinearScale, 7 | PointElement, 8 | LineElement, 9 | Title, 10 | Tooltip, 11 | Filler, 12 | Legend, 13 | BarElement, 14 | ArcElement, 15 | } from 'chart.js'; 16 | import Link from 'next/link'; 17 | import { Bar, Doughnut, Line } from 'react-chartjs-2'; 18 | import useSWR from 'swr'; 19 | 20 | import { formatNumber } from '@/lib/utils'; 21 | 22 | ChartJS.register( 23 | CategoryScale, 24 | LinearScale, 25 | PointElement, 26 | LineElement, 27 | Title, 28 | Tooltip, 29 | Filler, 30 | Legend, 31 | BarElement, 32 | ArcElement, 33 | ); 34 | 35 | export const options = { 36 | responsive: true, 37 | plugins: { 38 | legend: { 39 | position: 'top', 40 | }, 41 | }, 42 | }; 43 | 44 | const Dashboard = () => { 45 | const { data: summary, error } = useSWR(`/api/admin/summary`); 46 | 47 | console.log(summary); 48 | 49 | if (error) return error.message; 50 | if (!summary) return 'Loading...'; 51 | 52 | const salesData = { 53 | labels: summary.salesData.map((x: { _id: string }) => x._id), 54 | datasets: [ 55 | { 56 | fill: true, 57 | label: 'Sales', 58 | data: summary.salesData.map( 59 | (x: { totalSales: number }) => x.totalSales, 60 | ), 61 | borderColor: 'rgb(53, 162, 235)', 62 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 63 | }, 64 | ], 65 | }; 66 | const ordersData = { 67 | labels: summary.salesData.map((x: { _id: string }) => x._id), 68 | datasets: [ 69 | { 70 | fill: true, 71 | label: 'Orders', 72 | data: summary.salesData.map( 73 | (x: { totalOrders: number }) => x.totalOrders, 74 | ), 75 | borderColor: 'rgb(53, 162, 235)', 76 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 77 | }, 78 | ], 79 | }; 80 | const productsData = { 81 | labels: summary.productsData.map((x: { _id: string }) => x._id), // 2022/01 2022/03 82 | datasets: [ 83 | { 84 | label: 'Category', 85 | data: summary.productsData.map( 86 | (x: { totalProducts: number }) => x.totalProducts, 87 | ), 88 | backgroundColor: [ 89 | 'rgba(255, 99, 132, 0.2)', 90 | 'rgba(54, 162, 235, 0.2)', 91 | 'rgba(255, 206, 86, 0.2)', 92 | 'rgba(75, 192, 192, 0.2)', 93 | 'rgba(153, 102, 255, 0.2)', 94 | 'rgba(255, 159, 64, 0.2)', 95 | ], 96 | borderColor: [ 97 | 'rgba(255, 99, 132, 1)', 98 | 'rgba(54, 162, 235, 1)', 99 | 'rgba(255, 206, 86, 1)', 100 | 'rgba(75, 192, 192, 1)', 101 | 'rgba(153, 102, 255, 1)', 102 | 'rgba(255, 159, 64, 1)', 103 | ], 104 | }, 105 | ], 106 | }; 107 | const usersData = { 108 | labels: summary.usersData.map((x: { _id: string }) => x._id), // 2022/01 2022/03 109 | datasets: [ 110 | { 111 | label: 'Users', 112 | borderColor: 'rgb(53, 162, 235)', 113 | backgroundColor: 'rgba(75, 136, 177, 0.5)', 114 | data: summary.usersData.map( 115 | (x: { totalUsers: number }) => x.totalUsers, 116 | ), 117 | }, 118 | ], 119 | }; 120 | 121 | return ( 122 |
123 |
124 |
125 |
Sales
126 |
127 | ${formatNumber(summary.ordersPrice)} 128 |
129 |
130 | View sales 131 |
132 |
133 |
134 |
Orders
135 |
{summary.ordersCount}
136 |
137 | View orders 138 |
139 |
140 |
141 |
Products
142 |
{summary.productsCount}
143 |
144 | View products 145 |
146 |
147 |
148 |
Users
149 |
{summary.usersCount}
150 |
151 | View users 152 |
153 |
154 |
155 |
156 |
157 |

Sales Report

158 | 159 |
160 |
161 |

Orders Report

162 | 163 |
164 |
165 |
166 |
167 |

Products Report

168 |
169 | 170 |
171 |
172 |
173 |

Users Report

174 | 175 |
176 |
177 |
178 | ); 179 | }; 180 | 181 | export default Dashboard; 182 | -------------------------------------------------------------------------------- /app/admin/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Dashboard from './Dashboard'; 4 | 5 | export const metadata = { 6 | title: 'Admin Dashboard', 7 | }; 8 | const DashbaordPage = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default DashbaordPage; 17 | -------------------------------------------------------------------------------- /app/admin/orders/Orders.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import useSWR from 'swr'; 5 | 6 | import { Order } from '@/lib/models/OrderModel'; 7 | 8 | export default function Orders() { 9 | const { data: orders, error, isLoading } = useSWR(`/api/admin/orders`); 10 | 11 | if (error) return 'An error has occurred.'; 12 | if (isLoading) return 'Loading...'; 13 | 14 | return ( 15 |
16 |

Orders

17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {orders.map((order: Order) => ( 32 | 33 | 34 | 35 | 36 | 37 | 42 | 47 | 52 | 53 | ))} 54 | 55 |
IDUSERDATETOTALPAIDDELIVEREDACTION
..{order._id.substring(20, 24)}{order.user?.name || 'Deleted user'}{order.createdAt.substring(0, 10)}${order.totalPrice} 38 | {order.isPaid && order.paidAt 39 | ? `${order.paidAt.substring(0, 10)}` 40 | : 'not paid'} 41 | 43 | {order.isDelivered && order.deliveredAt 44 | ? `${order.deliveredAt.substring(0, 10)}` 45 | : 'not delivered'} 46 | 48 | 49 | Details 50 | 51 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /app/admin/orders/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Orders from './Orders'; 4 | 5 | export const metadata = { 6 | title: 'Admin Orders', 7 | }; 8 | const AdminOrdersPage = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default AdminOrdersPage; 17 | -------------------------------------------------------------------------------- /app/admin/products/Products.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import toast from 'react-hot-toast'; 6 | import useSWR from 'swr'; 7 | import useSWRMutation from 'swr/mutation'; 8 | 9 | import { Product } from '@/lib/models/ProductModel'; 10 | import { formatId } from '@/lib/utils'; 11 | 12 | export default function Products() { 13 | const { data: products, error } = useSWR(`/api/admin/products`); 14 | 15 | const router = useRouter(); 16 | 17 | const { trigger: deleteProduct } = useSWRMutation( 18 | `/api/admin/products`, 19 | async (url, { arg }: { arg: { productId: string } }) => { 20 | const toastId = toast.loading('Deleting product...'); 21 | const res = await fetch(`${url}/${arg.productId}`, { 22 | method: 'DELETE', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | }); 27 | const data = await res.json(); 28 | res.ok 29 | ? toast.success('Product deleted successfully', { 30 | id: toastId, 31 | }) 32 | : toast.error(data.message, { 33 | id: toastId, 34 | }); 35 | }, 36 | ); 37 | 38 | const { trigger: createProduct, isMutating: isCreating } = useSWRMutation( 39 | `/api/admin/products`, 40 | async (url) => { 41 | const res = await fetch(url, { 42 | method: 'POST', 43 | headers: { 44 | 'Content-Type': 'application/json', 45 | }, 46 | }); 47 | const data = await res.json(); 48 | if (!res.ok) return toast.error(data.message); 49 | 50 | toast.success('Product created successfully'); 51 | router.push(`/admin/products/${data.product._id}`); 52 | }, 53 | ); 54 | 55 | if (error) return 'An error has occurred.'; 56 | if (!products) return 'Loading...'; 57 | 58 | return ( 59 |
60 |
61 |

Products

62 | 70 |
71 | 72 |
73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | {products.map((product: Product) => ( 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 111 | 112 | ))} 113 | 114 |
idnamepricecategorycount in stockratingactions
{formatId(product._id!)}{product.name}${product.price}{product.category}{product.countInStock}{product.rating} 95 | 100 | Edit 101 | 102 |   103 | 110 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /app/admin/products/[id]/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useEffect } from 'react'; 6 | import { ValidationRule, useForm } from 'react-hook-form'; 7 | import toast from 'react-hot-toast'; 8 | import useSWR from 'swr'; 9 | import useSWRMutation from 'swr/mutation'; 10 | 11 | import { Product } from '@/lib/models/ProductModel'; 12 | import { formatId } from '@/lib/utils'; 13 | 14 | export default function ProductEditForm({ productId }: { productId: string }) { 15 | const { data: product, error } = useSWR(`/api/admin/products/${productId}`); 16 | const router = useRouter(); 17 | const { trigger: updateProduct, isMutating: isUpdating } = useSWRMutation( 18 | `/api/admin/products/${productId}`, 19 | async (url, { arg }) => { 20 | const res = await fetch(`${url}`, { 21 | method: 'PUT', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify(arg), 26 | }); 27 | const data = await res.json(); 28 | if (!res.ok) return toast.error(data.message); 29 | 30 | toast.success('Product updated successfully'); 31 | router.push('/admin/products'); 32 | }, 33 | ); 34 | 35 | const { 36 | register, 37 | handleSubmit, 38 | formState: { errors }, 39 | setValue, 40 | } = useForm(); 41 | 42 | useEffect(() => { 43 | if (!product) return; 44 | setValue('name', product.name); 45 | setValue('slug', product.slug); 46 | setValue('price', product.price); 47 | setValue('image', product.image); 48 | setValue('category', product.category); 49 | setValue('brand', product.brand); 50 | setValue('countInStock', product.countInStock); 51 | setValue('description', product.description); 52 | }, [product, setValue]); 53 | 54 | const formSubmit = async (formData: any) => { 55 | await updateProduct(formData); 56 | }; 57 | 58 | if (error) return error.message; 59 | 60 | if (!product) return 'Loading...'; 61 | 62 | const FormInput = ({ 63 | id, 64 | name, 65 | required, 66 | pattern, 67 | }: { 68 | id: keyof Product; 69 | name: string; 70 | required?: boolean; 71 | pattern?: ValidationRule; 72 | }) => ( 73 |
74 | 77 |
78 | 87 | {errors[id]?.message && ( 88 |
{errors[id]?.message}
89 | )} 90 |
91 |
92 | ); 93 | 94 | const uploadHandler = async (e: any) => { 95 | const toastId = toast.loading('Uploading image...'); 96 | try { 97 | const resSign = await fetch('/api/cloudinary-sign', { 98 | method: 'POST', 99 | }); 100 | const { signature, timestamp } = await resSign.json(); 101 | const file = e.target.files[0]; 102 | const formData = new FormData(); 103 | formData.append('file', file); 104 | formData.append('signature', signature); 105 | formData.append('timestamp', timestamp); 106 | formData.append('api_key', process.env.NEXT_PUBLIC_CLOUDINARY_API_KEY!); 107 | const res = await fetch( 108 | `https://api.cloudinary.com/v1_1/${process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME}/upload`, 109 | { 110 | method: 'POST', 111 | body: formData, 112 | }, 113 | ); 114 | const data = await res.json(); 115 | console.log(data.secure_url); 116 | setValue('image', data.secure_url); 117 | toast.success('File uploaded successfully', { 118 | id: toastId, 119 | }); 120 | } catch (err: any) { 121 | toast.error(err.message, { 122 | id: toastId, 123 | }); 124 | } 125 | }; 126 | 127 | return ( 128 |
129 |

Edit Product {formatId(productId)}

130 |
131 | 132 | 133 | 134 | 135 |
136 | 139 |
140 | 146 |
147 |
148 | 149 | 150 | 151 | 152 | 153 | 154 | 162 | 163 | Cancel 164 | 165 | 166 |
167 |
168 | ); 169 | } 170 | -------------------------------------------------------------------------------- /app/admin/products/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Form from './Form'; 4 | 5 | export function generateMetadata({ params }: { params: { id: string } }) { 6 | return { 7 | title: `Edit Product ${params.id}`, 8 | }; 9 | } 10 | 11 | export default function ProductEditPage({ 12 | params, 13 | }: { 14 | params: { id: string }; 15 | }) { 16 | return ( 17 | 18 |
19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/admin/products/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Products from './Products'; 4 | 5 | const AdminProductsPge = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default AdminProductsPge; 14 | -------------------------------------------------------------------------------- /app/admin/users/Users.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import toast from 'react-hot-toast'; 5 | import useSWR from 'swr'; 6 | import useSWRMutation from 'swr/mutation'; 7 | 8 | import { User } from '@/lib/models/UserModel'; 9 | import { formatId } from '@/lib/utils'; 10 | 11 | export default function Users() { 12 | const { data: users, error } = useSWR(`/api/admin/users`); 13 | const { trigger: deleteUser } = useSWRMutation( 14 | `/api/admin/users`, 15 | async (url, { arg }: { arg: { userId: string } }) => { 16 | const toastId = toast.loading('Deleting user...'); 17 | const res = await fetch(`${url}/${arg.userId}`, { 18 | method: 'DELETE', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | }); 23 | const data = await res.json(); 24 | res.ok 25 | ? toast.success('User deleted successfully', { 26 | id: toastId, 27 | }) 28 | : toast.error(data.message, { 29 | id: toastId, 30 | }); 31 | }, 32 | ); 33 | if (error) return 'An error has occurred.'; 34 | if (!users) return 'Loading...'; 35 | 36 | return ( 37 |
38 |

Users

39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {users.map((user: User) => ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 76 | 77 | ))} 78 | 79 |
idnameemailadminactions
{formatId(user._id)}{user.name}{user.email}{user.isAdmin ? 'YES' : 'NO'} 60 | 65 | Edit 66 | 67 |   68 | 75 |
80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/admin/users/[id]/Form.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import { useRouter } from 'next/navigation'; 5 | import { useEffect } from 'react'; 6 | import { ValidationRule, useForm } from 'react-hook-form'; 7 | import toast from 'react-hot-toast'; 8 | import useSWR from 'swr'; 9 | import useSWRMutation from 'swr/mutation'; 10 | 11 | import { User } from '@/lib/models/UserModel'; 12 | import { formatId } from '@/lib/utils'; 13 | 14 | export default function UserEditForm({ userId }: { userId: string }) { 15 | const { data: user, error } = useSWR(`/api/admin/users/${userId}`); 16 | const router = useRouter(); 17 | const { trigger: updateUser, isMutating: isUpdating } = useSWRMutation( 18 | `/api/admin/users/${userId}`, 19 | async (url, { arg }) => { 20 | const res = await fetch(`${url}`, { 21 | method: 'PUT', 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | body: JSON.stringify(arg), 26 | }); 27 | const data = await res.json(); 28 | if (!res.ok) return toast.error(data.message); 29 | 30 | toast.success('User updated successfully'); 31 | router.push('/admin/users'); 32 | }, 33 | ); 34 | 35 | const { 36 | register, 37 | handleSubmit, 38 | formState: { errors }, 39 | setValue, 40 | } = useForm(); 41 | 42 | useEffect(() => { 43 | if (!user) return; 44 | setValue('name', user.name); 45 | setValue('email', user.email); 46 | setValue('isAdmin', user.isAdmin); 47 | }, [user, setValue]); 48 | 49 | const formSubmit = async (formData: any) => { 50 | await updateUser(formData); 51 | }; 52 | 53 | if (error) return error.message; 54 | if (!user) return 'Loading...'; 55 | 56 | const FormInput = ({ 57 | id, 58 | name, 59 | required, 60 | pattern, 61 | }: { 62 | id: keyof User; 63 | name: string; 64 | required?: boolean; 65 | pattern?: ValidationRule; 66 | }) => ( 67 |
68 | 71 |
72 | 81 | {errors[id]?.message && ( 82 |
{errors[id]?.message}
83 | )} 84 |
85 |
86 | ); 87 | 88 | return ( 89 |
90 |

Edit User {formatId(userId)}

91 |
92 | 93 | 94 | 95 | 96 |
97 | 100 |
101 | 107 |
108 |
109 | 110 | 118 | 119 | Cancel 120 | 121 | 122 |
123 |
124 | ); 125 | } 126 | -------------------------------------------------------------------------------- /app/admin/users/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Form from './Form'; 4 | 5 | export function generateMetadata({ params }: { params: { id: string } }) { 6 | return { 7 | title: `Edit User ${params.id}`, 8 | }; 9 | } 10 | 11 | export default function UserEditPage({ params }: { params: { id: string } }) { 12 | return ( 13 | 14 |
15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import AdminLayout from '@/components/admin/AdminLayout'; 2 | 3 | import Users from './Users'; 4 | 5 | export const metadata = { 6 | title: 'Admin Users', 7 | }; 8 | const AdminUsersPage = () => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default AdminUsersPage; 17 | -------------------------------------------------------------------------------- /app/api/admin/orders/[id]/deliver/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | 5 | export const PUT = auth(async (...args: any) => { 6 | const [req, { params }] = args; 7 | if (!req.auth || !req.auth.user?.isAdmin) { 8 | return Response.json( 9 | { message: 'unauthorized' }, 10 | { 11 | status: 401, 12 | }, 13 | ); 14 | } 15 | try { 16 | await dbConnect(); 17 | 18 | const order = await OrderModel.findById(params.id); 19 | if (order) { 20 | // order must be paid to mark as delivered 21 | if (!order.isPaid) 22 | return Response.json( 23 | { message: 'Order is not paid' }, 24 | { 25 | status: 400, 26 | }, 27 | ); 28 | order.isDelivered = true; 29 | order.deliveredAt = Date.now(); 30 | const updatedOrder = await order.save(); 31 | return Response.json(updatedOrder); 32 | } else { 33 | return Response.json( 34 | { message: 'Order not found' }, 35 | { 36 | status: 404, 37 | }, 38 | ); 39 | } 40 | } catch (err: any) { 41 | return Response.json( 42 | { message: err.message }, 43 | { 44 | status: 500, 45 | }, 46 | ); 47 | } 48 | }) as any; 49 | -------------------------------------------------------------------------------- /app/api/admin/orders/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | 5 | export const GET = auth(async (req: any) => { 6 | if (!req.auth || !req.auth.user?.isAdmin) { 7 | return Response.json( 8 | { message: 'unauthorized' }, 9 | { 10 | status: 401, 11 | }, 12 | ); 13 | } 14 | await dbConnect(); 15 | const orders = await OrderModel.find() 16 | .sort({ createdAt: -1 }) 17 | // get the name of user 18 | .populate('user', 'name'); 19 | 20 | return Response.json(orders); 21 | }) as any; 22 | -------------------------------------------------------------------------------- /app/api/admin/products/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import ProductModel from '@/lib/models/ProductModel'; 4 | 5 | export const GET = auth(async (...args: any) => { 6 | const [req, { params }] = args; 7 | if (!req.auth || !req.auth.user?.isAdmin) { 8 | return Response.json( 9 | { message: 'unauthorized' }, 10 | { 11 | status: 401, 12 | }, 13 | ); 14 | } 15 | await dbConnect(); 16 | const product = await ProductModel.findById(params.id); 17 | if (!product) { 18 | return Response.json( 19 | { message: 'product not found' }, 20 | { 21 | status: 404, 22 | }, 23 | ); 24 | } 25 | return Response.json(product); 26 | }) as any; 27 | 28 | export const PUT = auth(async (...args: any) => { 29 | const [req, { params }] = args; 30 | if (!req.auth || !req.auth.user?.isAdmin) { 31 | return Response.json( 32 | { message: 'unauthorized' }, 33 | { 34 | status: 401, 35 | }, 36 | ); 37 | } 38 | 39 | const { 40 | name, 41 | slug, 42 | price, 43 | category, 44 | image, 45 | brand, 46 | countInStock, 47 | description, 48 | } = await req.json(); 49 | 50 | try { 51 | await dbConnect(); 52 | 53 | const product = await ProductModel.findById(params.id); 54 | if (product) { 55 | product.name = name; 56 | product.slug = slug; 57 | product.price = price; 58 | product.category = category; 59 | product.image = image; 60 | product.brand = brand; 61 | product.countInStock = countInStock; 62 | product.description = description; 63 | 64 | const updatedProduct = await product.save(); 65 | return Response.json(updatedProduct); 66 | } else { 67 | return Response.json( 68 | { message: 'Product not found' }, 69 | { 70 | status: 404, 71 | }, 72 | ); 73 | } 74 | } catch (err: any) { 75 | return Response.json( 76 | { message: err.message }, 77 | { 78 | status: 500, 79 | }, 80 | ); 81 | } 82 | }) as any; 83 | 84 | export const DELETE = auth(async (...args: any) => { 85 | const [req, { params }] = args; 86 | 87 | if (!req.auth || !req.auth.user?.isAdmin) { 88 | return Response.json( 89 | { message: 'unauthorized' }, 90 | { 91 | status: 401, 92 | }, 93 | ); 94 | } 95 | 96 | try { 97 | await dbConnect(); 98 | const product = await ProductModel.findById(params.id); 99 | if (product) { 100 | await product.deleteOne(); 101 | return Response.json({ message: 'Product deleted successfully' }); 102 | } else { 103 | return Response.json( 104 | { message: 'Product not found' }, 105 | { 106 | status: 404, 107 | }, 108 | ); 109 | } 110 | } catch (err: any) { 111 | return Response.json( 112 | { message: err.message }, 113 | { 114 | status: 500, 115 | }, 116 | ); 117 | } 118 | }) as any; 119 | -------------------------------------------------------------------------------- /app/api/admin/products/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import ProductModel from '@/lib/models/ProductModel'; 4 | 5 | export const GET = auth(async (req: any) => { 6 | if (!req.auth || !req.auth.user?.isAdmin) { 7 | return Response.json( 8 | { message: 'unauthorized' }, 9 | { 10 | status: 401, 11 | }, 12 | ); 13 | } 14 | await dbConnect(); 15 | const products = await ProductModel.find(); 16 | return Response.json(products); 17 | }) as any; 18 | 19 | export const POST = auth(async (req: any) => { 20 | if (!req.auth || !req.auth.user?.isAdmin) { 21 | return Response.json( 22 | { message: 'unauthorized' }, 23 | { 24 | status: 401, 25 | }, 26 | ); 27 | } 28 | await dbConnect(); 29 | const product = new ProductModel({ 30 | name: 'sample name', 31 | slug: 'sample-name-' + Math.random(), 32 | image: 33 | 'https://res.cloudinary.com/dqxlehni0/image/upload/v1715622109/No_Image_Available_kbdno1.jpg', 34 | price: 0, 35 | category: 'sample category', 36 | brand: 'sample brand', 37 | countInStock: 0, 38 | description: 'sample description', 39 | rating: 0, 40 | numReviews: 0, 41 | }); 42 | try { 43 | await product.save(); 44 | return Response.json( 45 | { message: 'Product created successfully', product }, 46 | { 47 | status: 201, 48 | }, 49 | ); 50 | } catch (err: any) { 51 | return Response.json( 52 | { message: err.message }, 53 | { 54 | status: 500, 55 | }, 56 | ); 57 | } 58 | }) as any; 59 | -------------------------------------------------------------------------------- /app/api/admin/summary/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | import ProductModel from '@/lib/models/ProductModel'; 5 | import UserModel from '@/lib/models/UserModel'; 6 | 7 | export const GET = auth(async (...request: any) => { 8 | const [req, { params }] = request; 9 | if (!req.auth || !req.auth.user?.isAdmin) { 10 | return Response.json({ message: 'unauthorized' }, { status: 401 }); 11 | } 12 | await dbConnect(); 13 | 14 | const ordersCount = await OrderModel.countDocuments(); 15 | const productsCount = await ProductModel.countDocuments(); 16 | const usersCount = await UserModel.countDocuments(); 17 | 18 | const ordersPriceGroup = await OrderModel.aggregate([ 19 | { 20 | $group: { 21 | _id: null, 22 | // sum calculate total price of all orders 23 | sales: { $sum: '$totalPrice' }, 24 | }, 25 | }, 26 | ]); 27 | 28 | const ordersPrice = 29 | ordersPriceGroup.length > 0 ? ordersPriceGroup[0].sales : 0; 30 | 31 | const salesData = await OrderModel.aggregate([ 32 | { 33 | $group: { 34 | _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } }, 35 | totalOrders: { $sum: 1 }, 36 | totalSales: { $sum: '$totalPrice' }, 37 | }, 38 | }, 39 | { $sort: { _id: 1 } }, 40 | ]); 41 | 42 | const productsData = await ProductModel.aggregate([ 43 | { 44 | $group: { 45 | _id: '$category', 46 | totalProducts: { $sum: 1 }, 47 | }, 48 | }, 49 | { $sort: { _id: 1 } }, 50 | ]); 51 | 52 | const usersData = await UserModel.aggregate([ 53 | { 54 | $group: { 55 | _id: { $dateToString: { format: '%Y-%m', date: '$createdAt' } }, 56 | totalUsers: { $sum: 1 }, 57 | }, 58 | }, 59 | { $sort: { _id: 1 } }, 60 | ]); 61 | 62 | return Response.json({ 63 | ordersCount, 64 | productsCount, 65 | usersCount, 66 | ordersPrice, 67 | salesData, 68 | productsData, 69 | usersData, 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /app/api/admin/users/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import UserModel from '@/lib/models/UserModel'; 4 | 5 | export const GET = auth(async (...args: any) => { 6 | const [req, { params }] = args; 7 | if (!req.auth || !req.auth.user?.isAdmin) { 8 | return Response.json( 9 | { message: 'unauthorized' }, 10 | { 11 | status: 401, 12 | }, 13 | ); 14 | } 15 | await dbConnect(); 16 | const user = await UserModel.findById(params.id); 17 | if (!user) { 18 | return Response.json( 19 | { message: 'user not found' }, 20 | { 21 | status: 404, 22 | }, 23 | ); 24 | } 25 | return Response.json(user); 26 | }) as any; 27 | 28 | export const PUT = auth(async (...p: any) => { 29 | const [req, { params }] = p; 30 | if (!req.auth || !req.auth.user?.isAdmin) { 31 | return Response.json( 32 | { message: 'unauthorized' }, 33 | { 34 | status: 401, 35 | }, 36 | ); 37 | } 38 | 39 | const { name, email, isAdmin } = await req.json(); 40 | 41 | try { 42 | await dbConnect(); 43 | const user = await UserModel.findById(params.id); 44 | if (user) { 45 | user.name = name; 46 | user.email = email; 47 | user.isAdmin = Boolean(isAdmin); 48 | 49 | const updatedUser = await user.save(); 50 | return Response.json({ 51 | message: 'User updated successfully', 52 | user: updatedUser, 53 | }); 54 | } else { 55 | return Response.json( 56 | { message: 'User not found' }, 57 | { 58 | status: 404, 59 | }, 60 | ); 61 | } 62 | } catch (err: any) { 63 | return Response.json( 64 | { message: err.message }, 65 | { 66 | status: 500, 67 | }, 68 | ); 69 | } 70 | }) as any; 71 | 72 | export const DELETE = auth(async (...args: any) => { 73 | const [req, { params }] = args; 74 | if (!req.auth || !req.auth.user?.isAdmin) { 75 | return Response.json( 76 | { message: 'unauthorized' }, 77 | { 78 | status: 401, 79 | }, 80 | ); 81 | } 82 | 83 | try { 84 | await dbConnect(); 85 | const user = await UserModel.findById(params.id); 86 | if (user) { 87 | if (user.isAdmin) 88 | return Response.json( 89 | { message: 'User is admin' }, 90 | { 91 | status: 400, 92 | }, 93 | ); 94 | await user.deleteOne(); 95 | return Response.json({ message: 'User deleted successfully' }); 96 | } else { 97 | return Response.json( 98 | { message: 'User not found' }, 99 | { 100 | status: 404, 101 | }, 102 | ); 103 | } 104 | } catch (err: any) { 105 | return Response.json( 106 | { message: err.message }, 107 | { 108 | status: 500, 109 | }, 110 | ); 111 | } 112 | }) as any; 113 | -------------------------------------------------------------------------------- /app/api/admin/users/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import UserModel from '@/lib/models/UserModel'; 4 | 5 | export const GET = auth(async (req: any) => { 6 | if (!req.auth || !req.auth.user?.isAdmin) { 7 | return Response.json( 8 | { message: 'unauthorized' }, 9 | { 10 | status: 401, 11 | }, 12 | ); 13 | } 14 | await dbConnect(); 15 | const users = await UserModel.find(); 16 | return Response.json(users); 17 | }) as any; 18 | -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from '@/lib/auth'; 2 | -------------------------------------------------------------------------------- /app/api/auth/profile/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | import dbConnect from '@/lib/dbConnect'; 5 | import UserModel from '@/lib/models/UserModel'; 6 | 7 | export const PUT = auth(async (req) => { 8 | if (!req.auth) { 9 | return Response.json({ message: 'Not authenticated' }, { status: 401 }); 10 | } 11 | const { user } = req.auth; 12 | const { name, email, password } = await req.json(); 13 | await dbConnect(); 14 | try { 15 | const dbUser = await UserModel.findById(user._id); 16 | if (!dbUser) { 17 | return Response.json( 18 | { message: 'User not found' }, 19 | { 20 | status: 404, 21 | }, 22 | ); 23 | } 24 | dbUser.name = name; 25 | dbUser.email = email; 26 | dbUser.password = password 27 | ? await bcrypt.hash(password, 5) 28 | : dbUser.password; 29 | await dbUser.save(); 30 | return Response.json({ message: 'User has been updated' }); 31 | } catch (err: any) { 32 | return Response.json( 33 | { message: err.message }, 34 | { 35 | status: 500, 36 | }, 37 | ); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /app/api/auth/register/route.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import { NextRequest } from 'next/server'; 3 | 4 | import dbConnect from '@/lib/dbConnect'; 5 | import UserModel from '@/lib/models/UserModel'; 6 | 7 | export const POST = async (request: NextRequest) => { 8 | const { name, email, password } = await request.json(); 9 | await dbConnect(); 10 | const hashedPassword = await bcrypt.hash(password, 5); 11 | const newUser = new UserModel({ 12 | name, 13 | email, 14 | password: hashedPassword, 15 | }); 16 | try { 17 | await newUser.save(); 18 | return Response.json( 19 | { message: 'User has been created' }, 20 | { 21 | status: 201, 22 | }, 23 | ); 24 | } catch (err: any) { 25 | return Response.json( 26 | { message: err.message }, 27 | { 28 | status: 500, 29 | }, 30 | ); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /app/api/cloudinary-sign/route.ts: -------------------------------------------------------------------------------- 1 | import cloudinary from 'cloudinary'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | 5 | export const POST = auth(async (req: any) => { 6 | if (!req.auth || !req.auth.user?.isAdmin) { 7 | return Response.json( 8 | { message: 'unauthorized' }, 9 | { 10 | status: 401, 11 | }, 12 | ); 13 | } 14 | 15 | const timestamp = Math.round(new Date().getTime() / 1000); 16 | const signature = cloudinary.v2.utils.api_sign_request( 17 | { 18 | timestamp: timestamp, 19 | }, 20 | process.env.CLOUDINARY_SECRET!, 21 | ); 22 | 23 | return Response.json({ signature, timestamp }); 24 | }) as any; 25 | -------------------------------------------------------------------------------- /app/api/orders/[id]/capture-paypal-order/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | import { paypal } from '@/lib/paypal'; 5 | 6 | export const POST = auth(async (...request: any) => { 7 | const [req, { params }] = request; 8 | if (!req.auth) { 9 | return Response.json( 10 | { message: 'unauthorized' }, 11 | { 12 | status: 401, 13 | }, 14 | ); 15 | } 16 | await dbConnect(); 17 | const order = await OrderModel.findById(params.id); 18 | if (order) { 19 | try { 20 | const { orderID } = await req.json(); 21 | const captureData = await paypal.capturePayment(orderID); 22 | order.isPaid = true; 23 | order.paidAt = Date.now(); 24 | order.paymentResult = { 25 | id: captureData.id, 26 | status: captureData.status, 27 | email_address: captureData.payer.email_address, 28 | }; 29 | const updatedOrder = await order.save(); 30 | return Response.json(updatedOrder); 31 | } catch (err: any) { 32 | return Response.json( 33 | { message: err.message }, 34 | { 35 | status: 500, 36 | }, 37 | ); 38 | } 39 | } else { 40 | return Response.json( 41 | { message: 'Order not found' }, 42 | { 43 | status: 404, 44 | }, 45 | ); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /app/api/orders/[id]/create-paypal-order/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | import { paypal } from '@/lib/paypal'; 5 | 6 | export const POST = auth(async (...request: any) => { 7 | const [req, { params }] = request; 8 | if (!req.auth) { 9 | return Response.json( 10 | { message: 'unauthorized' }, 11 | { 12 | status: 401, 13 | }, 14 | ); 15 | } 16 | await dbConnect(); 17 | 18 | const order = await OrderModel.findById(params.id); 19 | if (order) { 20 | try { 21 | const paypalOrder = await paypal.createOrder(order.totalPrice); 22 | return Response.json(paypalOrder); 23 | } catch (err: any) { 24 | return Response.json( 25 | { message: err.message }, 26 | { 27 | status: 500, 28 | }, 29 | ); 30 | } 31 | } else { 32 | return Response.json( 33 | { message: 'Order not found' }, 34 | { 35 | status: 404, 36 | }, 37 | ); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /app/api/orders/[id]/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | 5 | export const GET = auth(async (...request: any) => { 6 | const [req, { params }] = request; 7 | if (!req.auth) { 8 | return Response.json( 9 | { message: 'unauthorized' }, 10 | { 11 | status: 401, 12 | }, 13 | ); 14 | } 15 | await dbConnect(); 16 | const order = await OrderModel.findById(params.id); 17 | return Response.json(order); 18 | }); 19 | -------------------------------------------------------------------------------- /app/api/orders/mine/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel from '@/lib/models/OrderModel'; 4 | 5 | export const GET = auth(async (req: any) => { 6 | if (!req.auth) { 7 | return Response.json( 8 | { message: 'unauthorized' }, 9 | { 10 | status: 401, 11 | }, 12 | ); 13 | } 14 | const { user } = req.auth; 15 | await dbConnect(); 16 | const orders = await OrderModel.find({ user: user._id }); 17 | return Response.json(orders); 18 | }); 19 | -------------------------------------------------------------------------------- /app/api/orders/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from '@/lib/auth'; 2 | import dbConnect from '@/lib/dbConnect'; 3 | import OrderModel, { OrderItem } from '@/lib/models/OrderModel'; 4 | import ProductModel from '@/lib/models/ProductModel'; 5 | import { round2 } from '@/lib/utils'; 6 | 7 | const calcPrices = (orderItems: OrderItem[]) => { 8 | // Calculate the items price 9 | const itemsPrice = round2( 10 | orderItems.reduce((acc, item) => acc + item.price * item.qty, 0), 11 | ); 12 | // Calculate the shipping price 13 | const shippingPrice = round2(itemsPrice > 100 ? 0 : 10); 14 | // Calculate the tax price 15 | const taxPrice = round2(Number((0.15 * itemsPrice).toFixed(2))); 16 | // Calculate the total price 17 | const totalPrice = round2(itemsPrice + shippingPrice + taxPrice); 18 | return { itemsPrice, shippingPrice, taxPrice, totalPrice }; 19 | }; 20 | 21 | export const POST = auth(async (req: any) => { 22 | if (!req.auth) { 23 | return Response.json( 24 | { message: 'unauthorized' }, 25 | { 26 | status: 401, 27 | }, 28 | ); 29 | } 30 | const { user } = req.auth; 31 | try { 32 | const payload = await req.json(); 33 | await dbConnect(); 34 | const dbProductPrices = await ProductModel.find( 35 | { 36 | _id: { $in: payload.items.map((x: { _id: string }) => x._id) }, 37 | }, 38 | 'price', 39 | ); 40 | const dbOrderItems = payload.items.map((x: { _id: string }) => ({ 41 | ...x, 42 | product: x._id, 43 | price: dbProductPrices.find((item) => item._id.toString() === x._id) 44 | .price, 45 | _id: undefined, 46 | })); 47 | 48 | const { itemsPrice, taxPrice, shippingPrice, totalPrice } = 49 | calcPrices(dbOrderItems); 50 | 51 | const newOrder = new OrderModel({ 52 | items: dbOrderItems, 53 | itemsPrice, 54 | taxPrice, 55 | shippingPrice, 56 | totalPrice, 57 | shippingAddress: payload.shippingAddress, 58 | paymentMethod: payload.paymentMethod, 59 | user: user._id, 60 | }); 61 | 62 | const createdOrder = await newOrder.save(); 63 | return Response.json( 64 | { message: 'Order has been created', order: createdOrder }, 65 | { 66 | status: 201, 67 | }, 68 | ); 69 | } catch (err: any) { 70 | return Response.json( 71 | { message: err.message }, 72 | { 73 | status: 500, 74 | }, 75 | ); 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /app/api/products/categories/route.ts: -------------------------------------------------------------------------------- 1 | import dbConnect from '@/lib/dbConnect'; 2 | import ProductModel from '@/lib/models/ProductModel'; 3 | 4 | export const GET = async (req: any) => { 5 | await dbConnect(); 6 | const categories = await ProductModel.find().distinct('category'); 7 | return Response.json(categories); 8 | }; 9 | -------------------------------------------------------------------------------- /app/api/products/seed/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server'; 2 | 3 | import data from '@/lib/data'; 4 | import dbConnect from '@/lib/dbConnect'; 5 | // import ProductModel from '@/lib/models/ProductModel'; 6 | // import UserModel from '@/lib/models/UserModel'; 7 | 8 | export const GET = async (request: NextRequest) => { 9 | const { users, products } = data; 10 | await dbConnect(); 11 | // await UserModel.deleteMany(); 12 | // await UserModel.insertMany(users); 13 | 14 | // await ProductModel.deleteMany(); 15 | // await ProductModel.insertMany(products); 16 | 17 | return NextResponse.json({ 18 | message: 'seeded successfully', 19 | // users, 20 | // products, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next'; 2 | import { Inter } from 'next/font/google'; 3 | import './globals.css'; 4 | 5 | import DrawerButton from '@/components/DrawerButton'; 6 | import Footer from '@/components/footer/Footer'; 7 | import Header from '@/components/header/Header'; 8 | import Providers from '@/components/Providers'; 9 | import Sidebar from '@/components/Sidebar'; 10 | 11 | const inter = Inter({ subsets: ['latin'], display: 'swap' }); 12 | 13 | export const metadata: Metadata = { 14 | title: 'Fashion Corner', 15 | description: 'Generated by create next app', 16 | }; 17 | 18 | const RootLayout = ({ children }: { children: React.ReactNode }) => { 19 | return ( 20 | 21 | 22 | 23 |
24 | 25 |
26 |
27 |
28 | {children} 29 |
30 |
31 |
32 |
33 | 38 | 39 |
40 |
41 |
42 | 43 | 44 | ); 45 | }; 46 | 47 | export default RootLayout; 48 | -------------------------------------------------------------------------------- /app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import React from 'react'; 3 | 4 | const NotFoundPage = () => { 5 | return ( 6 |
7 |
8 |

404 - Page not found

9 | 10 | Back Home 11 | 12 |
13 |
14 | ); 15 | }; 16 | 17 | export default NotFoundPage; 18 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/ClientProvider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AppProgressBar as ProgressBar } from 'next-nprogress-bar'; 4 | import { useEffect, useState } from 'react'; 5 | import toast, { Toaster } from 'react-hot-toast'; 6 | import { SWRConfig } from 'swr'; 7 | 8 | import { cartStore } from '@/lib/hooks/useCartStore'; 9 | import useLayoutService from '@/lib/hooks/useLayout'; 10 | 11 | const ClientProvider = ({ children }: { children: React.ReactNode }) => { 12 | const { theme } = useLayoutService(); 13 | const [selectedTheme, setSelectedTheme] = useState('system'); 14 | 15 | useEffect(() => { 16 | setSelectedTheme(theme); 17 | }, [theme]); 18 | 19 | const updateStore = () => { 20 | cartStore.persist.rehydrate(); 21 | }; 22 | 23 | // cart will be refreshed on cart change n browser 24 | useEffect(() => { 25 | document.addEventListener('visibilitychange', updateStore); 26 | window.addEventListener('focus', updateStore); 27 | return () => { 28 | document.removeEventListener('visibilitychange', updateStore); 29 | window.removeEventListener('focus', updateStore); 30 | }; 31 | }, []); 32 | 33 | return ( 34 | { 37 | toast.error(error.message); 38 | }, 39 | fetcher: async (resource, init) => { 40 | const res = await fetch(resource, init); 41 | if (!res.ok) { 42 | throw new Error('An error occurred while fetching the data.'); 43 | } 44 | return res.json(); 45 | }, 46 | }} 47 | > 48 |
49 | 50 | 51 | {children} 52 |
53 |
54 | ); 55 | }; 56 | 57 | export default ClientProvider; 58 | -------------------------------------------------------------------------------- /components/DrawerButton.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useLayoutService from '@/lib/hooks/useLayout'; 4 | 5 | const DrawerButton = () => { 6 | const { drawerOpen, toggleDrawer } = useLayoutService(); 7 | 8 | return ( 9 | 16 | ); 17 | }; 18 | 19 | export default DrawerButton; 20 | -------------------------------------------------------------------------------- /components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { SessionProvider } from 'next-auth/react'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | 5 | import ClientProvider from './ClientProvider'; 6 | 7 | const Providers = async ({ children }: { children: React.ReactNode }) => { 8 | const session = await auth(); 9 | return ( 10 | 11 | {children} 12 | 13 | ); 14 | }; 15 | 16 | export default Providers; 17 | -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import Link from 'next/link'; 4 | import useSWR from 'swr'; 5 | 6 | import useLayoutService from '@/lib/hooks/useLayout'; 7 | 8 | const Sidebar = () => { 9 | const { toggleDrawer } = useLayoutService(); 10 | const { 11 | data: categories, 12 | error, 13 | isLoading, 14 | } = useSWR('/api/products/categories'); 15 | 16 | if (error) return error.message; 17 | if (isLoading || !categories) return 'Loading...'; 18 | 19 | return ( 20 |
    21 |
  • 22 |

    Shop Categories

    23 |
  • 24 | {categories.map((category: string) => ( 25 |
  • 26 | 27 | {category} 28 | 29 |
  • 30 | ))} 31 |
32 | ); 33 | }; 34 | 35 | export default Sidebar; 36 | -------------------------------------------------------------------------------- /components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | const Wrapper = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | 5 | export default Wrapper; 6 | -------------------------------------------------------------------------------- /components/admin/AdminLayout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | 3 | import { auth } from '@/lib/auth'; 4 | 5 | const AdminLayout = async ({ 6 | activeItem = 'dashboard', 7 | children, 8 | }: { 9 | activeItem: string; 10 | children: React.ReactNode; 11 | }) => { 12 | const session = await auth(); 13 | if (!session || !session.user.isAdmin) { 14 | return ( 15 |
16 |
17 |

Unauthorized

18 |

Admin permission required

19 |
20 |
21 | ); 22 | } 23 | 24 | return ( 25 |
26 |
27 |
28 |
    29 |
  • 30 | 34 | Dashboard 35 | 36 |
  • 37 |
  • 38 | 42 | Orders 43 | 44 |
  • 45 |
  • 46 | 50 | Products 51 | 52 |
  • 53 |
  • 54 | 58 | Users 59 | 60 |
  • 61 |
62 |
63 |
{children}
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default AdminLayout; 70 | -------------------------------------------------------------------------------- /components/carousel/carousel.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | import { 5 | Carousel as SCarousel, 6 | CarouselContent, 7 | CarouselItem, 8 | CarouselNext, 9 | CarouselPrevious, 10 | } from '@/components/ui/carousel'; 11 | import productService from '@/lib/services/productService'; 12 | import { delay } from '@/lib/utils'; 13 | 14 | const Carousel = async () => { 15 | await delay(3000); 16 | const featuredProducts = await productService.getFeatured(); 17 | 18 | return ( 19 | 20 | 21 | {featuredProducts.map((product) => ( 22 | 23 |
24 | 25 | {product.name} 36 | 37 |
38 |
39 | ))} 40 |
41 | 42 | 43 |
44 | ); 45 | }; 46 | 47 | export default Carousel; 48 | 49 | export const CarouselSkeleton = () => { 50 | return
; 51 | }; 52 | -------------------------------------------------------------------------------- /components/categories/Categories.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | import Overlay from './Overlay'; 5 | import Handbags from '../../public/images/categories/Handbags.webp'; 6 | import Pants from '../../public/images/categories/Pants.webp'; 7 | import Shirts from '../../public/images/categories/Shirts.webp'; 8 | 9 | const Categories = () => { 10 | return ( 11 |
12 | 16 | Collection of shirts 25 | 26 | 27 | 31 | Collection of pants 40 | 41 | 42 | 46 | Collection of handbags 55 | 56 | 57 |
58 | ); 59 | }; 60 | 61 | export default Categories; 62 | -------------------------------------------------------------------------------- /components/categories/Overlay.tsx: -------------------------------------------------------------------------------- 1 | const Overlay = ({ category }: { category: string }) => { 2 | return ( 3 |
4 | 5 | {category} 6 | 7 |
8 | ); 9 | }; 10 | 11 | export default Overlay; 12 | -------------------------------------------------------------------------------- /components/checkout/CheckoutSteps.tsx: -------------------------------------------------------------------------------- 1 | const CheckoutSteps = ({ current = 0 }) => { 2 | return ( 3 |
    4 | {['User Login', 'Shipping Address', 'Payment Method', 'Place Order'].map( 5 | (step, index) => ( 6 |
  • 10 | {step} 11 |
  • 12 | ), 13 | )} 14 |
15 | ); 16 | }; 17 | export default CheckoutSteps; 18 | -------------------------------------------------------------------------------- /components/footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | const Footer = () => { 2 | return ( 3 |
4 |

5 | Copyright © {new Date().getFullYear()} - All rights reserved by 6 | Fashion Corner 7 |

8 |
9 | ); 10 | }; 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /components/header/Header.tsx: -------------------------------------------------------------------------------- 1 | import { AlignJustify } from 'lucide-react'; 2 | import Link from 'next/link'; 3 | 4 | import Menu from './Menu'; 5 | import { SearchBox } from './SearchBox'; 6 | 7 | const Header = () => { 8 | return ( 9 |
10 | 29 |
30 | ); 31 | }; 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /components/header/Menu.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ChevronDown, Moon, ShoppingCart, Sun } from 'lucide-react'; 4 | import Link from 'next/link'; 5 | import { signOut, signIn, useSession } from 'next-auth/react'; 6 | 7 | import useCartService from '@/lib/hooks/useCartStore'; 8 | import useLayoutService from '@/lib/hooks/useLayout'; 9 | 10 | import { SearchBox } from './SearchBox'; 11 | 12 | const Menu = () => { 13 | const { items, init } = useCartService(); 14 | const { data: session } = useSession(); 15 | const { theme, toggleTheme } = useLayoutService(); 16 | 17 | const signOutHandler = () => { 18 | signOut({ callbackUrl: '/signin' }); 19 | init(); 20 | }; 21 | 22 | const handleClick = () => { 23 | (document.activeElement as HTMLElement).blur(); 24 | }; 25 | 26 | return ( 27 | <> 28 |
29 | 30 |
31 |
    32 |
  • 33 | 43 | 48 | 49 | 50 | {items.length !== 0 && ( 51 |
    52 | {items.reduce((a, c) => a + c.qty, 0)} 53 |
    54 | )} 55 |
    56 | 57 |
  • 58 | {session && session.user ? ( 59 |
  • 60 |
    61 | 65 |
      69 | {session.user.isAdmin && ( 70 |
    • 71 | Admin Dashboard 72 |
    • 73 | )} 74 | 75 |
    • 76 | Order history 77 |
    • 78 |
    • 79 | Profile 80 |
    • 81 |
    • 82 | 85 |
    • 86 |
    87 |
    88 |
  • 89 | ) : ( 90 |
  • 91 | 98 |
  • 99 | )} 100 |
101 | 102 | ); 103 | }; 104 | 105 | export default Menu; 106 | -------------------------------------------------------------------------------- /components/header/SearchBox.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useSearchParams } from 'next/navigation'; 4 | import { useRouter } from 'next-nprogress-bar'; 5 | import { useState } from 'react'; 6 | import useSWR from 'swr'; 7 | 8 | export const SearchBox = () => { 9 | const searchParams = useSearchParams(); 10 | const q = searchParams.get('q') || ''; 11 | const category = searchParams.get('category') || 'all'; 12 | const router = useRouter(); 13 | 14 | const [formCategory, setFormCategory] = useState(category); 15 | const [formQuery, setFormQuery] = useState(q); 16 | 17 | const { 18 | data: categories, 19 | error, 20 | isLoading, 21 | } = useSWR('/api/products/categories'); 22 | 23 | if (error) return error.message; 24 | 25 | if (isLoading) return
; 26 | 27 | const handleSubmit = (e: React.FormEvent) => { 28 | e.preventDefault(); 29 | router.push(`/search?category=${formCategory}&q=${formQuery}`); 30 | }; 31 | 32 | return ( 33 | 34 |
35 | 49 | setFormQuery(e.target.value)} 56 | /> 57 | 60 |
61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /components/icons/Icons.tsx: -------------------------------------------------------------------------------- 1 | import { Truck, Wallet, LockKeyhole, Phone } from 'lucide-react'; 2 | 3 | const Icons = () => { 4 | return ( 5 |
6 |
7 | 8 |
9 |

10 | Free Shipping 11 |

12 |

Order above $200

13 |
14 |
15 |
16 | 17 |
18 |

19 | Money-back 20 |

21 |

30 days guarantee0

22 |
23 |
24 |
25 | 26 |
27 |

28 | Secure Payments 29 |

30 |

Secured by Stripe

31 |
32 |
33 |
34 | 35 |
36 |

37 | 24/7 Support 38 |

39 |

Phone and Email support

40 |
41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Icons; 47 | -------------------------------------------------------------------------------- /components/products/AddToCart.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useRouter } from 'next/navigation'; 4 | import { useEffect, useState } from 'react'; 5 | 6 | import useCartService from '@/lib/hooks/useCartStore'; 7 | import { OrderItem } from '@/lib/models/OrderModel'; 8 | 9 | const AddToCart = ({ item }: { item: OrderItem }) => { 10 | const router = useRouter(); 11 | const { items, increase, decrease } = useCartService(); 12 | const [existItem, setExistItem] = useState(); 13 | 14 | useEffect(() => { 15 | setExistItem(items.find((x) => x.slug === item.slug)); 16 | }, [item, items]); 17 | 18 | const addToCartHandler = () => { 19 | increase(item); 20 | }; 21 | 22 | return existItem ? ( 23 |
24 | 27 | {existItem.qty} 28 | 31 |
32 | ) : ( 33 | 40 | ); 41 | }; 42 | 43 | export default AddToCart; 44 | -------------------------------------------------------------------------------- /components/products/ProductItem.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | import { getPlaiceholder } from 'plaiceholder'; 4 | 5 | import { Product } from '@/lib/models/ProductModel'; 6 | 7 | import { Rating } from './Rating'; 8 | 9 | const ProductItem = async ({ product }: { product: Product }) => { 10 | const buffer = await fetch(product.image).then(async (res) => 11 | Buffer.from(await res.arrayBuffer()), 12 | ); 13 | 14 | const { base64 } = await getPlaiceholder(buffer); 15 | 16 | return ( 17 |
18 |
19 | 23 | {product.name} 32 | 33 |
34 |
35 | 36 |

37 | {product.name} 38 |

39 | 40 | 41 |

{product.brand}

42 |
43 | ${product.price} 44 |
45 |
46 |
47 | ); 48 | }; 49 | 50 | export default ProductItem; 51 | -------------------------------------------------------------------------------- /components/products/ProductItems.tsx: -------------------------------------------------------------------------------- 1 | import productService from '@/lib/services/productService'; 2 | import { convertDocToObj, delay } from '@/lib/utils'; 3 | 4 | import ProductItem from './ProductItem'; 5 | 6 | const ProductItems = async () => { 7 | await delay(4000); 8 | const latestProducts = await productService.getLatest(); 9 | 10 | return ( 11 |
12 |

Latest Products

13 |
14 | {latestProducts.map((product) => ( 15 | 16 | ))} 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default ProductItems; 23 | 24 | const ProductItemSkeleton = () => { 25 | return ( 26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export const ProductItemsSkeleton = ({ 43 | qty, 44 | name, 45 | }: { 46 | qty: number; 47 | name: string; 48 | }) => { 49 | return ( 50 |
51 |

{name}

52 |
53 | {Array.from({ length: qty }).map((_, i) => { 54 | return ; 55 | })} 56 |
57 |
58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /components/products/Rating.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from '@/lib/utils'; 2 | 3 | export const Rating = ({ 4 | value, 5 | caption, 6 | isCard, 7 | }: { 8 | value: number; 9 | caption: string; 10 | isCard?: boolean; 11 | }) => { 12 | const Full = () => ( 13 | 18 | 19 | 20 | ); 21 | const Half = () => ( 22 | 27 | 28 | 29 | ); 30 | const Empty = () => ( 31 | 36 | 37 | 38 | ); 39 | 40 | return ( 41 |
45 |
46 | {value >= 1 ? : value >= 0.5 ? : } 47 | {value >= 2 ? : value >= 1.5 ? : } 48 | {value >= 3 ? : value >= 2.5 ? : } 49 | {value >= 4 ? : value >= 3.5 ? : } 50 | {value >= 5 ? : value >= 4.5 ? : } 51 |
52 | 53 | {caption &&
{caption}
} 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /components/readMore/ReadMore.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ChevronDown, ChevronUp } from 'lucide-react'; 4 | import { useState } from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | const ReadMore = ({ children }: { children: React.ReactNode }) => { 9 | const [isMore, setIsMore] = useState(false); 10 | 11 | return ( 12 |
13 |
18 | {!isMore && ( 19 |
20 | 27 |
28 | )} 29 | {children} 30 |
31 | {isMore && ( 32 | 36 | )} 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default ReadMore; 44 | -------------------------------------------------------------------------------- /components/readMore/Text.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Text = () => { 4 | return ( 5 |
6 |

7 | Fashion Corner: Leading E-Commerce Clothing Hub 8 |

9 |

10 | Your Ultimate Destination for Trendsetting Apparel 11 |

12 |

13 | At Fashion Corner, we believe that style is a way to say who you are 14 | without having to speak. Explore our vast selection of apparel, 15 | including trendy hoodies, elegant shirts, casual t-shirts, and much 16 | more, all crafted to enhance your wardrobe with style and 17 | sophistication. Whether you're updating your everyday look or 18 | shopping for a special occasion, our extensive collection ensures that 19 | every style preference and fashion need is catered to. 20 |

21 |

22 | Our commitment to quality and staying in tune with the latest trends 23 | ensures that every piece in our collection not only offers comfort but 24 | also a high-fashion aesthetic. From the perfect fit of our tailored 25 | shirts to the soft embrace of our casual tees, each item is meticulously 26 | designed with the finest materials to offer you the best in fashion and 27 | comfort. Our user-friendly online store makes shopping effortless, 28 | delivering your favorite styles right to your doorstep with just a few 29 | clicks. 30 |

31 |

32 | Dive into our diverse clothing lines to find your unique style. Whether 33 | it’s the casual comfort of our well-crafted denim jeans, the classic 34 | charm of our sweater collection, or the bold statement pieces from our 35 | limited edition designer collaborations, Fashion Corner has it all. Each 36 | product is showcased with detailed descriptions and high-quality images 37 | to give you a close-up view of the fabric, fit, and colors available. 38 |

39 |

40 | Stay ahead of fashion trends with our new arrivals that are constantly 41 | updated to keep your style fresh and exciting. Sign up for our 42 | newsletter to receive timely updates on the latest collections, seasonal 43 | sales, and exclusive discounts tailored just for you. At Fashion Corner, 44 | fashion meets convenience with a touch of elegance, providing you with 45 | an unrivaled online shopping experience. 46 |

47 |

48 | Why Choose Fashion Corner? 49 |

50 |

51 | At Fashion Corner, sustainability meets style. Our dedication to 52 | sustainable fashion sets us apart, making every purchase a testament to 53 | your commitment to environmental responsibility. Our eco-friendly 54 | materials and ethical production processes aim to minimize environmental 55 | impact while providing you with high-quality, durable clothing that you 56 | can feel good about. 57 |

58 |

59 | We also take pride in offering exceptional customer service. Our 60 | friendly and knowledgeable customer support team is always eager to 61 | assist you with any questions, ensuring a seamless shopping experience 62 | from start to finish. With our hassle-free returns and exchanges policy, 63 | shopping at Fashion Corner is completely worry-free. 64 |

65 |

66 | Moreover, our loyalty program rewards you for every purchase, turning 67 | every dollar spent into points that can be redeemed for discounts on 68 | future orders. Join our community of fashion enthusiasts and get 69 | exclusive access to VIP events and sneak peeks at upcoming products. 70 |

71 |

72 | Immerse yourself in the world of Fashion Corner today. Discover our 73 | curated selections, where each piece tells a story of quality 74 | craftsmanship and style excellence. Refresh your wardrobe with key 75 | pieces that you will love and cherish, and experience the perfect blend 76 | of style, quality, and sustainability. Shop at Fashion Corner now to see 77 | what fashion wonders await you. 78 |

79 |

80 | Discover Exclusive Styles Only Available at Fashion Corner 81 |

82 |

83 | Unearth a treasure trove of unique fashion pieces that you won't 84 | find anywhere else. Our exclusive collections are designed with the 85 | fashion-forward individual in mind, featuring limited edition apparel 86 | that makes a bold statement. From runway-inspired designs to avant-garde 87 | accessories, each item is a masterpiece that embodies creativity and 88 | distinction. 89 |

90 |

91 | Our focus on exclusive offerings ensures that our customers enjoy a 92 | distinct shopping experience that elevates their style to new heights. 93 | By continually partnering with innovative designers and brands, we bring 94 | fresh, dynamic collections that are at the forefront of fashion trends. 95 | Explore these unique styles and add a touch of uniqueness to your 96 | wardrobe that truly sets you apart from the crowd. 97 |

98 |

99 | Join the Fashion Corner family today and tap into the world of exclusive 100 | fashion. Let us be your guide to discovering new styles that inspire and 101 | empower you to express your individuality. Whether you're looking 102 | for something bold and expressive or subtle and sophisticated, find it 103 | at Fashion Corner. 104 |

105 |
106 | ); 107 | }; 108 | 109 | export default Text; 110 | -------------------------------------------------------------------------------- /components/slider/CardSlider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useState, useEffect } from 'react'; 4 | 5 | import { 6 | Carousel, 7 | CarouselContent, 8 | CarouselNext, 9 | CarouselPrevious, 10 | type CarouselApi, 11 | } from '@/components/ui/carousel'; 12 | 13 | interface IProducts { 14 | children: React.ReactNode; 15 | } 16 | 17 | const CardSlider = ({ children }: IProducts) => { 18 | const [api, setApi] = useState(); 19 | const [current, setCurrent] = useState(0); 20 | const [count, setCount] = useState(0); 21 | 22 | useEffect(() => { 23 | if (!api) { 24 | return; 25 | } 26 | 27 | setCount(api.scrollSnapList().length); 28 | setCurrent(api.selectedScrollSnap() + 1); 29 | 30 | api.on('select', () => { 31 | setCurrent(api.selectedScrollSnap() + 1); 32 | }); 33 | }, [api]); 34 | 35 | return ( 36 | 37 | 38 | {/* {products.map((product) => ( 39 | 43 | 44 | 45 | ))} */} 46 | {children} 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default CardSlider; 55 | -------------------------------------------------------------------------------- /components/slider/Slider.tsx: -------------------------------------------------------------------------------- 1 | import ProductItem from '@/components/products/ProductItem'; 2 | import CardSlider from '@/components/slider/CardSlider'; 3 | import { CarouselItem } from '@/components/ui/carousel'; 4 | import productService from '@/lib/services/productService'; 5 | import { convertDocToObj } from '@/lib/utils'; 6 | 7 | const Slider = async () => { 8 | const topRated = await productService.getTopRated(); 9 | 10 | return ( 11 |
12 |

Top Rated

13 | 14 | {/*Wrap for SSR */} 15 | {topRated.map((product) => ( 16 | 20 | 21 | 22 | ))} 23 | 24 |
25 | ); 26 | }; 27 | 28 | export default Slider; 29 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot'; 2 | import { cva, type VariantProps } from 'class-variance-authority'; 3 | import * as React from 'react'; 4 | 5 | import { cn } from '@/lib/utils'; 6 | 7 | const buttonVariants = cva( 8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90', 15 | outline: 16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', 17 | secondary: 18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80', 19 | ghost: 'hover:bg-accent hover:text-accent-foreground', 20 | link: 'text-primary underline-offset-4 hover:underline', 21 | }, 22 | size: { 23 | default: 'h-10 px-4 py-2', 24 | sm: 'h-9 rounded-md px-3', 25 | lg: 'h-11 rounded-md px-8', 26 | icon: 'h-10 w-10', 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: 'default', 31 | size: 'default', 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : 'button'; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = 'Button'; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /components/ui/carousel.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import useEmblaCarousel, { 4 | type UseEmblaCarouselType, 5 | } from 'embla-carousel-react'; 6 | import { ArrowLeft, ArrowRight } from 'lucide-react'; 7 | import * as React from 'react'; 8 | 9 | import { Button } from '@/components/ui/button'; 10 | import { cn } from '@/lib/utils'; 11 | 12 | type CarouselApi = UseEmblaCarouselType[1]; 13 | type UseCarouselParameters = Parameters; 14 | type CarouselOptions = UseCarouselParameters[0]; 15 | type CarouselPlugin = UseCarouselParameters[1]; 16 | 17 | type CarouselProps = { 18 | opts?: CarouselOptions; 19 | plugins?: CarouselPlugin; 20 | orientation?: 'horizontal' | 'vertical'; 21 | setApi?: (api: CarouselApi) => void; 22 | }; 23 | 24 | type CarouselContextProps = { 25 | carouselRef: ReturnType[0]; 26 | api: ReturnType[1]; 27 | scrollPrev: () => void; 28 | scrollNext: () => void; 29 | canScrollPrev: boolean; 30 | canScrollNext: boolean; 31 | } & CarouselProps; 32 | 33 | const CarouselContext = React.createContext(null); 34 | 35 | function useCarousel() { 36 | const context = React.useContext(CarouselContext); 37 | 38 | if (!context) { 39 | throw new Error('useCarousel must be used within a '); 40 | } 41 | 42 | return context; 43 | } 44 | 45 | const Carousel = React.forwardRef< 46 | HTMLDivElement, 47 | React.HTMLAttributes & CarouselProps 48 | >( 49 | ( 50 | { 51 | orientation = 'horizontal', 52 | opts, 53 | setApi, 54 | plugins, 55 | className, 56 | children, 57 | ...props 58 | }, 59 | ref, 60 | ) => { 61 | const [carouselRef, api] = useEmblaCarousel( 62 | { 63 | ...opts, 64 | axis: orientation === 'horizontal' ? 'x' : 'y', 65 | }, 66 | plugins, 67 | ); 68 | const [canScrollPrev, setCanScrollPrev] = React.useState(false); 69 | const [canScrollNext, setCanScrollNext] = React.useState(false); 70 | 71 | const onSelect = React.useCallback((api: CarouselApi) => { 72 | if (!api) { 73 | return; 74 | } 75 | 76 | setCanScrollPrev(api.canScrollPrev()); 77 | setCanScrollNext(api.canScrollNext()); 78 | }, []); 79 | 80 | const scrollPrev = React.useCallback(() => { 81 | api?.scrollPrev(); 82 | }, [api]); 83 | 84 | const scrollNext = React.useCallback(() => { 85 | api?.scrollNext(); 86 | }, [api]); 87 | 88 | const handleKeyDown = React.useCallback( 89 | (event: React.KeyboardEvent) => { 90 | if (event.key === 'ArrowLeft') { 91 | event.preventDefault(); 92 | scrollPrev(); 93 | } else if (event.key === 'ArrowRight') { 94 | event.preventDefault(); 95 | scrollNext(); 96 | } 97 | }, 98 | [scrollPrev, scrollNext], 99 | ); 100 | 101 | React.useEffect(() => { 102 | if (!api || !setApi) { 103 | return; 104 | } 105 | 106 | setApi(api); 107 | }, [api, setApi]); 108 | 109 | React.useEffect(() => { 110 | if (!api) { 111 | return; 112 | } 113 | 114 | onSelect(api); 115 | api.on('reInit', onSelect); 116 | api.on('select', onSelect); 117 | 118 | return () => { 119 | api?.off('select', onSelect); 120 | }; 121 | }, [api, onSelect]); 122 | 123 | return ( 124 | 137 |
145 | {children} 146 |
147 |
148 | ); 149 | }, 150 | ); 151 | Carousel.displayName = 'Carousel'; 152 | 153 | const CarouselContent = React.forwardRef< 154 | HTMLDivElement, 155 | React.HTMLAttributes 156 | >(({ className, ...props }, ref) => { 157 | const { carouselRef, orientation } = useCarousel(); 158 | 159 | return ( 160 |
161 |
170 |
171 | ); 172 | }); 173 | CarouselContent.displayName = 'CarouselContent'; 174 | 175 | const CarouselItem = React.forwardRef< 176 | HTMLDivElement, 177 | React.HTMLAttributes 178 | >(({ className, ...props }, ref) => { 179 | const { orientation } = useCarousel(); 180 | 181 | return ( 182 |
193 | ); 194 | }); 195 | CarouselItem.displayName = 'CarouselItem'; 196 | 197 | const CarouselPrevious = React.forwardRef< 198 | HTMLButtonElement, 199 | React.ComponentProps 200 | >(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { 201 | const { orientation, scrollPrev, canScrollPrev } = useCarousel(); 202 | 203 | return ( 204 | 222 | ); 223 | }); 224 | CarouselPrevious.displayName = 'CarouselPrevious'; 225 | 226 | const CarouselNext = React.forwardRef< 227 | HTMLButtonElement, 228 | React.ComponentProps 229 | >(({ className, variant = 'outline', size = 'icon', ...props }, ref) => { 230 | const { orientation, scrollNext, canScrollNext } = useCarousel(); 231 | 232 | return ( 233 | 251 | ); 252 | }); 253 | CarouselNext.displayName = 'CarouselNext'; 254 | 255 | export { 256 | type CarouselApi, 257 | Carousel, 258 | CarouselContent, 259 | CarouselItem, 260 | CarouselPrevious, 261 | CarouselNext, 262 | }; 263 | -------------------------------------------------------------------------------- /lib/auth.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import NextAuth from 'next-auth'; 3 | import CredentialsProvider from 'next-auth/providers/credentials'; 4 | 5 | import dbConnect from './dbConnect'; 6 | import UserModel from './models/UserModel'; 7 | 8 | export const config = { 9 | providers: [ 10 | CredentialsProvider({ 11 | credentials: { 12 | email: { 13 | type: 'email', 14 | }, 15 | password: { 16 | type: 'password', 17 | }, 18 | }, 19 | async authorize(credentials) { 20 | await dbConnect(); 21 | if (credentials === null) return null; 22 | 23 | const user = await UserModel.findOne({ email: credentials.email }); 24 | 25 | if (user) { 26 | const isMatch = await bcrypt.compare( 27 | credentials.password as string, 28 | user.password, 29 | ); 30 | if (isMatch) { 31 | return user; 32 | } 33 | } 34 | return null; 35 | }, 36 | }), 37 | ], 38 | // custom pages for sign in and register 39 | pages: { 40 | signIn: '/signin', 41 | newUser: '/register', 42 | error: '/error', 43 | }, 44 | callbacks: { 45 | async jwt({ user, trigger, session, token }: any) { 46 | if (user) { 47 | token.user = { 48 | _id: user._id, 49 | email: user.email, 50 | name: user.name, 51 | isAdmin: user.isAdmin, 52 | }; 53 | } 54 | if (trigger === 'update' && session) { 55 | token.user = { 56 | ...token.user, 57 | email: session.user.email, 58 | name: session.user.name, 59 | }; 60 | } 61 | return token; 62 | }, 63 | session: async ({ session, token }: any) => { 64 | if (token) { 65 | session.user = token.user; 66 | } 67 | return session; 68 | }, 69 | }, 70 | }; 71 | 72 | export const { 73 | handlers: { GET, POST }, 74 | auth, 75 | signIn, 76 | signOut, 77 | } = NextAuth(config); 78 | -------------------------------------------------------------------------------- /lib/data.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | const data = { 4 | users: [ 5 | { 6 | name: 'Admin', 7 | email: 'admin@admin.com', 8 | password: bcrypt.hashSync('admin123'), 9 | isAdmin: true, 10 | }, 11 | { 12 | name: 'Test', 13 | email: 'test@test.com', 14 | password: bcrypt.hashSync('test'), 15 | isAdmin: false, 16 | }, 17 | ], 18 | products: [ 19 | { 20 | name: 'Free Shirt', 21 | slug: 'free-shirt', 22 | category: 'Shirts', 23 | image: '/images/shirt1.jpg', 24 | price: 70, 25 | brand: 'Nike', 26 | rating: 4.5, 27 | numReviews: 8, 28 | countInStock: 20, 29 | description: 'A popular shirt', 30 | isFeatured: true, 31 | banner: '/images/banner/banner1.jpg', 32 | }, 33 | { 34 | name: 'Fit Shirt', 35 | slug: 'fit-shirt', 36 | category: 'Shirts', 37 | image: '/images/shirt2.jpg', 38 | price: 80, 39 | brand: 'Adidas', 40 | rating: 3.2, 41 | numReviews: 10, 42 | countInStock: 20, 43 | description: 'A popular shirt', 44 | isFeatured: true, 45 | banner: '/images/banner/banner2.jpg', 46 | }, 47 | // { 48 | // name: 'Slim Shirt', 49 | // slug: 'slim-shirt', 50 | // category: 'Shirts', 51 | // image: '/images/shirt3.jpg', 52 | // price: 90, 53 | // brand: 'Raymond', 54 | // rating: 4.5, 55 | // numReviews: 3, 56 | // countInStock: 20, 57 | // description: 'A popular shirt', 58 | // }, 59 | // { 60 | // name: 'Golf Pants', 61 | // slug: 'golf-pants', 62 | // category: 'Pants', 63 | // image: '/images/pants1.jpg', 64 | // price: 90, 65 | // brand: 'Oliver', 66 | // rating: 2.9, 67 | // numReviews: 13, 68 | // countInStock: 20, 69 | // description: 'Smart looking pants', 70 | // }, 71 | // { 72 | // name: 'Fit Pants', 73 | // slug: 'fit-pants', 74 | // category: 'Pants', 75 | // image: '/images/pants2.jpg', 76 | // price: 95, 77 | // brand: 'Zara', 78 | // rating: 3.5, 79 | // numReviews: 7, 80 | // countInStock: 20, 81 | // description: 'A popular pants', 82 | // }, 83 | // { 84 | // name: 'Classic Pants', 85 | // slug: 'classic-pants', 86 | // category: 'Pants', 87 | // image: '/images/pants3.jpg', 88 | // price: 75, 89 | // brand: 'Casely', 90 | // rating: 2.4, 91 | // numReviews: 14, 92 | // countInStock: 20, 93 | // description: 'A popular pants', 94 | // }, 95 | ], 96 | }; 97 | 98 | export default data; 99 | -------------------------------------------------------------------------------- /lib/dbConnect.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const dbConnect = async () => { 4 | try { 5 | await mongoose.connect(process.env.MONGO_URI!); 6 | } catch (error) { 7 | throw new Error('Connection Failed!'); 8 | } 9 | }; 10 | 11 | export default dbConnect; 12 | -------------------------------------------------------------------------------- /lib/hooks/useCartStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | 4 | import { OrderItem, ShippingAddress } from '../models/OrderModel'; 5 | import { round2 } from '../utils'; 6 | 7 | type Cart = { 8 | items: OrderItem[]; 9 | itemsPrice: number; 10 | taxPrice: number; 11 | shippingPrice: number; 12 | totalPrice: number; 13 | 14 | paymentMethod: string; 15 | shippingAddress: ShippingAddress; 16 | }; 17 | 18 | const initialState: Cart = { 19 | items: [], 20 | itemsPrice: 0, 21 | taxPrice: 0, 22 | shippingPrice: 0, 23 | totalPrice: 0, 24 | paymentMethod: 'PayPal', 25 | shippingAddress: { 26 | fullName: '', 27 | address: '', 28 | city: '', 29 | postalCode: '', 30 | country: '', 31 | }, 32 | }; 33 | 34 | export const cartStore = create()( 35 | persist(() => initialState, { 36 | name: 'cartStore', 37 | }), 38 | ); 39 | 40 | const useCartService = () => { 41 | const { 42 | items, 43 | itemsPrice, 44 | taxPrice, 45 | shippingPrice, 46 | totalPrice, 47 | paymentMethod, 48 | shippingAddress, 49 | } = cartStore(); 50 | 51 | return { 52 | items, 53 | itemsPrice, 54 | taxPrice, 55 | shippingPrice, 56 | totalPrice, 57 | paymentMethod, 58 | shippingAddress, 59 | increase: (item: OrderItem) => { 60 | const exist = items.find((x) => x.slug === item.slug); 61 | 62 | const updatedCartItems = exist 63 | ? items.map((x) => 64 | x.slug === item.slug ? { ...exist, qty: exist.qty + 1 } : x, 65 | ) 66 | : [...items, { ...item, qty: 1 }]; 67 | 68 | const { itemsPrice, shippingPrice, taxPrice, totalPrice } = 69 | calcPrice(updatedCartItems); 70 | cartStore.setState({ 71 | items: updatedCartItems, 72 | itemsPrice, 73 | shippingPrice, 74 | taxPrice, 75 | totalPrice, 76 | }); 77 | }, 78 | decrease: (item: OrderItem) => { 79 | const exist = items.find((x) => x.slug === item.slug); 80 | if (!exist) return; 81 | 82 | const updatedCartItems = 83 | exist.qty === 1 84 | ? items.filter((x) => x.slug !== item.slug) 85 | : items.map((x) => 86 | x.slug === item.slug ? { ...exist, qty: exist.qty - 1 } : x, 87 | ); 88 | 89 | const { itemsPrice, shippingPrice, taxPrice, totalPrice } = 90 | calcPrice(updatedCartItems); 91 | cartStore.setState({ 92 | items: updatedCartItems, 93 | itemsPrice, 94 | shippingPrice, 95 | taxPrice, 96 | totalPrice, 97 | }); 98 | }, 99 | saveShippingAddress: (shippingAddress: ShippingAddress) => { 100 | cartStore.setState({ 101 | shippingAddress, 102 | }); 103 | }, 104 | savePaymentMethod: (paymentMethod: string) => { 105 | cartStore.setState({ 106 | paymentMethod, 107 | }); 108 | }, 109 | clear: () => { 110 | cartStore.setState({ 111 | items: [], 112 | }); 113 | }, 114 | init: () => cartStore.setState(initialState), 115 | }; 116 | }; 117 | 118 | export default useCartService; 119 | 120 | export const calcPrice = (items: OrderItem[]) => { 121 | const itemsPrice = round2( 122 | items.reduce((acc, item) => acc + item.price * item.qty, 0), 123 | ), 124 | shippingPrice = round2(itemsPrice > 100 ? 0 : 100), 125 | taxPrice = round2(Number(0.15 * itemsPrice)), 126 | totalPrice = round2(itemsPrice + shippingPrice + taxPrice); 127 | return { itemsPrice, shippingPrice, taxPrice, totalPrice }; 128 | }; 129 | -------------------------------------------------------------------------------- /lib/hooks/useLayout.ts: -------------------------------------------------------------------------------- 1 | import { create } from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | 4 | type Layout = { 5 | theme: string; 6 | drawerOpen: boolean; 7 | }; 8 | const initialState: Layout = { 9 | theme: 'system', 10 | drawerOpen: false, 11 | }; 12 | 13 | export const layoutStore = create()( 14 | persist(() => initialState, { 15 | name: 'layoutStore', 16 | }), 17 | ); 18 | 19 | export default function useLayoutService() { 20 | const { theme, drawerOpen } = layoutStore(); 21 | 22 | return { 23 | theme, 24 | drawerOpen, 25 | toggleTheme: () => { 26 | layoutStore.setState({ 27 | theme: theme === 'dark' ? 'light' : 'dark', 28 | }); 29 | }, 30 | toggleDrawer: () => { 31 | layoutStore.setState({ 32 | drawerOpen: !drawerOpen, 33 | }); 34 | }, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /lib/models/OrderModel.ts: -------------------------------------------------------------------------------- 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 | items: [ 11 | { 12 | product: { 13 | type: mongoose.Schema.Types.ObjectId, 14 | ref: 'Product', 15 | required: true, 16 | }, 17 | name: { type: String, required: true }, 18 | slug: { type: String, required: true }, 19 | qty: { type: Number, required: true }, 20 | image: { type: String, required: true }, 21 | price: { type: Number, required: true }, 22 | }, 23 | ], 24 | shippingAddress: { 25 | fullName: { type: String, required: true }, 26 | address: { type: String, required: true }, 27 | city: { type: String, required: true }, 28 | postalCode: { type: String, required: true }, 29 | country: { type: String, required: true }, 30 | }, 31 | paymentMethod: { type: String, required: true }, 32 | paymentResult: { 33 | id: String, 34 | status: String, 35 | email_address: String, 36 | }, 37 | itemsPrice: { type: Number, required: true }, 38 | shippingPrice: { type: Number, required: true }, 39 | taxPrice: { type: Number, required: true }, 40 | totalPrice: { type: Number, required: true }, 41 | isPaid: { type: Boolean, required: true, default: false }, 42 | isDelivered: { type: Boolean, required: true, default: false }, 43 | paidAt: { type: Date }, 44 | deliveredAt: { type: Date }, 45 | }, 46 | { 47 | timestamps: true, 48 | }, 49 | ); 50 | 51 | const OrderModel = 52 | mongoose.models.Order || mongoose.model('Order', orderSchema); 53 | 54 | export default OrderModel; 55 | 56 | export type Order = { 57 | _id: string; 58 | user?: { name: string }; 59 | items: [OrderItem]; 60 | shippingAddress: { 61 | fullName: string; 62 | address: string; 63 | city: string; 64 | postalCode: string; 65 | country: string; 66 | }; 67 | paymentMethod: string; 68 | paymentResult?: { id: string; status: string; email_address: string }; 69 | itemsPrice: number; 70 | shippingPrice: number; 71 | taxPrice: number; 72 | totalPrice: number; 73 | isPaid: boolean; 74 | isDelivered: boolean; 75 | paidAt?: string; 76 | deliveredAt?: string; 77 | createdAt: string; 78 | }; 79 | 80 | export type OrderItem = { 81 | name: string; 82 | slug: string; 83 | qty: number; 84 | image: string; 85 | price: number; 86 | color: string; 87 | size: string; 88 | }; 89 | 90 | export type ShippingAddress = { 91 | fullName: string; 92 | address: string; 93 | city: string; 94 | postalCode: string; 95 | country: string; 96 | }; 97 | -------------------------------------------------------------------------------- /lib/models/ProductModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export type Product = { 4 | _id?: string; 5 | name: string; 6 | slug: string; 7 | image: string; 8 | banner?: string; 9 | price: number; 10 | brand: string; 11 | description: string; 12 | category: string; 13 | rating: number; 14 | numReviews: number; 15 | countInStock: number; 16 | colors?: []; 17 | sizes?: []; 18 | }; 19 | 20 | const productSchema = new mongoose.Schema( 21 | { 22 | name: { type: String, required: true }, 23 | slug: { type: String, required: true, unique: true }, 24 | category: { type: String, required: true }, 25 | image: { type: String, required: true }, 26 | price: { type: Number, required: true }, 27 | brand: { type: String, required: true }, 28 | rating: { type: Number, required: true, default: 0 }, 29 | numReviews: { type: Number, required: true, default: 0 }, 30 | countInStock: { type: Number, required: true, default: 0 }, 31 | description: { type: String, required: true }, 32 | isFeatured: { type: Boolean, default: false }, 33 | banner: String, 34 | }, 35 | { 36 | timestamps: true, 37 | }, 38 | ); 39 | 40 | const ProductModel = 41 | mongoose.models.Product || mongoose.model('Product', productSchema); 42 | 43 | export default ProductModel; 44 | -------------------------------------------------------------------------------- /lib/models/UserModel.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | export type User = { 4 | _id: string; 5 | name: string; 6 | email: string; 7 | isAdmin: boolean; 8 | }; 9 | 10 | const UserSchema = new mongoose.Schema( 11 | { 12 | name: { 13 | type: String, 14 | required: true, 15 | }, 16 | email: { 17 | type: String, 18 | required: true, 19 | unique: true, 20 | }, 21 | password: { 22 | type: String, 23 | required: true, 24 | }, 25 | isAdmin: { type: Boolean, required: true, default: false }, 26 | }, 27 | { timestamps: true }, 28 | ); 29 | 30 | const UserModel = mongoose.models?.User || mongoose.model('User', UserSchema); 31 | 32 | export default UserModel; 33 | -------------------------------------------------------------------------------- /lib/paypal.ts: -------------------------------------------------------------------------------- 1 | const base = process.env.PAYPAL_API_URL || 'https://api-m.sandbox.paypal.com'; 2 | 3 | export const paypal = { 4 | createOrder: async function createOrder(price: number) { 5 | const accessToken = await generateAccessToken(); 6 | const url = `${base}/v2/checkout/orders`; 7 | const response = await fetch(url, { 8 | method: 'post', 9 | headers: { 10 | 'Content-Type': 'application/json', 11 | Authorization: `Bearer ${accessToken}`, 12 | }, 13 | body: JSON.stringify({ 14 | intent: 'CAPTURE', 15 | purchase_units: [ 16 | { 17 | amount: { 18 | currency_code: 'USD', 19 | value: price, 20 | }, 21 | }, 22 | ], 23 | }), 24 | }); 25 | return handleResponse(response); 26 | }, 27 | capturePayment: async function capturePayment(orderId: string) { 28 | const accessToken = await generateAccessToken(); 29 | const url = `${base}/v2/checkout/orders/${orderId}/capture`; 30 | const response = await fetch(url, { 31 | method: 'post', 32 | headers: { 33 | 'Content-Type': 'application/json', 34 | Authorization: `Bearer ${accessToken}`, 35 | }, 36 | }); 37 | 38 | return handleResponse(response); 39 | }, 40 | }; 41 | 42 | async function generateAccessToken() { 43 | const { PAYPAL_CLIENT_ID, PAYPAL_APP_SECRET } = process.env; 44 | 45 | console.log(process.env.PAYPAL_CLIENT_ID, process.env.PAYPAL_APP_SECRET); 46 | const auth = Buffer.from(PAYPAL_CLIENT_ID + ':' + PAYPAL_APP_SECRET).toString( 47 | 'base64', 48 | ); 49 | const response = await fetch(`${base}/v1/oauth2/token`, { 50 | method: 'post', 51 | body: 'grant_type=client_credentials', 52 | headers: { 53 | Authorization: `Basic ${auth}`, 54 | }, 55 | }); 56 | 57 | const jsonData = await handleResponse(response); 58 | return jsonData.access_token; 59 | } 60 | 61 | async function handleResponse(response: any) { 62 | if (response.status === 200 || response.status === 201) { 63 | return response.json(); 64 | } 65 | 66 | const errorMessage = await response.text(); 67 | throw new Error(errorMessage); 68 | } 69 | -------------------------------------------------------------------------------- /lib/services/productService.ts: -------------------------------------------------------------------------------- 1 | import { cache } from 'react'; 2 | 3 | import dbConnect from '@/lib/dbConnect'; 4 | import ProductModel, { Product } from '@/lib/models/ProductModel'; 5 | 6 | export const revalidate = 3600; 7 | 8 | const getLatest = cache(async () => { 9 | await dbConnect(); 10 | const products = await ProductModel.find({}) 11 | .sort({ _id: -1 }) 12 | .limit(8) 13 | .lean(); // Converts the MongoDB documents to plain JavaScript objects 14 | return products as Product[]; 15 | }); 16 | 17 | const getTopRated = cache(async () => { 18 | await dbConnect(); 19 | const products = await ProductModel.find({}) 20 | .sort({ rating: -1 }) // Sort by rating in descending order 21 | .limit(8) 22 | .lean(); // Converts the MongoDB documents to plain JavaScript objects 23 | return products as Product[]; 24 | }); 25 | 26 | // intentionally disable Next.js Cache to better demo 27 | const getFeatured = async () => { 28 | await dbConnect(); 29 | const products = await ProductModel.find({ isFeatured: true }) 30 | .limit(3) 31 | .lean(); 32 | return products as Product[]; 33 | }; 34 | 35 | const getBySlug = cache(async (slug: string) => { 36 | await dbConnect(); 37 | const product = await ProductModel.findOne({ slug }).lean(); 38 | return product as Product; 39 | }); 40 | 41 | const PAGE_SIZE = 3; 42 | const getByQuery = cache( 43 | async ({ 44 | q, 45 | category, 46 | sort, 47 | price, 48 | rating, 49 | page = '1', 50 | }: { 51 | q: string; 52 | category: string; 53 | price: string; 54 | rating: string; 55 | sort: string; 56 | page: string; 57 | }) => { 58 | await dbConnect(); 59 | 60 | const queryFilter = 61 | q && q !== 'all' 62 | ? { 63 | name: { 64 | $regex: q, 65 | $options: 'i', 66 | }, 67 | } 68 | : {}; 69 | const categoryFilter = category && category !== 'all' ? { category } : {}; 70 | const ratingFilter = 71 | rating && rating !== 'all' 72 | ? { 73 | rating: { 74 | $gte: Number(rating), 75 | }, 76 | } 77 | : {}; 78 | // 10-50 79 | const priceFilter = 80 | price && price !== 'all' 81 | ? { 82 | price: { 83 | $gte: Number(price.split('-')[0]), 84 | $lte: Number(price.split('-')[1]), 85 | }, 86 | } 87 | : {}; 88 | const order: Record = 89 | sort === 'lowest' 90 | ? { price: 1 } 91 | : sort === 'highest' 92 | ? { price: -1 } 93 | : sort === 'toprated' 94 | ? { rating: -1 } 95 | : { _id: -1 }; 96 | 97 | const categories = await ProductModel.find().distinct('category'); 98 | const products = await ProductModel.find( 99 | { 100 | ...queryFilter, 101 | ...categoryFilter, 102 | ...priceFilter, 103 | ...ratingFilter, 104 | }, 105 | '-reviews', 106 | ) 107 | .sort(order) 108 | .skip(PAGE_SIZE * (Number(page) - 1)) 109 | .limit(PAGE_SIZE) 110 | .lean(); 111 | 112 | const countProducts = await ProductModel.countDocuments({ 113 | ...queryFilter, 114 | ...categoryFilter, 115 | ...priceFilter, 116 | ...ratingFilter, 117 | }); 118 | 119 | return { 120 | products: products as Product[], 121 | countProducts, 122 | page, 123 | pages: Math.ceil(countProducts / PAGE_SIZE), 124 | categories, 125 | }; 126 | }, 127 | ); 128 | 129 | const getCategories = cache(async () => { 130 | await dbConnect(); 131 | const categories = await ProductModel.find().distinct('category'); 132 | return categories; 133 | }); 134 | 135 | const productService = { 136 | getLatest, 137 | getFeatured, 138 | getBySlug, 139 | getByQuery, 140 | getCategories, 141 | getTopRated, 142 | }; 143 | 144 | export default productService; 145 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const round2 = (num: number) => { 9 | return Math.round((num + Number.EPSILON) * 100) / 100; 10 | }; 11 | 12 | export const convertDocToObj = (doc: any) => { 13 | doc._id = doc._id.toString(); 14 | return doc; 15 | }; 16 | 17 | export const formatNumber = (x: number) => { 18 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); 19 | }; 20 | 21 | export const formatId = (x: string) => { 22 | return `..${x.substring(20, 24)}`; 23 | }; 24 | 25 | export const delay = (ms: number) => 26 | new Promise((resolve) => setTimeout(resolve, ms)); 27 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from 'next-auth'; 2 | import type { NextAuthConfig } from 'next-auth'; 3 | 4 | const authConfig = { 5 | providers: [], 6 | callbacks: { 7 | authorized({ request, auth }: any) { 8 | const protectedPaths = [ 9 | /\/shipping/, 10 | /\/payment/, 11 | /\/place-order/, 12 | /\/profile/, 13 | /\/order\/(.*)/, 14 | /\/admin/, 15 | ]; 16 | const { pathname } = request.nextUrl; 17 | if (protectedPaths.some((p) => p.test(pathname))) return !!auth; 18 | return true; 19 | }, 20 | }, 21 | } satisfies NextAuthConfig; 22 | 23 | export const { auth: middleware } = NextAuth(authConfig); 24 | 25 | export const config = { 26 | matcher: [ 27 | /* 28 | * Match all request paths except for the ones starting with: 29 | * - api (API routes) 30 | * - _next/static (static files) 31 | * - _next/image (image optimization files) 32 | * - favicon.ico (favicon file) 33 | */ 34 | '/((?!api|_next/static|_next/image|favicon.ico).*)', 35 | ], 36 | }; 37 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import withPlaiceholder from '@plaiceholder/next'; 3 | 4 | /** @type {import('next').NextConfig} */ 5 | const nextConfig = { 6 | images: { 7 | remotePatterns: [ 8 | { 9 | protocol: 'https', 10 | hostname: 'res.cloudinary.com', 11 | }, 12 | { 13 | protocol: 'http', 14 | hostname: 'localhost', 15 | }, 16 | ], 17 | formats: ['image/avif', 'image/webp'], 18 | }, 19 | // experimental: { 20 | // ppr: true, 21 | // }, 22 | }; 23 | 24 | export default withPlaiceholder(nextConfig); 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fullstack-next-store", 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 | "@paypal/react-paypal-js": "^8.3.0", 13 | "@plaiceholder/next": "^3.0.0", 14 | "@radix-ui/react-slot": "^1.0.2", 15 | "bcryptjs": "^2.4.3", 16 | "chart.js": "^4.4.2", 17 | "class-variance-authority": "^0.7.0", 18 | "cloudinary": "^2.2.0", 19 | "clsx": "^2.1.1", 20 | "embla-carousel-react": "^8.0.4", 21 | "lucide-react": "^0.378.0", 22 | "mongoose": "^8.2.3", 23 | "next": "^14.1.4", 24 | "next-auth": "^5.0.0-beta.16", 25 | "next-nprogress-bar": "^2.3.11", 26 | "react": "18.2", 27 | "react-chartjs-2": "^5.2.0", 28 | "react-dom": "18.2", 29 | "react-hook-form": "^7.51.2", 30 | "react-hot-toast": "^2.4.1", 31 | "sharp": "^0.32.6", 32 | "swr": "^2.2.5", 33 | "tailwind-merge": "^2.3.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "zustand": "^4.5.2" 36 | }, 37 | "devDependencies": { 38 | "@types/bcryptjs": "^2.4.6", 39 | "@types/node": "^20", 40 | "@types/react": "^18", 41 | "@types/react-dom": "^18", 42 | "autoprefixer": "^10.0.1", 43 | "daisyui": "^4.9.0", 44 | "eslint": "^8", 45 | "eslint-config-next": "^15.0.0-rc.0", 46 | "eslint-plugin-import": "^2.29.1", 47 | "postcss": "^8", 48 | "prettier": "^3.2.5", 49 | "prettier-plugin-tailwindcss": "^0.5.14", 50 | "tailwindcss": "^3.3.0", 51 | "typescript": "^5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['prettier-plugin-tailwindcss'], 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | }; 6 | -------------------------------------------------------------------------------- /public/Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/images/banner/banner1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/images/banner/banner1.webp -------------------------------------------------------------------------------- /public/images/banner/banner2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/images/banner/banner2.webp -------------------------------------------------------------------------------- /public/images/categories/Handbags.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/images/categories/Handbags.webp -------------------------------------------------------------------------------- /public/images/categories/Pants.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/images/categories/Pants.webp -------------------------------------------------------------------------------- /public/images/categories/Shirts.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/images/categories/Shirts.webp -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/readme/Fashion-Corner-Fullstack-Next-js-Store-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/readme/Fashion-Corner-Fullstack-Next-js-Store-dark.webp -------------------------------------------------------------------------------- /public/readme/Fashion-Corner-Fullstack-Next-js-Store.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyxzb/Fashion-Corner-Next.js-Ecommerce/89856c6602621c04af685c38c2ed211b1f95d6f4/public/readme/Fashion-Corner-Fullstack-Next-js-Store.webp -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss'; 2 | 3 | const config: Config = { 4 | darkMode: ['class'], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: '', 12 | daisyui: { 13 | themes: [ 14 | { 15 | light: { 16 | ...require('daisyui/src/theming/themes')['light'], 17 | primary: '#fbbf24', 18 | '.toaster-con': { 19 | 'background-color': 'white', 20 | color: 'black', 21 | }, 22 | }, 23 | dark: { 24 | ...require('daisyui/src/theming/themes')['dark'], 25 | primary: '#fbbf24', 26 | '.toaster-con': { 27 | 'background-color': 'black', 28 | color: 'white', 29 | }, 30 | }, 31 | }, 32 | ], 33 | }, 34 | theme: { 35 | container: { 36 | center: true, 37 | padding: '2rem', 38 | screens: { 39 | '2xl': '1400px', 40 | }, 41 | }, 42 | extend: { 43 | colors: { 44 | border: 'hsl(var(--border))', 45 | input: 'hsl(var(--input))', 46 | ring: 'hsl(var(--ring))', 47 | background: 'hsl(var(--background))', 48 | foreground: 'hsl(var(--foreground))', 49 | primary: { 50 | DEFAULT: 'hsl(var(--primary))', 51 | foreground: 'hsl(var(--primary-foreground))', 52 | }, 53 | secondary: { 54 | DEFAULT: 'hsl(var(--secondary))', 55 | foreground: 'hsl(var(--secondary-foreground))', 56 | }, 57 | destructive: { 58 | DEFAULT: 'hsl(var(--destructive))', 59 | foreground: 'hsl(var(--destructive-foreground))', 60 | }, 61 | muted: { 62 | DEFAULT: 'hsl(var(--muted))', 63 | foreground: 'hsl(var(--muted-foreground))', 64 | }, 65 | accent: { 66 | DEFAULT: 'hsl(var(--accent))', 67 | foreground: 'hsl(var(--accent-foreground))', 68 | }, 69 | popover: { 70 | DEFAULT: 'hsl(var(--popover))', 71 | foreground: 'hsl(var(--popover-foreground))', 72 | }, 73 | card: { 74 | DEFAULT: 'hsl(var(--card))', 75 | foreground: 'hsl(var(--card-foreground))', 76 | }, 77 | }, 78 | borderRadius: { 79 | lg: 'var(--radius)', 80 | md: 'calc(var(--radius) - 2px)', 81 | sm: 'calc(var(--radius) - 4px)', 82 | }, 83 | keyframes: { 84 | 'accordion-down': { 85 | from: { height: '0' }, 86 | to: { height: 'var(--radix-accordion-content-height)' }, 87 | }, 88 | 'accordion-up': { 89 | from: { height: 'var(--radix-accordion-content-height)' }, 90 | to: { height: '0' }, 91 | }, 92 | }, 93 | animation: { 94 | 'accordion-down': 'accordion-down 0.2s ease-out', 95 | 'accordion-up': 'accordion-up 0.2s ease-out', 96 | }, 97 | }, 98 | }, 99 | plugins: [require('tailwindcss-animate'), require('daisyui')], 100 | } satisfies Config; 101 | 102 | export default config; 103 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [{ 20 | "name": "next" 21 | }], 22 | "paths": { 23 | "@/*": [ 24 | "./*" 25 | ] 26 | }, 27 | "target": "ES2017" 28 | }, 29 | "include": [ 30 | "next-env.d.ts", 31 | "**/*.ts", 32 | "**/*.tsx", 33 | ".next/types/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { DefaultSession, DefaultUser } from 'next-auth'; 2 | 3 | declare module 'next-auth' { 4 | interface Session { 5 | user: { 6 | _id?: string | null; 7 | isAdmin?: boolean; 8 | } & DefaultSession['user']; 9 | } 10 | 11 | export interface User extends DefaultUser { 12 | _id?: string; 13 | isAdmin?: boolean; 14 | } 15 | } 16 | --------------------------------------------------------------------------------