├── .eslintrc.json ├── .gitignore ├── .hintrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── app ├── (routes) │ ├── (home) │ │ ├── layouts │ │ │ ├── BooksSection.tsx │ │ │ ├── HeroSection.tsx │ │ │ └── InstagramSection.tsx │ │ └── page.tsx │ ├── about-us │ │ └── page.tsx │ ├── account │ │ ├── layouts │ │ │ ├── LoginForm.tsx │ │ │ └── RegisterForm.tsx │ │ └── page.tsx │ ├── cart │ │ ├── layouts │ │ │ ├── CartItems.tsx │ │ │ └── MobileCartTotal.tsx │ │ └── page.tsx │ ├── categories │ │ ├── CategoriesSection.tsx │ │ ├── [category] │ │ │ ├── layouts │ │ │ │ └── BooksContainer.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── checkout │ │ ├── layouts │ │ │ └── CheckoutSection.tsx │ │ └── page.tsx │ ├── item │ │ └── [slug] │ │ │ ├── layout.tsx │ │ │ ├── layouts │ │ │ ├── BookDetails.tsx │ │ │ └── RelatedBooks.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ └── wishlist │ │ ├── components │ │ ├── AddAllToCart.tsx │ │ ├── WishlistTable.tsx │ │ └── WishlistTitle.tsx │ │ └── page.tsx ├── components │ ├── AuthAlert.tsx │ ├── BookRow.tsx │ ├── Breadcrumb.tsx │ ├── CartDropdown.tsx │ ├── CheckoutButton.tsx │ ├── CollapsibleMenu.tsx │ ├── Footer.tsx │ ├── Input.tsx │ ├── ItemCard.tsx │ ├── NavBar.tsx │ ├── Pagination.tsx │ ├── SearchDialog.tsx │ ├── SocialGroup.tsx │ ├── Toast.tsx │ ├── TopBar.tsx │ └── loading-ui │ │ ├── BookDetailsSkeleton.tsx │ │ ├── CardSkeletons.tsx │ │ ├── CartDropdownSkeleton.tsx │ │ ├── CartItemSkeleton.tsx │ │ └── LoadingOverlay.tsx ├── globals.css ├── icons │ ├── AlertIcon.tsx │ ├── CancelIcon.tsx │ ├── CaretDownIcon.tsx │ ├── CartIcon.tsx │ ├── DownArrowIcon.tsx │ ├── EmptyBoxIcon.tsx │ ├── EmptyCartIcon.tsx │ ├── FacebookIcon.tsx │ ├── HeartIcon.tsx │ ├── InstagramIcon.tsx │ ├── LoadingIcon.tsx │ ├── MailIcon.tsx │ ├── MenuIcon.tsx │ ├── MinusIcon.tsx │ ├── PlusIcon.tsx │ ├── SearchIcon.tsx │ ├── SuccessIcon.tsx │ ├── TelegramIcon.tsx │ └── UserIcon.tsx ├── layout.tsx └── providers.tsx ├── backend ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .tmp │ └── data.db ├── README.md ├── config │ ├── admin.js │ ├── api.js │ ├── database.js │ ├── env │ │ └── production │ │ │ └── server.js │ ├── middlewares.js │ ├── plugins.js │ └── server.js ├── database │ └── migrations │ │ └── .gitkeep ├── favicon.png ├── package.json ├── pnpm-lock.yaml ├── public │ ├── robots.txt │ └── uploads │ │ └── .gitkeep ├── render.yaml └── src │ ├── admin │ ├── app.example.js │ └── webpack.config.example.js │ ├── api │ ├── .gitkeep │ ├── author │ │ ├── content-types │ │ │ └── author │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── author.js │ │ ├── routes │ │ │ └── author.js │ │ └── services │ │ │ └── author.js │ ├── book │ │ ├── content-types │ │ │ └── book │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── book.js │ │ ├── routes │ │ │ ├── book.js │ │ │ └── random.js │ │ └── services │ │ │ └── book.js │ ├── category │ │ ├── content-types │ │ │ └── category │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── category.js │ │ ├── routes │ │ │ └── category.js │ │ └── services │ │ │ └── category.js │ ├── customer │ │ ├── content-types │ │ │ └── customer │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── customer.js │ │ ├── routes │ │ │ └── customer.js │ │ └── services │ │ │ └── customer.js │ ├── order-detail │ │ ├── content-types │ │ │ └── order-detail │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── order-detail.js │ │ ├── routes │ │ │ └── order-detail.js │ │ └── services │ │ │ └── order-detail.js │ ├── order │ │ ├── content-types │ │ │ └── order │ │ │ │ └── schema.json │ │ ├── controllers │ │ │ └── order.js │ │ ├── routes │ │ │ └── order.js │ │ └── services │ │ │ └── order.js │ └── state │ │ ├── content-types │ │ └── state │ │ │ └── schema.json │ │ ├── controllers │ │ └── state.js │ │ ├── routes │ │ └── state.js │ │ └── services │ │ └── state.js │ ├── extensions │ ├── .gitkeep │ └── users-permissions │ │ └── content-types │ │ └── user │ │ └── schema.json │ └── index.js ├── lib ├── api │ └── axios.ts ├── hooks │ ├── index.ts │ ├── useCart.ts │ ├── useDebounce.ts │ ├── useMounted.ts │ └── useScroll.ts ├── store │ ├── client │ │ ├── authStore.ts │ │ ├── cartStore.ts │ │ ├── index.ts │ │ ├── toastStore.ts │ │ └── wishlistStore.ts │ └── server │ │ ├── books │ │ ├── queries.tsx │ │ └── types.ts │ │ └── categories │ │ ├── queries.tsx │ │ └── types.ts ├── types │ ├── Book.d.ts │ ├── Category.d.ts │ └── api.d.ts └── utils │ ├── navLinks.tsx │ ├── scrollToTop.tsx │ └── utilFuncs.ts ├── next-bookstore.png ├── next.config.js ├── package.json ├── pages └── api │ └── hello.ts ├── pnpm-lock.yaml ├── postcss.config.js ├── public ├── about.webp ├── books-collection.webp ├── cafe-book.webp ├── default-og.jpg ├── favicon.ico ├── icons │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── icon.png ├── instagram-photos │ ├── ig_1.jpg │ ├── ig_2.jpg │ ├── ig_3.jpg │ ├── ig_4.jpg │ └── ig_5.jpg ├── vercel.svg └── we-were-liars-book.jpeg ├── script.sh ├── tailwind.config.js └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "compat-api/css": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | /* 3 | 4 | # Except these files & folders 5 | !/app 6 | !/pages 7 | !.eslintrc.json 8 | !.prettierrc 9 | !tsconfig.json 10 | !next.config.js 11 | !package.json 12 | !postcss.config.js 13 | !tailwind.config.js 14 | !/lib 15 | !README.md 16 | !CHANGELOG.md -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "printWidth": 80, 6 | "singleQuote": false, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "es5", 9 | "bracketSpacing": true, 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "css.lint.unknownAtRules": "ignore", 5 | "workbench.settings.openDefaultSettings": true 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## 0.1.0 (2023-03-23) 6 | 7 | ### Features 8 | 9 | - Responsive Design 10 | - Search Functionality 11 | - Add To Cart 12 | - Add To Wishlist 13 | - SEO-friendly 14 | - Accessible 15 | - 9 Pages 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sat Naing 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next Bookstore (Beta) 2 | 3 | ![Next Bookstore by Sat Naing](next-bookstore.png) 4 | 5 | An e-commerce project for an online bookstore developed using NextJS 13 and its experimental `appDir`. Frontend UI is crafted with radix-ui and TailwindCSS. To manage server and client state, TanStack Query and Zustand are used respectively. StrapiCMS serves as the backend for this project. 6 | 7 | > I designed the entire UI/UX using Figma and created the database design myself. Additionally, I developed this web application from scratch, handling all aspects of the development process. 8 | 9 | ## Features 🔥 10 | 11 | - Responsive Design 12 | - Search Functionality 13 | - Add To Cart 14 | - Add To Wishlist 15 | - SEO-friendly 16 | - Accessible 17 | 18 | ## Features (Coming Soon 👀) 🚧 19 | 20 | The following are the features and functionalities to be added in the future. 21 | 22 | - Order Processing 23 | - Filtering Items 24 | - Better Pagination 25 | - Better Error Handling 26 | - Better Loading UI with Next 13 27 | - Accessibility enhancements 28 | - Security improvements 29 | - PWA? 🤷🏻‍♂️ 30 | - Testing? 🤷🏻‍♂️ 31 | 32 | ## Technologies Used 👨🏻‍💻 33 | 34 | - **NextJS 13 with appDir** - _frontend development_ 35 | - **TypeScript** - _type checking_ 36 | - **Radix UI** - _accessible components_ 37 | - **TailwindCSS** - _styling_ 38 | - **Zustand** - _client state_ 39 | - **Tanstack-Query & Axios** - _data fetching and server state_ 40 | - **React-hook-form** - _form management_ 41 | - **Eslint** - _linting_ 42 | - **Figma** - _UI/UX_ 43 | - **StrapiCMS** - _backend_ 44 | - **Cloudinary** - _image hosting_ 45 | - **Vercel & Railway** - _frontend & backend hosting_ 46 | 47 | ## Installation 🔮 48 | 49 | To run the project locally, follow these steps: 50 | 51 | Clone the repository: 52 | 53 | ```bash 54 | git clone https://github.com/satnaing/next-bookstore.git 55 | ``` 56 | 57 | Install dependencies for frontend: 58 | 59 | ```bash 60 | cd next-bookstore && npm install 61 | ``` 62 | 63 | Install dependencies for backend: 64 | 65 | ```bash 66 | cd backend && npm install 67 | ``` 68 | 69 | Start the frontend: (at the root /) 70 | 71 | ```bash 72 | npm run dev 73 | ``` 74 | 75 | Start the backend: 76 | 77 | ```bash 78 | cd backend && npm run develop 79 | ``` 80 | 81 | Open your browser and go to 82 | 83 | ## Important Note ⚠️ 84 | 85 | I have intentionally committed the `backend/.tmp/data.db` file to the GitHub repository. This is because I did not want to set up a separate database for the project and connect it to my free backend hosting. Please note that this approach is not recommended for production-level applications. In a real-world scenario, I would use a more robust DBMS like PostgreSQL, with proper hosting and security configurations. 86 | 87 | ## Photo Credits 📸 88 | 89 | - Hero section image: [Photo by Evgeny Tchebotarev from Pexels](https://www.pexels.com/photo/the-world-atlas-of-coffee-book-2187601/) 90 | - About Page background: [Photo by Min An from Pexels](https://www.pexels.com/photo/pile-of-assorted-novel-books-694740/) 91 | - About Page other image: [Photo by Marta Dzedyshko from Pexels](https://www.pexels.com/photo/assorted-title-books-collection-2067569/) 92 | 93 | ## Contributing ✨ 94 | 95 | Contributions are welcome! If you find a bug or want to suggest an improvement, please open an issue or submit a pull request. 96 | 97 | ## License 📜 98 | 99 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 100 | -------------------------------------------------------------------------------- /app/(routes)/(home)/layouts/BooksSection.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import BookRow from "@/components/BookRow" 5 | import CaretDownIcon from "@/icons/CaretDownIcon" 6 | import { useCategories } from "@/store/server/categories/queries" 7 | import { Books } from "@/store/server/books/types" 8 | import { Categories } from "@/store/server/categories/types" 9 | 10 | const BooksSection = ({ 11 | categories, 12 | books, 13 | }: { 14 | categories: Categories 15 | books: Record 16 | }) => { 17 | const { data } = useCategories({ categories, featured: true }) 18 | 19 | return ( 20 |
21 | {data.map(({ name, slug }) => ( 22 |
23 |
24 |

25 | {name} 26 |

27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | ))} 35 |
36 | ) 37 | } 38 | 39 | type SeeAllType = { 40 | href: string 41 | bottom?: boolean 42 | } 43 | 44 | const SeeAll = ({ href, bottom = false }: SeeAllType) => ( 45 | 51 | See All 52 | 53 | 54 | ) 55 | 56 | export default BooksSection 57 | -------------------------------------------------------------------------------- /app/(routes)/(home)/layouts/HeroSection.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import Image from "next/image" 3 | import SocialGroup from "@/components/SocialGroup" 4 | import DownArrowIcon from "@/icons/DownArrowIcon" 5 | import cafeBookPic from "@/public/cafe-book.webp" 6 | 7 | const HeroSection = () => { 8 | return ( 9 |
10 |
11 | Open Book 17 |
18 |
19 |

20 | Best Place to Find
21 | Your Favourite
22 | Books. 23 |

24 | 25 |

26 | Unleash your imagination with our online bookstore! Discover a vast 27 | selection of books for all ages and interests, with something for 28 | everyone. Shop now and find your next favorite read! 29 |

30 | 31 | 40 | 41 | 42 | 43 |
44 |
Fast Delivery
45 |
Exclusive Deals
46 |
Curated Collections
47 |
48 |
49 |
50 | ) 51 | } 52 | 53 | export default HeroSection 54 | -------------------------------------------------------------------------------- /app/(routes)/(home)/layouts/InstagramSection.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import igOne from "@/public/instagram-photos/ig_1.jpg" 3 | import igTwo from "@/public/instagram-photos/ig_2.jpg" 4 | import igThree from "@/public/instagram-photos/ig_3.jpg" 5 | import igFour from "@/public/instagram-photos/ig_4.jpg" 6 | import igFive from "@/public/instagram-photos/ig_5.jpg" 7 | 8 | const igPhotos = [ 9 | { id: 1, image: igOne, desc: "Instagram Photo 1" }, 10 | { id: 2, image: igTwo, desc: "Instagram Photo 2" }, 11 | { id: 3, image: igThree, desc: "Instagram Photo 3" }, 12 | { id: 4, image: igFour, desc: "Instagram Photo 4" }, 13 | { id: 5, image: igFive, desc: "Instagram Photo 5" }, 14 | ] 15 | 16 | const InstagramSection = () => { 17 | return ( 18 |
19 |

20 | # Follow Us on Instagram 21 |

22 |
23 | {igPhotos.map(photo => ( 24 | {photo.desc} 30 | ))} 31 |
32 |
33 | ) 34 | } 35 | 36 | export default InstagramSection 37 | -------------------------------------------------------------------------------- /app/(routes)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import BooksSection from "./layouts/BooksSection" 2 | import HeroSection from "./layouts/HeroSection" 3 | import InstagramSection from "./layouts/InstagramSection" 4 | import { getCategories } from "@/store/server/categories/queries" 5 | import { getInitialBooks } from "@/utils/utilFuncs" 6 | 7 | export default async function Home() { 8 | const categories = await getCategories(true) 9 | const books = await getInitialBooks(categories) 10 | 11 | return ( 12 |
13 | 14 | 15 | 16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/(routes)/about-us/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Breadcrumb from "@/components/Breadcrumb" 3 | import aboutBanner from "@/public/about.webp" 4 | import booksCollection from "@/public/books-collection.webp" 5 | 6 | export const metadata = { 7 | title: "About", 8 | openGraph: { 9 | title: "About", 10 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/about-us`, 11 | }, 12 | twitter: { title: "About" }, 13 | } 14 | 15 | export default function Page() { 16 | return ( 17 | <> 18 |
19 | Pile of books 25 |

26 | About Us 27 |

28 |
29 |
30 | 31 |
32 |
33 |

Our Mission

34 |

35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent 36 | volutpat odio et dapibus dignissim. Praesent maximus tincidunt 37 | ultricies. Nam sodales dolor arcu, non venenatis odio tempor eu 38 |

39 |
40 |
41 |

What We Are

42 |

43 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent 44 | volutpat odio et dapibus dignissim. Praesent maximus tincidunt 45 | ultricies. Nam sodales dolor arcu, non venenatis odio tempor eu 46 |

47 |

48 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent 49 | volutpat odio et dapibus dignissim. Praesent maximus tincidunt 50 | ultricies. Nam sodales dolor arcu, non venenatis odio tempor eu 51 |

52 |
53 |
54 | Books Collection 59 |
60 |
61 |
62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/(routes)/account/layouts/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import Link from "next/link" 5 | import axios, { AxiosError } from "axios" 6 | import { useMutation } from "@tanstack/react-query" 7 | import { SubmitHandler, useForm } from "react-hook-form" 8 | import Input from "@/components/Input" 9 | import AuthAlert from "@/components/AuthAlert" 10 | import AlertIcon from "@/icons/AlertIcon" 11 | import scrollToTop from "@/utils/scrollToTop" 12 | import { useAuthStore } from "@/store/client" 13 | 14 | type Inputs = { 15 | identifier: string 16 | password: string 17 | } 18 | 19 | export default function LoginForm() { 20 | const { 21 | register, 22 | handleSubmit, 23 | formState: { errors }, 24 | } = useForm() 25 | 26 | const { setToken } = useAuthStore() 27 | 28 | const [errorMsg, setErrorMsg] = React.useState(null) 29 | const [fullName, setFullName] = React.useState(null) 30 | const [open, setOpen] = React.useState(false) 31 | 32 | const mutation = useMutation({ 33 | mutationFn: (userData: Inputs) => 34 | axios.post( 35 | `${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/local/`, 36 | userData 37 | ), 38 | onError: (error: Error | AxiosError) => { 39 | if (axios.isAxiosError(error)) { 40 | if (error.code === "ERR_NETWORK") { 41 | setErrorMsg("Network error occurs.") 42 | } else if (error.code === "ERR_BAD_REQUEST") { 43 | setErrorMsg( 44 | error.response?.data.error.message.replace("identifier", "email") 45 | ) 46 | } else { 47 | setErrorMsg("An error occurs.") 48 | } 49 | scrollToTop() 50 | } else { 51 | // Just a stock error 52 | setErrorMsg("Unknown error occurs.") 53 | } 54 | }, 55 | onSuccess: (data, variables, context) => { 56 | setToken(data.data.jwt) 57 | setFullName(data.data.user.fullName) 58 | setErrorMsg(null) 59 | setOpen(true) 60 | }, 61 | }) 62 | 63 | const onSubmit: SubmitHandler = data => { 64 | mutation.mutate(data) 65 | } 66 | 67 | return ( 68 | <> 69 |
70 |

Login

71 | {errorMsg && ( 72 | 73 | {errorMsg} 74 | 75 | )} 76 |
77 | 93 | 94 | 110 | 111 |
112 | 116 | Forgot your password? 117 | 118 |
119 | 120 | 126 |
127 |
128 | 129 | 135 | 136 | ) 137 | } 138 | -------------------------------------------------------------------------------- /app/(routes)/account/layouts/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import Error from "next/error" 5 | import axios, { AxiosError } from "axios" 6 | import { useMutation } from "@tanstack/react-query" 7 | import { SubmitHandler, useForm } from "react-hook-form" 8 | import AuthAlert from "@/components/AuthAlert" 9 | import Input from "@/components/Input" 10 | import AlertIcon from "@/icons/AlertIcon" 11 | import scrollToTop from "@/utils/scrollToTop" 12 | import { useAuthStore } from "@/store/client" 13 | 14 | type Inputs = { 15 | fullName: string 16 | email: string 17 | username: string 18 | phone: string 19 | password: string 20 | confirmPassword: string 21 | address: string 22 | } 23 | 24 | export default function RegisterForm() { 25 | const { 26 | register, 27 | handleSubmit, 28 | watch, 29 | formState: { errors }, 30 | } = useForm() 31 | const { setToken } = useAuthStore() 32 | 33 | const [errorMsg, setErrorMsg] = useState(null) 34 | const [open, setOpen] = useState(false) 35 | 36 | const mutation = useMutation({ 37 | mutationFn: (userData: Inputs) => 38 | axios.post( 39 | `${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/local/register`, 40 | userData 41 | ), 42 | onError: (error: Error | AxiosError) => { 43 | if (axios.isAxiosError(error)) { 44 | if (error.code === "ERR_NETWORK") { 45 | setErrorMsg("Network error occurs.") 46 | } else if (error.code === "ERR_BAD_REQUEST") { 47 | console.log(error) 48 | const errMsg = (error?.response?.data as any).error?.message || "" 49 | setErrorMsg(errMsg.replace("This attribute", "Phone")) 50 | } else { 51 | setErrorMsg("An error occurs.") 52 | } 53 | scrollToTop() 54 | } else { 55 | // Just a stock error 56 | setErrorMsg("Unknown error occurs.") 57 | } 58 | }, 59 | onSuccess: (data, variables, context) => { 60 | setToken(data.data.jwt) 61 | setErrorMsg(null) 62 | setOpen(true) 63 | }, 64 | }) 65 | 66 | const onSubmit: SubmitHandler = data => { 67 | mutation.mutate(data) 68 | } 69 | 70 | return ( 71 | <> 72 |
73 |

Register

74 | {errorMsg && ( 75 | 76 | {errorMsg} 77 | 78 | )} 79 |
80 | 95 | 96 | 112 | 113 | 132 | 133 | 148 | 149 | 165 | 166 | { 177 | if (watch("password") != val) { 178 | return "Passwords do no match" 179 | } 180 | }, 181 | })} 182 | /> 183 | 184 |
185 |
158 |
159 |
160 | 166 | 171 | 172 | Coupon code will be applied on the checkout 173 | 174 |
175 |
176 | 182 |
183 |
184 | Total Price : 185 | {totalPrice} Ks 186 |
187 | 192 |
193 | 194 | 195 |
196 | ) 197 | } 198 | -------------------------------------------------------------------------------- /app/(routes)/cart/layouts/MobileCartTotal.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import CheckoutButton from "@/components/CheckoutButton" 4 | import { useCart } from "@/hooks" 5 | 6 | export default function CartTotalMobile() { 7 | const { cartData, totalPrice } = useCart() 8 | 9 | return ( 10 |
11 |
12 | Total Price: 13 | {totalPrice} Ks 14 |
15 | 20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/(routes)/cart/page.tsx: -------------------------------------------------------------------------------- 1 | import CartItemSection from "app/(routes)/cart/layouts/CartItems" 2 | import MobileCartTotal from "app/(routes)/cart/layouts/MobileCartTotal" 3 | 4 | export const metadata = { 5 | title: "Cart", 6 | openGraph: { 7 | title: "Cart", 8 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/cart`, 9 | }, 10 | twitter: { title: "Cart" }, 11 | } 12 | 13 | export default function Page() { 14 | return ( 15 | <> 16 |
17 | 18 |
19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/(routes)/categories/CategoriesSection.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import BookRow from "@/components/BookRow" 5 | import CaretDownIcon from "@/icons/CaretDownIcon" 6 | import { useCategories } from "@/store/server/categories/queries" 7 | import { Categories } from "@/store/server/categories/types" 8 | import { Books } from "@/store/server/books/types" 9 | 10 | interface CategoriesSectionProps { 11 | categories: Categories 12 | books: Record 13 | } 14 | 15 | export default function CategoriesSection({ 16 | categories, 17 | books, 18 | }: CategoriesSectionProps) { 19 | const { data } = useCategories({ categories }) 20 | 21 | return ( 22 | <> 23 | {data.length > 0 && 24 | data.map(({ name, slug }) => ( 25 |
26 |
27 |

28 | {name} 29 |

30 | 31 |
32 | 33 |
34 | 35 |
36 |
37 | ))} 38 | 39 | ) 40 | } 41 | 42 | type SeeAllType = { 43 | href: string 44 | bottom?: boolean 45 | } 46 | 47 | const SeeAll = ({ href, bottom = false }: SeeAllType) => ( 48 | 54 | See All 55 | 56 | 57 | ) 58 | -------------------------------------------------------------------------------- /app/(routes)/categories/[category]/layouts/BooksContainer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useQuery } from "@tanstack/react-query" 4 | import ItemCard from "app/components/ItemCard" 5 | import Pagination from "app/components/Pagination" 6 | import CardSkeletons from "@/loading-ui/CardSkeletons" 7 | import scrollToTop from "@/utils/scrollToTop" 8 | import { getOptimizedImage } from "@/utils/utilFuncs" 9 | import { Books } from "@/store/server/books/types" 10 | import { 11 | getBooksByCategory, 12 | useBooksByCategory, 13 | } from "@/store/server/books/queries" 14 | 15 | type Props = { 16 | initialData: Books 17 | category: string 18 | currentPage: number 19 | } 20 | 21 | export default function BooksContainer({ 22 | initialData, 23 | category, 24 | currentPage, 25 | }: Props) { 26 | const { data, isLoading, isError } = useBooksByCategory({ 27 | slug: category, 28 | pageNum: currentPage, 29 | initialData, 30 | }) 31 | 32 | if (isLoading || isError) return 33 | 34 | const { page, pageSize, pageCount, total } = data.meta.pagination 35 | 36 | const startItem = (page - 1) * pageSize + 1 37 | const lastItem = page * pageSize < total ? page * pageSize : total 38 | 39 | return ( 40 |
scrollToTop()}> 41 |
42 | {data.data.map(({ id, attributes }) => { 43 | const { slug, price, title, image } = attributes 44 | 45 | return ( 46 | 55 | ) 56 | })} 57 |
58 |
59 | 60 | Showing {startItem} ~ {lastItem} of {total} 61 | 62 | {pageCount > 1 && ( 63 | 64 | )} 65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/(routes)/categories/[category]/page.tsx: -------------------------------------------------------------------------------- 1 | import BooksContainer from "./layouts/BooksContainer" 2 | // import { getBooksByCategory } from "@/lib/api" 3 | import Breadcrumb from "@/components/Breadcrumb" 4 | import { getBooksByCategory } from "@/store/server/books/queries" 5 | import { getCategoryBySlug } from "@/store/server/categories/queries" 6 | 7 | let mockBooks: number[] = [] 8 | for (let index = 1; index < 21; index++) { 9 | mockBooks.push(index) 10 | } 11 | 12 | type Props = { 13 | params: { category: string } 14 | searchParams: { page: number } 15 | } 16 | 17 | type MetaProps = { 18 | params: { category: string } 19 | } 20 | 21 | export async function generateMetadata({ params }: MetaProps) { 22 | const bookData = await getCategoryBySlug(params.category) 23 | const title = bookData.data[0].attributes.name 24 | console.log(bookData) 25 | return { 26 | title, 27 | openGraph: { 28 | title, 29 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/categories/${params.category}`, 30 | }, 31 | twitter: { title }, 32 | } 33 | } 34 | 35 | export default async function Page({ params, searchParams }: Props) { 36 | const currentPage = Number(searchParams.page) || 1 37 | 38 | const initialData = await getBooksByCategory({ 39 | slug: params.category, 40 | pageNum: currentPage, 41 | }) 42 | 43 | return ( 44 |
45 | 46 |

47 | {params.category} 48 |

49 | 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /app/(routes)/categories/page.tsx: -------------------------------------------------------------------------------- 1 | import Breadcrumb from "@/components/Breadcrumb" 2 | import { getCategories } from "@/store/server/categories/queries" 3 | import CategoriesSection from "./CategoriesSection" 4 | import { getInitialBooks } from "@/utils/utilFuncs" 5 | 6 | export default async function Home() { 7 | const categories = await getCategories() 8 | const books = await getInitialBooks(categories) 9 | 10 | return ( 11 |
12 | 13 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(routes)/checkout/page.tsx: -------------------------------------------------------------------------------- 1 | import CheckoutSection from "./layouts/CheckoutSection" 2 | 3 | export const metadata = { 4 | title: "Checkout", 5 | openGraph: { 6 | title: "Checkout", 7 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/checkout`, 8 | }, 9 | twitter: { title: "Checkout" }, 10 | } 11 | 12 | export default function Page() { 13 | return ( 14 | <> 15 |
16 | 17 |
18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /app/(routes)/item/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getBook } from "@/store/server/books/queries" 2 | 3 | type MetaProps = { 4 | params: { slug: string } 5 | } 6 | 7 | export async function generateMetadata({ params }: MetaProps) { 8 | const bookData = await getBook(params.slug) 9 | const title = bookData.data[0].attributes.title 10 | 11 | return { 12 | title, 13 | openGraph: { 14 | title, 15 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/item/${params.slug}`, 16 | }, 17 | twitter: { title }, 18 | } 19 | } 20 | 21 | export default function ItemLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode 25 | }) { 26 | return
{children}
27 | } 28 | -------------------------------------------------------------------------------- /app/(routes)/item/[slug]/layouts/BookDetails.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import Image from "next/image" 5 | import Link from "next/link" 6 | import ReactMarkdown from "react-markdown" 7 | import SocialGroup from "@/components/SocialGroup" 8 | import BookDetailsSkeleton from "@/loading-ui/BookDetailsSkeleton" 9 | import HeartIcon from "@/icons/HeartIcon" 10 | import LoadingIcon from "@/icons/LoadingIcon" 11 | import { useMounted } from "@/hooks" 12 | import { useCartStore, useToastStore, useWishlistStore } from "@/store/client" 13 | import { Books } from "@/store/server/books/types" 14 | import { useBook } from "@/store/server/books/queries" 15 | 16 | type Props = { 17 | slug: string 18 | initialData: Books 19 | } 20 | 21 | export default function BookDetails({ slug, initialData }: Props) { 22 | // quantity state 23 | const [quantity, setQuantity] = useState(1) 24 | 25 | // client global state 26 | const { addToCart } = useCartStore() 27 | const { wishlist, toggleWishlist } = useWishlistStore() 28 | const { setToast } = useToastStore() 29 | 30 | const mounted = useMounted() 31 | 32 | const { data, isLoading, isError } = useBook({ initialData, slug }) 33 | 34 | if (isLoading || isError) return 35 | 36 | const id = data.data[0].id 37 | const bookData = data.data[0].attributes 38 | const bookImageObj = bookData.image.data[0].attributes 39 | const authorName = bookData.author_id.data.attributes.name 40 | const categories: { name: string; slug: string }[] = 41 | bookData.categories.data.map(category => ({ 42 | name: category.attributes.name, 43 | slug: category.attributes.slug, 44 | })) 45 | 46 | const handleAddToCart = () => { 47 | addToCart({ id, quantity }) 48 | setToast({ 49 | status: "success", 50 | message: "The book has been added to cart", 51 | }) 52 | } 53 | 54 | const hasWishlisted = wishlist.find(item => item.id === id) 55 | const handleAddToWishlist = () => { 56 | setToast({ 57 | status: hasWishlisted ? "info" : "success", 58 | message: `The book has been ${ 59 | hasWishlisted ? "removed from" : "added to" 60 | } wishlist`, 61 | }) 62 | toggleWishlist(id) 63 | } 64 | 65 | return ( 66 |
67 |
70 |
71 | {bookData.title} 78 |
79 |
80 |
81 |

{bookData.title}

82 |
83 | {bookData.description} 84 |
85 | 86 |
87 | 88 |
89 |
Author :
90 |
{authorName}
91 | 92 |
Categories :
93 |
94 | {categories.map((category, index) => ( 95 | 96 | {index > 0 ? ", " : ""} 97 | 101 | {category.name} 102 | 103 | 104 | ))} 105 |
106 | 107 |
Availibility :
108 |
109 | {bookData.in_stock ? "In Stock" : "Waiting time 2 weeks"} 110 |
111 |
112 | 113 |
114 |
115 | 123 | {quantity} 124 | 132 |
133 | 134 | MMK {bookData.price.toLocaleString()} 135 | 136 |
137 | 138 |
139 | 146 | 166 |
167 | 168 | 169 |
170 |
171 | ) 172 | } 173 | -------------------------------------------------------------------------------- /app/(routes)/item/[slug]/layouts/RelatedBooks.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import ItemCard from "@/components/ItemCard" 4 | import CardSkeletons from "@/components/loading-ui/CardSkeletons" 5 | import { useRelatedBooks } from "@/store/server/books/queries" 6 | 7 | type Props = { 8 | currentBookId: number 9 | author: number 10 | categories: number[] 11 | } 12 | 13 | export default function RelatedBooks({ 14 | currentBookId, 15 | author, 16 | categories, 17 | }: Props) { 18 | const { data, isLoading, isError } = useRelatedBooks({ author, categories }) 19 | 20 | const relatedBooks = data?.data 21 | .filter(book => book.id !== currentBookId) 22 | .sort(() => Math.random() - 0.5) 23 | .slice(0, 5) 24 | 25 | return ( 26 |
27 |

Related Books

28 | {isLoading || isError ? ( 29 | 30 | ) : ( 31 |
32 | {relatedBooks?.map(({ id, attributes }) => { 33 | const { slug, price, title, image } = attributes 34 | return ( 35 | 44 | ) 45 | })} 46 |
47 | )} 48 |
49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /app/(routes)/item/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import BookDetailsSkeleton from "@/components/loading-ui/BookDetailsSkeleton" 2 | 3 | export default function Loading() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /app/(routes)/item/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getBook } from "@/store/server/books/queries" 2 | import BookDetails from "./layouts/BookDetails" 3 | import RelatedBooks from "./layouts/RelatedBooks" 4 | 5 | let mockBooks: number[] = [] 6 | for (let index = 1; index < 21; index++) { 7 | mockBooks.push(index) 8 | } 9 | 10 | type Props = { 11 | params: { slug: string } 12 | } 13 | 14 | export default async function Page({ params }: Props) { 15 | const initialData = await getBook(params.slug) 16 | 17 | const data = initialData.data[0] 18 | const currentBookId = Number(data.id) 19 | const authorId = Number(data.attributes.author_id.data.id) 20 | const categoryIds = data.attributes.categories.data.map(({ id }) => 21 | Number(id) 22 | ) 23 | 24 | return ( 25 | <> 26 | 27 |
28 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /app/(routes)/wishlist/components/AddAllToCart.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useCartStore, useToastStore, useWishlistStore } from "@/store/client" 4 | 5 | export default function AddAllToCart() { 6 | const { wishlist, toggleWishlist } = useWishlistStore() 7 | const { addToCart } = useCartStore() 8 | const { setToast } = useToastStore() 9 | 10 | const handleAddToCart = () => { 11 | wishlist.forEach(({ id }) => { 12 | addToCart({ id: id, quantity: 1 }) 13 | toggleWishlist(id) 14 | }) 15 | setToast({ 16 | status: "success", 17 | message: "All books has been added to cart", 18 | }) 19 | } 20 | return ( 21 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/(routes)/wishlist/components/WishlistTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image" 4 | import Link from "next/link" 5 | import { useQuery } from "@tanstack/react-query" 6 | import CancelIcon from "@/icons/CancelIcon" 7 | import LoadingIcon from "@/icons/LoadingIcon" 8 | import EmptyBoxIcon from "@/icons/EmptyBoxIcon" 9 | import { getOptimizedImage } from "@/utils/utilFuncs" 10 | import { useMounted } from "@/hooks" 11 | import { 12 | useCartStore, 13 | useToastStore, 14 | useWishlistStore, 15 | WishlistItem, 16 | } from "@/store/client" 17 | import { getBooks } from "@/store/server/books/queries" 18 | 19 | const fetchBooks = async (wishlistIds: number[], wishlist: WishlistItem[]) => { 20 | const response = await getBooks({ ids: wishlistIds }) 21 | const data = response.data 22 | 23 | // Timestamp Mapping 24 | const timestampMap = new Map() 25 | wishlist.forEach(item => { 26 | timestampMap.set(item.id, item.timestamp || 1) 27 | }) 28 | 29 | const withlist = data 30 | .map(item => { 31 | const { slug, title, price, image, in_stock } = item.attributes 32 | return { 33 | id: item.id, 34 | slug: slug, 35 | image: getOptimizedImage(image), 36 | title: title, 37 | price: price, 38 | inStock: in_stock, 39 | timestamp: timestampMap.get(item.id) || 1, 40 | } 41 | }) 42 | .sort((a, b) => b.timestamp - a.timestamp) 43 | 44 | return withlist 45 | } 46 | 47 | export default function WishlistTable() { 48 | const { wishlist, toggleWishlist } = useWishlistStore() 49 | const { addToCart } = useCartStore() 50 | const { setToast } = useToastStore() 51 | 52 | const wishlistIds = wishlist.map(item => item.id) 53 | const { data, isLoading, isError } = useQuery({ 54 | queryKey: ["wishlist", { wishlistIds }], 55 | queryFn: () => fetchBooks(wishlistIds, wishlist), 56 | keepPreviousData: true, 57 | }) 58 | 59 | const mounted = useMounted() 60 | 61 | if (isError) return null 62 | 63 | const handleAddToCart = (id: number) => { 64 | addToCart({ id, quantity: 1 }) 65 | setToast({ 66 | status: "success", 67 | message: "The book has been added to cart", 68 | }) 69 | toggleWishlist(id) 70 | } 71 | 72 | const removeFromWishlist = (id: number) => { 73 | toggleWishlist(id) 74 | setToast({ 75 | status: "success", 76 | message: "The book has been removed from wishlist", 77 | }) 78 | } 79 | 80 | return ( 81 |
82 | 83 | 84 | 85 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | {!mounted || wishlist.length < 1 ? ( 98 | 99 | 107 | 108 | ) : ( 109 | !isLoading && 110 | data.map(item => ( 111 | 115 | 126 | 134 | 140 | 146 | 155 | 166 | 167 | )) 168 | )} 169 | 170 |
86 | Book Title 87 | StatusPrice 91 | Action 92 |
100 |
101 | {mounted ? : } 102 | 103 | {mounted ? "Wishlist is empty!" : "Wishlist is loading..."} 104 | 105 |
106 |
116 |
117 | {item.title} 124 |
125 |
127 | 131 | {item.title} 132 | 133 | 135 | Status: 136 | 137 | {item.inStock ? "In Stock" : "Out of Stock"} 138 | 139 | 141 | Price: 142 | 143 | {item.price.toLocaleString()} Ks 144 | 145 | 147 | 154 | 156 | 165 |
171 |
172 | ) 173 | } 174 | -------------------------------------------------------------------------------- /app/(routes)/wishlist/components/WishlistTitle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useMounted } from "@/hooks" 4 | import { useWishlistStore } from "@/store/client" 5 | 6 | export default function WishlistTitle() { 7 | const mounted = useMounted() 8 | const { wishlist } = useWishlistStore() 9 | 10 | const numOfWishlist = wishlist.length > 0 ? `(${wishlist.length})` : `` 11 | return ( 12 |

13 | My Wishlist {mounted && numOfWishlist} 14 |

15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(routes)/wishlist/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import Breadcrumb from "@/components/Breadcrumb" 3 | import CaretDownIcon from "@/icons/CaretDownIcon" 4 | import AddAllToCart from "./components/AddAllToCart" 5 | import WishlistTable from "./components/WishlistTable" 6 | import WishlistTitle from "./components/WishlistTitle" 7 | 8 | export const metadata = { 9 | title: "Wishlist", 10 | openGraph: { 11 | title: "Wishlist", 12 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/wishlist`, 13 | }, 14 | twitter: { title: "Wishlist" }, 15 | } 16 | 17 | export default function Page() { 18 | return ( 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 |
27 | 33 |
34 | 38 | {" "} 39 | Continue Shopping 40 | 41 | 42 |
43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /app/components/AuthAlert.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import SuccessIcon from "@/icons/SuccessIcon" 4 | import * as AlertDialog from "@radix-ui/react-alert-dialog" 5 | import { useRouter } from "next/navigation" 6 | 7 | type AlertProps = { 8 | open: boolean 9 | setOpen: React.Dispatch> 10 | title: string 11 | desc: string 12 | } 13 | 14 | const AuthAlert = ({ open, setOpen, title, desc }: AlertProps) => { 15 | const router = useRouter() 16 | 17 | const handleOutsideClick = () => { 18 | setOpen(false) 19 | router.push("/") 20 | } 21 | 22 | return ( 23 | setOpen(prev => !prev)}> 24 | 25 | 29 | 30 | 31 | 32 | {title} 33 | 34 | 35 | {desc} 36 | 37 | 38 | 45 | 46 | 47 | 48 | 49 | ) 50 | } 51 | 52 | export default AuthAlert 53 | -------------------------------------------------------------------------------- /app/components/BookRow.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import ItemCard from "@/components/ItemCard" 4 | import CardSkeletons from "@/loading-ui/CardSkeletons" 5 | import { getOptimizedImage } from "@/utils/utilFuncs" 6 | import { Books } from "@/store/server/books/types" 7 | import { useBooks } from "@/store/server/books/queries" 8 | 9 | type Props = { 10 | slug: string 11 | books: Record 12 | } 13 | 14 | export default function BookRow({ slug, books }: Props) { 15 | const { data, isError, isLoading } = useBooks({ 16 | initialData: books[slug], 17 | filter: { slug, limit: 5 }, 18 | }) 19 | 20 | if (isLoading || isError) return 21 | 22 | return ( 23 |
24 | {data?.data.map(({ id, attributes }) => { 25 | const { slug, price, title, image } = attributes 26 | return ( 27 | = 5 31 | ? "last:hidden sm:last:flex sm:even:hidden md:last:hidden md:even:flex lg:last:flex" 32 | : data.data.length === 4 33 | ? "sm:last:hidden md:sm:last:flex" 34 | : "" 35 | }`} 36 | id={id} 37 | price={price} 38 | slug={slug} 39 | title={title} 40 | image={getOptimizedImage(image)} 41 | /> 42 | ) 43 | })} 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /app/components/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | 6 | const Breadcrumb = () => { 7 | const pathName = usePathname() 8 | const breadcrumbList = pathName?.split("/").filter(n => n) || [] 9 | 10 | return ( 11 | 43 | ) 44 | } 45 | 46 | export default Breadcrumb 47 | -------------------------------------------------------------------------------- /app/components/CartDropdown.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image" 4 | import Link from "next/link" 5 | import * as NavigationMenu from "@radix-ui/react-navigation-menu" 6 | import CartDropdownSkeleton from "@/loading-ui/CartDropdownSkeleton" 7 | import CheckoutButton from "./CheckoutButton" 8 | import CartIcon from "@/icons/CartIcon" 9 | import CancelIcon from "@/icons/CancelIcon" 10 | import EmptyCartIcon from "@/icons/EmptyCartIcon" 11 | import { useCartStore } from "@/store/client" 12 | import { useCart } from "@/hooks" 13 | 14 | const CartDropdown = () => { 15 | const { cart, removeFromCart, updateQuantity } = useCartStore() 16 | 17 | const { cartData, totalPrice, totalQuantity, isLoading } = useCart() 18 | 19 | return ( 20 | 21 | 26 | 27 | Cart 28 | {totalQuantity > 0 && ( 29 | 33 | {totalQuantity} 34 | 35 | )} 36 | 37 | 38 | 42 |
43 | My Shopping Cart 44 |
45 |
46 | {cart.length < 1 ? ( 47 |
48 |
49 | 50 | 51 | Cart is empty! 52 | 53 |
54 |
55 | ) : ( 56 |
    57 | {isLoading ? ( 58 | 59 | ) : ( 60 | cartData.map(item => ( 61 |
  • 65 |
    66 | 70 | {item.title} 78 | 79 |
    80 |
    81 | 82 | 86 | {item.title} 87 | 88 | 89 |
    90 |
    91 | Price: 92 | 93 | {item.price.toLocaleString()}Ks 94 | 95 |
    96 |
    97 | 110 | 111 | {item.quantity} 112 | 113 | 121 |
    122 |
    123 | 130 |
    131 |
  • 132 | )) 133 | )} 134 |
135 | )} 136 |
137 | 138 |
139 |
140 | Total Price : 141 | {totalPrice} Ks 142 |
143 |
144 | Shipping : 145 | 146 | Taxes and shipping fee will be calculated at checkout 147 | 148 |
149 |
150 | 151 | 155 | 156 | 157 | 161 | View Cart 162 | 163 | 164 |
165 |
166 |
167 | ) 168 | } 169 | 170 | export default CartDropdown 171 | -------------------------------------------------------------------------------- /app/components/CheckoutButton.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useMounted } from "@/hooks" 4 | import CartIcon from "@/icons/CartIcon" 5 | import Link from "next/link" 6 | 7 | type Props = { 8 | isDisabled?: boolean 9 | includeIcon?: boolean 10 | className?: string 11 | } 12 | 13 | const CheckoutButton = ({ 14 | isDisabled = false, 15 | includeIcon = false, 16 | className = "", 17 | }: Props) => { 18 | const isMounted = useMounted() 19 | 20 | return isMounted && isDisabled ? ( 21 | 28 | ) : ( 29 | 33 | {includeIcon && ( 34 | 37 | )} 38 | Checkout 39 | 40 | ) 41 | } 42 | 43 | export default CheckoutButton 44 | -------------------------------------------------------------------------------- /app/components/CollapsibleMenu.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEventHandler } from "react" 2 | import Link from "next/link" 3 | import * as Collapsible from "@radix-ui/react-collapsible" 4 | import * as NavigationMenu from "@radix-ui/react-navigation-menu" 5 | import MinusIcon from "@/icons/MinusIcon" 6 | import PlusIcon from "@/icons/PlusIcon" 7 | 8 | type Props = { 9 | title: string 10 | mobile?: boolean 11 | menuList: { 12 | name: string 13 | href: string 14 | }[] 15 | onClick?: MouseEventHandler 16 | } 17 | 18 | const CollapsibleMenu = ({ 19 | title, 20 | mobile = false, 21 | menuList, 22 | onClick, 23 | }: Props) => { 24 | return ( 25 | 26 | 29 | {title} 30 | 31 | 32 | 33 | 34 |
    35 | {menuList.map(menu => ( 36 |
  • 37 | 38 | 45 | {menu.name} 46 | 47 | 48 |
  • 49 | ))} 50 |
51 |
52 |
53 | ) 54 | } 55 | 56 | export default CollapsibleMenu 57 | -------------------------------------------------------------------------------- /app/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | import SocialGroup from "app/components/SocialGroup" 6 | import HeartIcon from "@/icons/HeartIcon" 7 | 8 | const Footer = () => { 9 | const pathname = usePathname() 10 | 11 | // hide Footer on /cart and /wishlist on mobile/tablet 12 | const hideOnMobile = pathname === "/cart" ? "hidden lg:block" : "" 13 | const hideOnTablet = pathname === "/wishlist" ? "hidden md:block" : "" 14 | 15 | return ( 16 |
17 |
18 |
19 |

20 | Next Bookstore 21 |

22 |
23 |

24 | We are an online bookstore that offers a wide selection of books 25 | in various genres, including fiction, non-fiction, biographies, 26 | and more. 27 |

28 |

29 | We provide a convenient and enjoyable shopping experience while 30 | offering competitive prices and excellent customer service. 31 |

32 |
33 |
34 | 35 |
36 |

Quick Links

37 | {quickLinks.map(({ id, href, title }) => ( 38 |
39 | 43 | {title} 44 | 45 |
46 | ))} 47 |
48 | 49 |
50 |

Contact

51 |

52 | Email:{" "} 53 | 54 | info@nextbook.com 55 | 56 |

57 |

58 | Phone:{" "} 59 | 60 | +959 50-960-70 61 | 62 |

63 |

64 | Address:{" "} 65 | 66 | No (77), 123 Main Street, Thingangyun, Yangon 67 | 68 |

69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 |
77 | © Copyright {new Date().getFullYear()} - Next Bookstore 78 | 79 | Crafted with{" "} 80 | {" "} 81 | by{" "} 82 | 86 | Sat Naing 87 | 88 | . 89 | 90 |
91 |
92 |
93 | ) 94 | } 95 | 96 | const quickLinks = [ 97 | { id: 1, title: "About Us", href: "/about-us" }, 98 | { id: 2, title: "Contact Us", href: "/contact-us" }, 99 | { id: 3, title: "FAQ", href: "/faq" }, 100 | { id: 4, title: "Return Policy", href: "/return-policy" }, 101 | { id: 5, title: "Terms & Conditions", href: "/" }, 102 | ] 103 | 104 | export default Footer 105 | -------------------------------------------------------------------------------- /app/components/Input.tsx: -------------------------------------------------------------------------------- 1 | import { HTMLInputTypeAttribute } from "react" 2 | import { UseFormRegisterReturn } from "react-hook-form" 3 | 4 | type Props = { 5 | label: string 6 | type?: HTMLInputTypeAttribute 7 | placeholder?: string 8 | errorMsg?: string 9 | register: UseFormRegisterReturn 10 | } 11 | 12 | const Input = ({ 13 | label, 14 | type = "text", 15 | placeholder, 16 | errorMsg, 17 | register, 18 | }: Props) => { 19 | return ( 20 |
21 | 30 | {errorMsg &&
{errorMsg}
} 31 |
32 | ) 33 | } 34 | 35 | export default Input 36 | -------------------------------------------------------------------------------- /app/components/ItemCard.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import Image from "next/image" 5 | import HeartIcon from "@/icons/HeartIcon" 6 | import { useCartStore, useToastStore, useWishlistStore } from "@/store/client" 7 | import { useMounted } from "@/hooks" 8 | 9 | type Props = { 10 | className?: string 11 | id: number 12 | title: string 13 | price: number 14 | slug: string 15 | image: string 16 | } 17 | 18 | const ItemCard = ({ className = "", id, title, price, slug, image }: Props) => { 19 | const { cart, addToCart } = useCartStore() 20 | const { wishlist, toggleWishlist } = useWishlistStore() 21 | const { setToast } = useToastStore() 22 | 23 | const handleAddToCart = () => { 24 | const alreadyAdded = cart.find(item => item.id === id) 25 | 26 | if (alreadyAdded) { 27 | setToast({ 28 | status: "info", 29 | message: "The book is already added", 30 | }) 31 | } else { 32 | addToCart({ id, quantity: 1 }) 33 | setToast({ 34 | status: "success", 35 | message: "The book has been added to cart", 36 | }) 37 | } 38 | } 39 | 40 | const hasWished = wishlist.some(item => item.id === id) 41 | const handleAddToWishlist = () => { 42 | setToast({ 43 | status: hasWished ? "info" : "success", 44 | message: `The book has been ${ 45 | hasWished ? "removed from" : "added to" 46 | } wishlist`, 47 | }) 48 | toggleWishlist(id) 49 | } 50 | 51 | const mounted = useMounted() 52 | 53 | return ( 54 |
57 | 62 |
63 | {title} 74 |
75 | 76 |
77 |
78 |

{title}

79 |
80 |
81 | MMK: 82 | {price.toLocaleString()} 83 |
84 |
85 | 92 | 106 |
107 |
108 |
109 | ) 110 | } 111 | 112 | export default ItemCard 113 | -------------------------------------------------------------------------------- /app/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import CaretDownIcon from "@/icons/CaretDownIcon" 4 | import Link from "next/link" 5 | import { usePathname } from "next/navigation" 6 | 7 | type Props = { 8 | pageCount: number 9 | currentPage: number 10 | } 11 | 12 | type Pages = { 13 | page: number 14 | slug: string 15 | }[] 16 | 17 | const Pagination = ({ pageCount, currentPage = 1 }: Props) => { 18 | const pathname = usePathname() 19 | let pages: Pages = [] 20 | for (let i = 1; i <= pageCount; i++) { 21 | pages.push({ page: i, slug: `${pathname}?page=${i}` }) 22 | } 23 | return ( 24 | 68 | ) 69 | } 70 | 71 | export default Pagination 72 | -------------------------------------------------------------------------------- /app/components/SearchDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChangeEvent, useEffect, useState } from "react" 4 | import Link from "next/link" 5 | import Image from "next/image" 6 | import * as Dialog from "@radix-ui/react-dialog" 7 | import SearchIcon from "@/icons/SearchIcon" 8 | import { getOptimizedImage } from "@/utils/utilFuncs" 9 | import { useDebounce } from "@/hooks" 10 | import { useBooks } from "@/store/server/books/queries" 11 | import { Books } from "@/store/server/books/types" 12 | 13 | const SearchDialog = () => { 14 | const [open, setOpen] = useState(false) 15 | // Search term 16 | const [searchTerm, setSearchTerm] = useState("") 17 | 18 | // Search result 19 | const [result, setResult] = useState(null) 20 | 21 | const [debouncedSearchTerm, setDebouncedSearchTerm] = useDebounce( 22 | searchTerm, 23 | 500 24 | ) 25 | 26 | const { data } = useBooks({ 27 | filter: { searchTerm }, 28 | enabled: Boolean(debouncedSearchTerm), 29 | }) 30 | 31 | const handleInput = (e: ChangeEvent) => { 32 | setSearchTerm(e.target.value) 33 | } 34 | 35 | useEffect(() => { 36 | searchTerm.length < 2 ? setResult(null) : setResult(data || null) 37 | }, [data, debouncedSearchTerm, searchTerm]) 38 | 39 | const handleDialog = () => { 40 | setResult(null) 41 | setDebouncedSearchTerm("") 42 | setOpen(prevState => !prevState) 43 | } 44 | 45 | return ( 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 |
60 | 84 |
85 | {result && result.data.length < 1 ? ( 86 |
89 |
90 | No results for 91 | {`"${searchTerm}"`} 92 |
93 |
94 | ) : ( 95 |
100 |
    101 | {result?.data.map(({ id, attributes }) => { 102 | const { slug, price, title, image, author_id } = attributes 103 | return ( 104 |
  • 105 | setOpen(false)} 108 | className="mb-1 flex gap-x-4 rounded p-2 hover:bg-skin-muted hover:bg-opacity-50" 109 | > 110 |
    111 | {title} 122 |
    123 |
    124 |
    125 | {title} 126 |
    127 |
    128 | by {author_id?.data?.attributes.name} 129 |
    130 |
    MMK {price.toLocaleString()}
    131 |
    132 | 133 |
  • 134 | ) 135 | })} 136 |
137 |
138 | )} 139 |
140 |
141 |
142 | ) 143 | } 144 | 145 | export default SearchDialog 146 | -------------------------------------------------------------------------------- /app/components/SocialGroup.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import FacebookIcon from "@/icons/FacebookIcon" 3 | import InstagramIcon from "@/icons/InstagramIcon" 4 | import TelegramIcon from "@/icons/TelegramIcon" 5 | import MailIcon from "@/icons/MailIcon" 6 | 7 | type Props = { 8 | className?: string 9 | placeBottom?: boolean 10 | } 11 | 12 | const SocialGroup = ({ className = "", placeBottom = false }: Props) => { 13 | return ( 14 |
    19 | {socialData.map(({ id, title, href, icon }) => ( 20 |
  • 21 | 26 | {icon} 27 | 28 |
  • 29 | ))} 30 |
31 | ) 32 | } 33 | 34 | const socialData = [ 35 | { 36 | id: 1, 37 | title: "Follow NextBookstore on Facebook", 38 | href: "https://fb.com/satnaing.dev", 39 | icon: ( 40 | 41 | ), 42 | }, 43 | { 44 | id: 2, 45 | title: "Follow NextBookstore on Instagram", 46 | href: "https://ig.com/satnaing.dev", 47 | icon: ( 48 | 49 | ), 50 | }, 51 | { 52 | id: 3, 53 | title: "Join NextBookstore Telegram channel", 54 | href: "https://telegram.com/satnaing.dev", 55 | icon: ( 56 | 57 | ), 58 | }, 59 | { 60 | id: 4, 61 | title: "Send NextBookstore an Email", 62 | href: "mailto:contact@satnaing.dev", 63 | icon: ( 64 | 65 | ), 66 | }, 67 | ] 68 | 69 | export default SocialGroup 70 | -------------------------------------------------------------------------------- /app/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useRef, useState } from "react" 4 | import * as RadixToast from "@radix-ui/react-toast" 5 | import { useToastStore } from "@/store/client" 6 | import CancelIcon from "@/icons/CancelIcon" 7 | 8 | const Toast = () => { 9 | const [open, setOpen] = useState(false) 10 | const timerRef = useRef(0) 11 | const { toast, setToast, toastObj } = useToastStore() 12 | 13 | useEffect(() => { 14 | return () => clearTimeout(timerRef.current) 15 | }, []) 16 | 17 | useEffect(() => { 18 | if (!toast) return 19 | 20 | setOpen(false) 21 | window.clearTimeout(timerRef.current) 22 | timerRef.current = window.setTimeout(() => { 23 | setOpen(true) 24 | setToast() 25 | }, 100) 26 | }, [toast, setToast]) 27 | 28 | return ( 29 | 30 | 35 | 38 | {open ? `${toastObj?.status}!` : ""} 39 | 40 | 41 | 42 | {open ? `${toastObj?.message}!` : ""} 43 | 44 | 45 | 46 | 54 | 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export default Toast 62 | -------------------------------------------------------------------------------- /app/components/TopBar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import * as NavigationMenu from "@radix-ui/react-navigation-menu" 5 | import navLinks from "@/lib/utils/navLinks" 6 | import CaretDownIcon from "@/icons/CaretDownIcon" 7 | 8 | const TopBar = () => { 9 | return ( 10 |
11 | 12 | 13 | {navLinks 14 | .filter(nav => ["top", "top-only"].includes(nav.position)) 15 | .map(nav => ( 16 | 17 | 21 | {nav.name} 22 | 23 | 24 | ))} 25 | 26 | 27 | 28 | 32 | English{" "} 33 | 37 | 38 | 42 |
    43 |
  • 44 | 45 | 49 | English 50 | 51 | 52 |
  • 53 |
  • 54 | 55 | 60 | Burmese 61 | 62 | 63 |
  • 64 |
65 |
66 |
67 |
68 |
69 |
70 | ) 71 | } 72 | 73 | export default TopBar 74 | -------------------------------------------------------------------------------- /app/components/loading-ui/BookDetailsSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { generateUniqueArray } from "@/utils/utilFuncs" 2 | 3 | const BookDetailsSkeleton = () => { 4 | return ( 5 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |

16 |
17 | {generateUniqueArray(15).map(e => ( 18 |
22 | ))} 23 |
24 | 25 |
26 | 27 |
28 |
Author :
29 |
30 |
31 |
32 | 33 |
Categories :
34 |
35 |
36 |
37 | 38 |
Availibility :
39 |
40 |
41 |
42 |
43 | 44 |
45 |
46 | 53 | 1 54 | 61 |
62 | MMK 63 |
64 | 65 |
66 | 72 | 78 |
79 |
80 |
81 |
82 | ) 83 | } 84 | 85 | export default BookDetailsSkeleton 86 | -------------------------------------------------------------------------------- /app/components/loading-ui/CardSkeletons.tsx: -------------------------------------------------------------------------------- 1 | import HeartIcon from "@/icons/HeartIcon" 2 | import { generateUniqueArray } from "@/utils/utilFuncs" 3 | 4 | const CardSkeletons = ({ num, slug }: { num: number; slug?: string }) => { 5 | // Generate Unique set of numbers array 6 | const numOfCards = generateUniqueArray(num) 7 | 8 | return ( 9 |
10 | {numOfCards.map(id => ( 11 |
16 |
17 |
18 |
19 |
20 |
21 |

22 |

23 |
24 |
25 |
26 |
27 | 33 | 42 |
43 |
44 |
45 | ))} 46 |
47 | ) 48 | } 49 | 50 | export default CardSkeletons 51 | -------------------------------------------------------------------------------- /app/components/loading-ui/CartDropdownSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import CancelIcon from "@/icons/CancelIcon" 2 | import { generateUniqueArray } from "@/utils/utilFuncs" 3 | 4 | const CartDropdownSkeleton = ({ num }: { num: number }) => { 5 | // Generate Unique set of numbers array 6 | const numOfCards = generateUniqueArray(num) 7 | 8 | return ( 9 | <> 10 | {numOfCards.map(id => ( 11 |
  • 15 |
    16 |
    17 |
    18 |
    19 |
    20 |
    21 | 22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 |
    29 | 36 | 37 |
    38 |
    39 | 46 |
    47 |
    48 | 51 |
    52 |
  • 53 | ))} 54 | 55 | ) 56 | } 57 | 58 | export default CartDropdownSkeleton 59 | -------------------------------------------------------------------------------- /app/components/loading-ui/CartItemSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import CancelIcon from "@/icons/CancelIcon" 2 | 3 | const CartItemSkeleton = () => { 4 | return ( 5 | 6 | 7 |
    8 |
    9 |
    10 | 11 | 12 | 13 |
    14 | 15 | 16 | 17 |
    18 | 19 | 20 | 27 | 28 |
    29 | 30 | 37 | 38 | 39 |
    40 | 41 | 42 | 45 | 46 | 47 | ) 48 | } 49 | 50 | export default CartItemSkeleton 51 | -------------------------------------------------------------------------------- /app/components/loading-ui/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | import LoadingIcon from "@/icons/LoadingIcon" 2 | 3 | const LoadingOverlay = () => { 4 | return ( 5 |
    6 | 7 | Loading ... 8 |
    9 | ) 10 | } 11 | export default LoadingOverlay 12 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | html { 7 | @apply scroll-smooth; 8 | } 9 | body { 10 | @apply bg-skin-base text-skin-dark; 11 | } 12 | a, 13 | button { 14 | @apply outline-skin-accent; 15 | } 16 | 17 | [data-state="open"] > .dropdown-caret { 18 | @apply -rotate-180; 19 | } 20 | .nav-menu-dropdown:has(> [data-state="open"]) { 21 | @apply after:w-full after:opacity-100; 22 | } 23 | [data-state="open"] > .plus-icon { 24 | @apply hidden; 25 | } 26 | [data-state="closed"] > .minus-icon { 27 | @apply hidden; 28 | } 29 | .main-navigation > div:last-child { 30 | @apply flex basis-1/3 justify-end; 31 | } 32 | 33 | .shipping-options > div:has(button[data-state="checked"]) { 34 | @apply outline outline-skin-accent-dark; 35 | } 36 | 37 | .book-desc p { 38 | @apply my-2; 39 | } 40 | 41 | /* Hide Scrollbar on Cart Dropdown */ 42 | .cart-dropdown div::-webkit-scrollbar { 43 | display: none; 44 | } 45 | .cart-dropdown div { 46 | -ms-overflow-style: none; /* IE and Edge */ 47 | scrollbar-width: none; /* Firefox */ 48 | } 49 | 50 | /* Scrollbar for Wishlist */ 51 | .wishlist-table::-webkit-scrollbar { 52 | width: 10px; 53 | } 54 | .wishlist-table::-webkit-scrollbar-track { 55 | @apply bg-skin-base; 56 | } 57 | .wishlist-table::-webkit-scrollbar-thumb { 58 | @apply bg-skin-dark bg-opacity-10; 59 | } 60 | .wishlist-table::-webkit-scrollbar-thumb:hover { 61 | @apply bg-skin-dark bg-opacity-30; 62 | } 63 | } 64 | 65 | @layer utilities { 66 | .padding-x { 67 | @apply px-4 md:px-8; 68 | } 69 | .max-width { 70 | @apply mx-auto max-w-6xl; 71 | } 72 | .main-container { 73 | @apply mx-auto w-full max-w-6xl px-4 py-6 md:px-8; 74 | } 75 | 76 | .success { 77 | @apply border-teal-500 stroke-teal-500 text-teal-500; 78 | } 79 | .info { 80 | @apply border-cyan-500 text-cyan-500; 81 | } 82 | .warning { 83 | @apply border-amber-500 text-amber-500; 84 | } 85 | .error { 86 | @apply border-rose-600 stroke-rose-600 text-rose-600; 87 | } 88 | } 89 | 90 | @layer components { 91 | .primary-btn-color { 92 | @apply bg-skin-dark text-skin-base 93 | focus-within:bg-opacity-90 focus-within:text-skin-accent 94 | hover:bg-opacity-80 active:bg-opacity-90; 95 | } 96 | .outline-btn-color { 97 | @apply border border-skin-dark text-skin-dark hover:bg-skin-muted; 98 | } 99 | .disabled-btn { 100 | @apply cursor-not-allowed !bg-opacity-70 !text-skin-base; 101 | } 102 | .text-link { 103 | @apply text-skin-dark underline decoration-dashed decoration-from-font underline-offset-2 opacity-80 hover:decoration-solid hover:opacity-100; 104 | } 105 | .cards-container { 106 | @apply my-4 grid grid-cols-2 gap-x-4 gap-y-6 sm:grid-cols-3 md:grid-cols-4 md:gap-x-6 lg:grid-cols-5; 107 | } 108 | .nav-menu-dropdown { 109 | @apply after:block after:w-0 after:border after:border-skin-accent after:opacity-0 after:transition-all after:duration-300 after:ease-out; 110 | } 111 | .nav-menu { 112 | @apply nav-menu-dropdown after:hover:w-full after:hover:opacity-100; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /app/icons/AlertIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const AlertIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 14 | 15 | 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default AlertIcon 23 | -------------------------------------------------------------------------------- /app/icons/CancelIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const CancelIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default CancelIcon 22 | -------------------------------------------------------------------------------- /app/icons/CaretDownIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const CaretDownIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default CaretDownIcon 22 | -------------------------------------------------------------------------------- /app/icons/CartIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const CartIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default CartIcon 22 | -------------------------------------------------------------------------------- /app/icons/DownArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const DownArrowIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 19 | 20 | ) 21 | } 22 | 23 | export default DownArrowIcon 24 | -------------------------------------------------------------------------------- /app/icons/EmptyBoxIcon.tsx: -------------------------------------------------------------------------------- 1 | const EmptyBoxIcon = () => { 2 | return ( 3 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default EmptyBoxIcon 34 | -------------------------------------------------------------------------------- /app/icons/EmptyCartIcon.tsx: -------------------------------------------------------------------------------- 1 | const EmptyCartIcon = ({ className = "h-14 w-14" }: { className?: string }) => { 2 | return ( 3 | 9 | 15 | 21 | 22 | 23 | 24 | 29 | 30 | 31 | 40 | 41 | 47 | 48 | 49 | 50 | 54 | 59 | 65 | 66 | 67 | 68 | ) 69 | } 70 | 71 | export default EmptyCartIcon 72 | -------------------------------------------------------------------------------- /app/icons/FacebookIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const FacebookIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default FacebookIcon 22 | -------------------------------------------------------------------------------- /app/icons/HeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const HeartIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 16 | 17 | ) 18 | } 19 | 20 | export default HeartIcon 21 | -------------------------------------------------------------------------------- /app/icons/InstagramIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const InstagramIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | 23 | 24 | ) 25 | } 26 | 27 | export default InstagramIcon 28 | -------------------------------------------------------------------------------- /app/icons/LoadingIcon.tsx: -------------------------------------------------------------------------------- 1 | const LoadingIcon = ({ 2 | mb = false, 3 | className = "w-12 h-12", 4 | }: { 5 | mb?: boolean 6 | className?: string 7 | }) => { 8 | return ( 9 | 13 | 17 | 21 | 22 | ) 23 | } 24 | 25 | export default LoadingIcon 26 | -------------------------------------------------------------------------------- /app/icons/MailIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const MailIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default MailIcon 23 | -------------------------------------------------------------------------------- /app/icons/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const MenuIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default MenuIcon 22 | -------------------------------------------------------------------------------- /app/icons/MinusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const MinusIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default MinusIcon 18 | -------------------------------------------------------------------------------- /app/icons/PlusIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const PlusIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 11 | 16 | 17 | ) 18 | } 19 | 20 | export default PlusIcon 21 | -------------------------------------------------------------------------------- /app/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const SearchIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default SearchIcon 22 | -------------------------------------------------------------------------------- /app/icons/SuccessIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const SuccessIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default SuccessIcon 22 | -------------------------------------------------------------------------------- /app/icons/TelegramIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const TelegramIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default TelegramIcon 22 | -------------------------------------------------------------------------------- /app/icons/UserIcon.tsx: -------------------------------------------------------------------------------- 1 | import { defaultStroke } from "@/lib/utils/utilFuncs" 2 | 3 | const UserIcon = ({ className = "" }: { className?: string }) => { 4 | const stroke = defaultStroke(className) 5 | return ( 6 | 12 | 17 | 18 | ) 19 | } 20 | 21 | export default UserIcon 22 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Fraunces, Quicksand } from "next/font/google" 2 | import TopBar from "@/components/TopBar" 3 | import NavBar from "@/components/NavBar" 4 | import Footer from "@/components/Footer" 5 | import Toast from "@/components/Toast" 6 | import Providers from "./providers" 7 | import "./globals.css" 8 | 9 | const fraunces = Fraunces({ 10 | variable: "--font-fraunces", 11 | subsets: ["latin"], 12 | display: "swap", 13 | }) 14 | 15 | const quicksand = Quicksand({ 16 | variable: "--font-quicksand", 17 | subsets: ["latin"], 18 | display: "swap", 19 | }) 20 | 21 | export const metadata = { 22 | title: { 23 | default: "Next Bookstore - Your Ultimate Destination for Books", 24 | template: "%s | Next Bookstore", 25 | }, 26 | description: 27 | "Discover your next favorite book at Next Bookstore! Browse our wide selection of bestsellers, new releases, and rare finds. Feed your reading passion!", 28 | openGraph: { 29 | title: { 30 | default: "Next Bookstore - Your Ultimate Destination for Books", 31 | template: "%s | Next Bookstore", 32 | }, 33 | url: process.env.NEXT_PUBLIC_SITE_URL, 34 | images: [ 35 | { 36 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/default-og.jpg`, 37 | width: 1200, 38 | height: 630, 39 | }, 40 | ], 41 | locale: "en-US", 42 | type: "website", 43 | }, 44 | twitter: { 45 | card: "summary_large_image", 46 | title: { 47 | default: "Next Bookstore - Your Ultimate Destination for Books", 48 | template: "%s | Next Bookstore", 49 | }, 50 | description: 51 | "Discover your next favorite book at Next Bookstore! Browse our wide selection of bestsellers, new releases, and rare finds. Feed your reading passion!", 52 | creator: "@SatNaingDev", 53 | images: [`${process.env.NEXT_PUBLIC_SITE_URL}/default-og.jpg`], 54 | }, 55 | robots: { 56 | index: true, 57 | }, 58 | icons: { 59 | icon: `${process.env.NEXT_PUBLIC_SITE_URL}/icons/icon.png`, 60 | shortcut: `${process.env.NEXT_PUBLIC_SITE_URL}/icons/favicon.ico`, 61 | apple: `${process.env.NEXT_PUBLIC_SITE_URL}/icons/apple-touch-icon.png`, 62 | other: [ 63 | { 64 | rel: "icon", 65 | sizes: "16x16", 66 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/icons/favicon-16x16.png`, 67 | }, 68 | { 69 | rel: "icon", 70 | sizes: "32x32", 71 | url: `${process.env.NEXT_PUBLIC_SITE_URL}/icons/favicon-32x32.png`, 72 | }, 73 | ], 74 | }, 75 | themeColor: "#EDF4F4", 76 | } 77 | 78 | export default function RootLayout({ 79 | children, 80 | }: { 81 | children: React.ReactNode 82 | }) { 83 | return ( 84 | 85 | 86 | 87 |
    88 | 89 | 90 | 91 | {children} 92 |
    93 | 94 | 95 |
    96 | 97 | 98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query" 5 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools" 6 | 7 | export default function Providers({ children }: { children: React.ReactNode }) { 8 | const [queryClient] = useState(() => new QueryClient()) 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{package.json,*.yml}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /backend/.env.example: -------------------------------------------------------------------------------- 1 | HOST=0.0.0.0 2 | PORT=1337 3 | APP_KEYS="toBeModified1,toBeModified2" 4 | API_TOKEN_SALT=tobemodified 5 | ADMIN_JWT_SECRET=tobemodified 6 | JWT_SECRET=tobemodified 7 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | .cache 2 | build 3 | **/node_modules/** 4 | -------------------------------------------------------------------------------- /backend/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true, 8 | "browser": false 9 | }, 10 | "parserOptions": { 11 | "ecmaFeatures": { 12 | "experimentalObjectRestSpread": true, 13 | "jsx": false 14 | }, 15 | "sourceType": "module" 16 | }, 17 | "globals": { 18 | "strapi": true 19 | }, 20 | "rules": { 21 | "indent": ["error", 2, { "SwitchCase": 1 }], 22 | "linebreak-style": ["error", "unix"], 23 | "no-console": 0, 24 | "quotes": ["error", "single"], 25 | "semi": ["error", "always"] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | ############################ 2 | # OS X 3 | ############################ 4 | 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | .Spotlight-V100 10 | .Trashes 11 | ._* 12 | 13 | 14 | ############################ 15 | # Linux 16 | ############################ 17 | 18 | *~ 19 | 20 | 21 | ############################ 22 | # Windows 23 | ############################ 24 | 25 | Thumbs.db 26 | ehthumbs.db 27 | Desktop.ini 28 | $RECYCLE.BIN/ 29 | *.cab 30 | *.msi 31 | *.msm 32 | *.msp 33 | 34 | 35 | ############################ 36 | # Packages 37 | ############################ 38 | 39 | *.7z 40 | *.csv 41 | *.dat 42 | *.dmg 43 | *.gz 44 | *.iso 45 | *.jar 46 | *.rar 47 | *.tar 48 | *.zip 49 | *.com 50 | *.class 51 | *.dll 52 | *.exe 53 | *.o 54 | *.seed 55 | *.so 56 | *.swo 57 | *.swp 58 | *.swn 59 | *.swm 60 | *.out 61 | *.pid 62 | 63 | 64 | ############################ 65 | # Logs and databases 66 | ############################ 67 | 68 | # .tmp 69 | *.log 70 | *.sql 71 | *.sqlite 72 | *.sqlite3 73 | 74 | 75 | ############################ 76 | # Misc. 77 | ############################ 78 | 79 | *# 80 | ssl 81 | .idea 82 | nbproject 83 | public/uploads/* 84 | !public/uploads/.gitkeep 85 | 86 | ############################ 87 | # Node.js 88 | ############################ 89 | 90 | lib-cov 91 | lcov.info 92 | pids 93 | logs 94 | results 95 | node_modules 96 | .node_history 97 | 98 | ############################ 99 | # Tests 100 | ############################ 101 | 102 | testApp 103 | coverage 104 | 105 | ############################ 106 | # Strapi 107 | ############################ 108 | 109 | .env 110 | license.txt 111 | exports 112 | *.cache 113 | dist 114 | build 115 | .strapi-updater.json 116 | -------------------------------------------------------------------------------- /backend/.tmp/data.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/.tmp/data.db -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Getting started with Strapi 2 | 3 | Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html) (CLI) which lets you scaffold and manage your project in seconds. 4 | 5 | ### `develop` 6 | 7 | Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-develop) 8 | 9 | ``` 10 | npm run develop 11 | # or 12 | yarn develop 13 | ``` 14 | 15 | ### `start` 16 | 17 | Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-start) 18 | 19 | ``` 20 | npm run start 21 | # or 22 | yarn start 23 | ``` 24 | 25 | ### `build` 26 | 27 | Build your admin panel. [Learn more](https://docs.strapi.io/developer-docs/latest/developer-resources/cli/CLI.html#strapi-build) 28 | 29 | ``` 30 | npm run build 31 | # or 32 | yarn build 33 | ``` 34 | 35 | ## ⚙️ Deployment 36 | 37 | Strapi gives you many possible deployment options for your project. Find the one that suits you on the [deployment section of the documentation](https://docs.strapi.io/developer-docs/latest/setup-deployment-guides/deployment.html). 38 | 39 | ## 📚 Learn more 40 | 41 | - [Resource center](https://strapi.io/resource-center) - Strapi resource center. 42 | - [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation. 43 | - [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community. 44 | - [Strapi blog](https://docs.strapi.io) - Official Strapi blog containing articles made by the Strapi team and the community. 45 | - [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements. 46 | 47 | Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome! 48 | 49 | ## ✨ Community 50 | 51 | - [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team. 52 | - [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members. 53 | - [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi. 54 | 55 | --- 56 | 57 | 🤫 Psst! [Strapi is hiring](https://strapi.io/careers). 58 | -------------------------------------------------------------------------------- /backend/config/admin.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | auth: { 3 | secret: env('ADMIN_JWT_SECRET'), 4 | }, 5 | apiToken: { 6 | salt: env('API_TOKEN_SALT'), 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /backend/config/api.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rest: { 3 | defaultLimit: 25, 4 | maxLimit: 100, 5 | withCount: true, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /backend/config/database.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = ({ env }) => ({ 4 | connection: { 5 | client: 'sqlite', 6 | connection: { 7 | filename: path.join(__dirname, '..', env('DATABASE_FILENAME', '.tmp/data.db')), 8 | }, 9 | useNullAsDefault: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /backend/config/env/production /server.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | url: env("RENDER_EXTERNAL_URL"), 3 | dirs: { 4 | public: "/data/public" 5 | }, 6 | }); -------------------------------------------------------------------------------- /backend/config/middlewares.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'strapi::errors', 3 | // 'strapi::security', 4 | { 5 | name: 'strapi::security', 6 | config: { 7 | contentSecurityPolicy: { 8 | useDefaults: true, 9 | directives: { 10 | 'connect-src': ["'self'", 'https:'], 11 | 'img-src': ["'self'", 'data:', 'blob:', 'dl.airtable.com', 'res.cloudinary.com'], 12 | 'media-src': ["'self'", 'data:', 'blob:', 'dl.airtable.com', 'res.cloudinary.com'], 13 | upgradeInsecureRequests: null, 14 | }, 15 | }, 16 | }, 17 | }, 18 | 'strapi::cors', 19 | 'strapi::poweredBy', 20 | 'strapi::logger', 21 | 'strapi::query', 22 | 'strapi::body', 23 | 'strapi::session', 24 | 'strapi::favicon', 25 | 'strapi::public', 26 | ]; 27 | -------------------------------------------------------------------------------- /backend/config/plugins.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | // ... 3 | upload: { 4 | config: { 5 | provider: 'cloudinary', 6 | providerOptions: { 7 | cloud_name: env('CLOUDINARY_NAME'), 8 | api_key: env('CLOUDINARY_KEY'), 9 | api_secret: env('CLOUDINARY_SECRET'), 10 | }, 11 | actionOptions: { 12 | upload: {}, 13 | uploadStream: { 14 | folder: env('CLOUDINARY_FOLDER'), 15 | }, 16 | delete: {}, 17 | }, 18 | }, 19 | }, 20 | // ... 21 | }); -------------------------------------------------------------------------------- /backend/config/server.js: -------------------------------------------------------------------------------- 1 | module.exports = ({ env }) => ({ 2 | host: env('HOST', '0.0.0.0'), 3 | port: env.int('PORT', 1337), 4 | app: { 5 | keys: env.array('APP_KEYS'), 6 | }, 7 | }); 8 | -------------------------------------------------------------------------------- /backend/database/migrations/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/database/migrations/.gitkeep -------------------------------------------------------------------------------- /backend/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/favicon.png -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "private": true, 4 | "version": "0.1.0", 5 | "description": "A Strapi application", 6 | "scripts": { 7 | "develop": "strapi develop", 8 | "start": "strapi start", 9 | "build": "strapi build", 10 | "strapi": "strapi" 11 | }, 12 | "dependencies": { 13 | "@strapi/plugin-i18n": "4.11.0", 14 | "@strapi/plugin-users-permissions": "4.12.1", 15 | "@strapi/provider-upload-cloudinary": "^4.11.0", 16 | "@strapi/strapi": "4.11.0", 17 | "better-sqlite3": "8.4.0" 18 | }, 19 | "author": { 20 | "name": "A Strapi developer" 21 | }, 22 | "strapi": { 23 | "uuid": "b7b9b4df-c62b-4848-af3a-7a72fd6b18ca" 24 | }, 25 | "engines": { 26 | "node": "16.14.2", 27 | "npm": ">=6.0.0" 28 | }, 29 | "license": "MIT" 30 | } 31 | -------------------------------------------------------------------------------- /backend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # To prevent search engines from seeing the site altogether, uncomment the next two lines: 2 | # User-Agent: * 3 | # Disallow: / 4 | -------------------------------------------------------------------------------- /backend/public/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/public/uploads/.gitkeep -------------------------------------------------------------------------------- /backend/render.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | - type: web 3 | name: strapi 4 | env: node 5 | plan: starter 6 | buildCommand: npm install && npm run build 7 | startCommand: rsync -a public/ /data/public/ && npm start 8 | healthCheckPath: /_health 9 | autoDeploy: false 10 | disk: 11 | name: strapi-data 12 | mountPath: /data 13 | sizeGB: 1 14 | envVars: 15 | - key: NODE_VERSION 16 | value: ~16.14.2 17 | - key: NODE_ENV 18 | value: production 19 | - key: DATABASE_FILENAME 20 | value: /data/strapi.db 21 | - key: JWT_SECRET 22 | generateValue: true 23 | - key: ADMIN_JWT_SECRET 24 | generateValue: true 25 | - key: APP_KEYS 26 | generateValue: true -------------------------------------------------------------------------------- /backend/src/admin/app.example.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | locales: [ 3 | // 'ar', 4 | // 'fr', 5 | // 'cs', 6 | // 'de', 7 | // 'dk', 8 | // 'es', 9 | // 'he', 10 | // 'id', 11 | // 'it', 12 | // 'ja', 13 | // 'ko', 14 | // 'ms', 15 | // 'nl', 16 | // 'no', 17 | // 'pl', 18 | // 'pt-BR', 19 | // 'pt', 20 | // 'ru', 21 | // 'sk', 22 | // 'sv', 23 | // 'th', 24 | // 'tr', 25 | // 'uk', 26 | // 'vi', 27 | // 'zh-Hans', 28 | // 'zh', 29 | ], 30 | }; 31 | 32 | const bootstrap = (app) => { 33 | console.log(app); 34 | }; 35 | 36 | export default { 37 | config, 38 | bootstrap, 39 | }; 40 | -------------------------------------------------------------------------------- /backend/src/admin/webpack.config.example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* eslint-disable no-unused-vars */ 4 | module.exports = (config, webpack) => { 5 | // Note: we provide webpack above so you should not `require` it 6 | // Perform customizations to webpack config 7 | // Important: return the modified config 8 | return config; 9 | }; 10 | -------------------------------------------------------------------------------- /backend/src/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/src/api/.gitkeep -------------------------------------------------------------------------------- /backend/src/api/author/content-types/author/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "authors", 4 | "info": { 5 | "singularName": "author", 6 | "pluralName": "authors", 7 | "displayName": "Author", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "description": { 21 | "type": "text" 22 | }, 23 | "books": { 24 | "type": "relation", 25 | "relation": "oneToMany", 26 | "target": "api::book.book", 27 | "mappedBy": "author_id" 28 | }, 29 | "slug": { 30 | "type": "uid", 31 | "targetField": "name" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /backend/src/api/author/controllers/author.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * author controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::author.author'); 10 | -------------------------------------------------------------------------------- /backend/src/api/author/routes/author.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * author router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::author.author'); 10 | -------------------------------------------------------------------------------- /backend/src/api/author/services/author.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * author service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::author.author'); 10 | -------------------------------------------------------------------------------- /backend/src/api/book/content-types/book/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "books", 4 | "info": { 5 | "singularName": "book", 6 | "pluralName": "books", 7 | "displayName": "Book", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": true 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "title": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "price": { 21 | "type": "integer", 22 | "required": true, 23 | "min": 0 24 | }, 25 | "author_id": { 26 | "type": "relation", 27 | "relation": "manyToOne", 28 | "target": "api::author.author", 29 | "inversedBy": "books" 30 | }, 31 | "description": { 32 | "type": "richtext" 33 | }, 34 | "categories": { 35 | "type": "relation", 36 | "relation": "manyToMany", 37 | "target": "api::category.category", 38 | "inversedBy": "books" 39 | }, 40 | "order_details": { 41 | "type": "relation", 42 | "relation": "oneToMany", 43 | "target": "api::order-detail.order-detail", 44 | "mappedBy": "book" 45 | }, 46 | "image": { 47 | "type": "media", 48 | "multiple": true, 49 | "required": true, 50 | "allowedTypes": [ 51 | "images" 52 | ] 53 | }, 54 | "slug": { 55 | "type": "uid", 56 | "targetField": "title", 57 | "required": false 58 | }, 59 | "in_stock": { 60 | "type": "boolean", 61 | "default": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /backend/src/api/book/controllers/book.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * book controller 5 | */ 6 | 7 | const { createCoreController } = require("@strapi/strapi").factories; 8 | 9 | module.exports = createCoreController("api::book.book", ({ strapi }) => { 10 | const numberOfEntries = 10; 11 | const uid = "api::book.book"; 12 | 13 | return { 14 | async random(ctx) { 15 | const bestSellers = 1; 16 | let categoryIds = [bestSellers]; 17 | 18 | const author = ctx.query.author; 19 | 20 | if (ctx.query.categories) { 21 | const ids = ctx.query.categories.split(",").map(id => Number(id)); 22 | categoryIds = [...ids]; 23 | } 24 | 25 | const bookIds = ( 26 | await strapi.db.connection.raw(` 27 | SELECT books.* 28 | FROM books 29 | JOIN books_categories_links ON books.id = books_categories_links.book_id 30 | JOIN categories ON books_categories_links.category_id = categories.id 31 | JOIN books_author_id_links ON books.id = books_author_id_links.book_id 32 | JOIN authors ON books_author_id_links.author_id = authors.id 33 | WHERE authors.id = ${author} OR categories.id IN (${categoryIds}) 34 | ORDER BY RANDOM() 35 | LIMIT ${numberOfEntries} 36 | `) 37 | ).map(it => it.id); 38 | 39 | const entries = await strapi.entityService.findMany(uid, { 40 | filters: { 41 | id: { 42 | $in: bookIds, 43 | }, 44 | }, 45 | populate: "*", 46 | }); 47 | 48 | const structureRandomEntries = { 49 | data: entries.map(entry => { 50 | return { 51 | id: entry.id, 52 | attributes: entry, 53 | }; 54 | }), 55 | }; 56 | 57 | ctx.body = structureRandomEntries; 58 | }, 59 | }; 60 | }); 61 | -------------------------------------------------------------------------------- /backend/src/api/book/routes/book.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * book router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::book.book'); 10 | -------------------------------------------------------------------------------- /backend/src/api/book/routes/random.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | routes: [ 5 | { 6 | method: "GET", 7 | path: "/book/random", 8 | handler: "book.random", 9 | config: { 10 | auth: false, 11 | }, 12 | }, 13 | ], 14 | }; -------------------------------------------------------------------------------- /backend/src/api/book/services/book.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * book service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::book.book'); 10 | -------------------------------------------------------------------------------- /backend/src/api/category/content-types/category/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "categories", 4 | "info": { 5 | "singularName": "category", 6 | "pluralName": "categories", 7 | "displayName": "Category", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "name": { 16 | "type": "string", 17 | "required": true, 18 | "unique": true 19 | }, 20 | "description": { 21 | "type": "text" 22 | }, 23 | "books": { 24 | "type": "relation", 25 | "relation": "manyToMany", 26 | "target": "api::book.book", 27 | "mappedBy": "categories" 28 | }, 29 | "slug": { 30 | "type": "uid", 31 | "targetField": "name", 32 | "required": true 33 | }, 34 | "featured": { 35 | "type": "boolean", 36 | "default": false 37 | }, 38 | "featured_order": { 39 | "type": "integer", 40 | "unique": true, 41 | "default": 1 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/api/category/controllers/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * category controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::category.category'); 10 | -------------------------------------------------------------------------------- /backend/src/api/category/routes/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * category router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::category.category'); 10 | -------------------------------------------------------------------------------- /backend/src/api/category/services/category.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * category service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::category.category'); 10 | -------------------------------------------------------------------------------- /backend/src/api/customer/content-types/customer/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "customers", 4 | "info": { 5 | "singularName": "customer", 6 | "pluralName": "customers", 7 | "displayName": "Customer", 8 | "description": "" 9 | }, 10 | "options": { 11 | "draftAndPublish": false 12 | }, 13 | "pluginOptions": {}, 14 | "attributes": { 15 | "fullName": { 16 | "type": "string", 17 | "required": true 18 | }, 19 | "phone": { 20 | "type": "string", 21 | "required": true 22 | }, 23 | "email": { 24 | "type": "email", 25 | "required": true, 26 | "unique": true 27 | }, 28 | "address": { 29 | "type": "text", 30 | "required": true 31 | }, 32 | "state": { 33 | "type": "relation", 34 | "relation": "manyToOne", 35 | "target": "api::state.state", 36 | "inversedBy": "customers" 37 | }, 38 | "role": { 39 | "type": "relation", 40 | "relation": "oneToOne", 41 | "target": "plugin::users-permissions.user" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/api/customer/controllers/customer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * customer controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::customer.customer'); 10 | -------------------------------------------------------------------------------- /backend/src/api/customer/routes/customer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * customer router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::customer.customer'); 10 | -------------------------------------------------------------------------------- /backend/src/api/customer/services/customer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * customer service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::customer.customer'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order-detail/content-types/order-detail/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "order_details", 4 | "info": { 5 | "singularName": "order-detail", 6 | "pluralName": "order-details", 7 | "displayName": "OrderDetail" 8 | }, 9 | "options": { 10 | "draftAndPublish": false 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "book": { 15 | "type": "relation", 16 | "relation": "manyToOne", 17 | "target": "api::book.book", 18 | "inversedBy": "order_details" 19 | }, 20 | "order": { 21 | "type": "relation", 22 | "relation": "manyToOne", 23 | "target": "api::order.order", 24 | "inversedBy": "order_details" 25 | }, 26 | "quantity": { 27 | "min": 1, 28 | "type": "integer", 29 | "required": false, 30 | "default": 1 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /backend/src/api/order-detail/controllers/order-detail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order-detail controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::order-detail.order-detail'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order-detail/routes/order-detail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order-detail router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::order-detail.order-detail'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order-detail/services/order-detail.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order-detail service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::order-detail.order-detail'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order/content-types/order/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "orders", 4 | "info": { 5 | "singularName": "order", 6 | "pluralName": "orders", 7 | "displayName": "Order" 8 | }, 9 | "options": { 10 | "draftAndPublish": false 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "user": { 15 | "type": "relation", 16 | "relation": "manyToOne", 17 | "target": "plugin::users-permissions.user", 18 | "inversedBy": "orders" 19 | }, 20 | "note": { 21 | "type": "text" 22 | }, 23 | "order_details": { 24 | "type": "relation", 25 | "relation": "oneToMany", 26 | "target": "api::order-detail.order-detail", 27 | "mappedBy": "order" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/src/api/order/controllers/order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::order.order'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order/routes/order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::order.order'); 10 | -------------------------------------------------------------------------------- /backend/src/api/order/services/order.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * order service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::order.order'); 10 | -------------------------------------------------------------------------------- /backend/src/api/state/content-types/state/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "states", 4 | "info": { 5 | "singularName": "state", 6 | "pluralName": "states", 7 | "displayName": "State" 8 | }, 9 | "options": { 10 | "draftAndPublish": false 11 | }, 12 | "pluginOptions": {}, 13 | "attributes": { 14 | "name": { 15 | "type": "string" 16 | }, 17 | "customers": { 18 | "type": "relation", 19 | "relation": "oneToMany", 20 | "target": "api::customer.customer", 21 | "mappedBy": "state" 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/api/state/controllers/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * state controller 5 | */ 6 | 7 | const { createCoreController } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreController('api::state.state'); 10 | -------------------------------------------------------------------------------- /backend/src/api/state/routes/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * state router 5 | */ 6 | 7 | const { createCoreRouter } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreRouter('api::state.state'); 10 | -------------------------------------------------------------------------------- /backend/src/api/state/services/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * state service 5 | */ 6 | 7 | const { createCoreService } = require('@strapi/strapi').factories; 8 | 9 | module.exports = createCoreService('api::state.state'); 10 | -------------------------------------------------------------------------------- /backend/src/extensions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/backend/src/extensions/.gitkeep -------------------------------------------------------------------------------- /backend/src/extensions/users-permissions/content-types/user/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "kind": "collectionType", 3 | "collectionName": "up_users", 4 | "info": { 5 | "name": "user", 6 | "description": "", 7 | "singularName": "user", 8 | "pluralName": "users", 9 | "displayName": "User" 10 | }, 11 | "options": { 12 | "draftAndPublish": false, 13 | "timestamps": true 14 | }, 15 | "attributes": { 16 | "username": { 17 | "type": "string", 18 | "minLength": 3, 19 | "unique": true, 20 | "configurable": false, 21 | "required": true 22 | }, 23 | "email": { 24 | "type": "email", 25 | "minLength": 6, 26 | "configurable": false, 27 | "required": true 28 | }, 29 | "provider": { 30 | "type": "string", 31 | "configurable": false 32 | }, 33 | "password": { 34 | "type": "password", 35 | "minLength": 6, 36 | "configurable": false, 37 | "private": true 38 | }, 39 | "resetPasswordToken": { 40 | "type": "string", 41 | "configurable": false, 42 | "private": true 43 | }, 44 | "confirmationToken": { 45 | "type": "string", 46 | "configurable": false, 47 | "private": true 48 | }, 49 | "confirmed": { 50 | "type": "boolean", 51 | "default": false, 52 | "configurable": false 53 | }, 54 | "blocked": { 55 | "type": "boolean", 56 | "default": false, 57 | "configurable": false 58 | }, 59 | "role": { 60 | "type": "relation", 61 | "relation": "manyToOne", 62 | "target": "plugin::users-permissions.role", 63 | "inversedBy": "users", 64 | "configurable": false 65 | }, 66 | "fullName": { 67 | "type": "string", 68 | "required": true, 69 | "minLength": 2 70 | }, 71 | "address": { 72 | "type": "text", 73 | "required": true, 74 | "minLength": 2 75 | }, 76 | "phone": { 77 | "type": "string", 78 | "minLength": 6, 79 | "maxLength": 15, 80 | "required": true, 81 | "unique": true 82 | }, 83 | "orders": { 84 | "type": "relation", 85 | "relation": "oneToMany", 86 | "target": "api::order.order", 87 | "mappedBy": "user" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /backend/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | /** 5 | * An asynchronous register function that runs before 6 | * your application is initialized. 7 | * 8 | * This gives you an opportunity to extend code. 9 | */ 10 | register(/*{ strapi }*/) {}, 11 | 12 | /** 13 | * An asynchronous bootstrap function that runs before 14 | * your application gets started. 15 | * 16 | * This gives you an opportunity to set up your data model, 17 | * run jobs, or perform some special logic. 18 | */ 19 | bootstrap(/*{ strapi }*/) {}, 20 | }; 21 | -------------------------------------------------------------------------------- /lib/api/axios.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | 3 | const BASE_URL = process.env.NEXT_PUBLIC_BACKEND_URL 4 | 5 | export default axios.create({ 6 | baseURL: BASE_URL, 7 | }) 8 | 9 | export const authJsonHeader = (token: string, file?: boolean) => { 10 | const contentType = file ? "multipart/form-data" : "Application/json" 11 | return { 12 | "Content-Type": contentType, 13 | Accept: "application/json", 14 | Authorization: `Bearer ${token}`, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useCart" 2 | export * from "./useDebounce" 3 | export * from "./useMounted" 4 | -------------------------------------------------------------------------------- /lib/hooks/useCart.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query" 2 | import { getOptimizedImage } from "@/utils/utilFuncs" 3 | import { useCartStore } from "@/store/client" 4 | import { getBooks } from "@/store/server/books/queries" 5 | 6 | export const useCart = () => { 7 | // Client Global State 8 | const { cart } = useCartStore() 9 | 10 | // Server State 11 | const cartIds = cart.map(item => item.id) 12 | const { data, isLoading, isError } = useQuery({ 13 | queryKey: ["cart", { cartIds }], 14 | queryFn: () => getBooks({ ids: cartIds }), 15 | keepPreviousData: true, 16 | }) 17 | 18 | if (cart.length < 1) 19 | return { cartData: [], totalPrice: "0", totalQuantity: 0 } 20 | 21 | if (isLoading || isError) 22 | return { 23 | cartData: [], 24 | totalPrice: "0", 25 | totalQuantity: 0, 26 | isLoading, 27 | isError, 28 | } 29 | 30 | // Quantity Mapping 31 | const qtyMap = new Map() 32 | cart.forEach(item => { 33 | qtyMap.set(item.id, item.quantity) 34 | }) 35 | 36 | // Timestamp Mapping 37 | const timestampMap = new Map() 38 | cart.forEach(item => { 39 | timestampMap.set(item.id, item.timestamp || 1) 40 | }) 41 | 42 | const cartData = data.data 43 | .map(item => { 44 | const { title, slug, price, image } = item.attributes 45 | return { 46 | id: item.id, 47 | title, 48 | price, 49 | slug, 50 | image: getOptimizedImage(image), 51 | quantity: qtyMap.get(item.id) || 1, 52 | timestamp: timestampMap.get(item.id) || 1, 53 | } 54 | }) 55 | .sort((a, b) => b.timestamp - a.timestamp) 56 | 57 | let totalPrice = "0" 58 | let totalQuantity = 0 59 | 60 | if (cartData) { 61 | totalPrice = cartData 62 | .reduce( 63 | (accumulator: number, currentItem) => 64 | accumulator + currentItem.price * currentItem.quantity, 65 | 0 66 | ) 67 | .toLocaleString() 68 | } 69 | 70 | totalQuantity = cart.reduce( 71 | (accumulator: number, currentItem) => accumulator + currentItem.quantity, 72 | 0 73 | ) 74 | 75 | return { cartData, totalPrice, totalQuantity, isLoading, isError } 76 | } 77 | -------------------------------------------------------------------------------- /lib/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useEffect, useState } from "react" 2 | 3 | export function useDebounce( 4 | value: T, 5 | delay: number 6 | ): [T, Dispatch>] { 7 | // State and setters for debounced value 8 | const [debouncedValue, setDebouncedValue] = useState(value) 9 | useEffect( 10 | () => { 11 | if (String(value).length < 2) return 12 | 13 | // Update debounced value after delay 14 | const handler = setTimeout(() => { 15 | setDebouncedValue(value) 16 | }, delay) 17 | // Cancel the timeout if value changes (also on delay change or unmount) 18 | // This is how we prevent debounced value from updating if value is changed ... 19 | // .. within the delay period. Timeout gets cleared and restarted. 20 | return () => { 21 | clearTimeout(handler) 22 | } 23 | }, 24 | [value, delay] // Only re-call effect if value or delay changes 25 | ) 26 | return [debouncedValue, setDebouncedValue] 27 | } 28 | -------------------------------------------------------------------------------- /lib/hooks/useMounted.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export const useMounted = () => { 4 | const [mounted, setMounted] = useState(false) 5 | 6 | useEffect(() => { 7 | setMounted(true) 8 | }, []) 9 | 10 | return mounted 11 | } 12 | -------------------------------------------------------------------------------- /lib/hooks/useScroll.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useState, useEffect } from "react" 2 | 3 | export default function useScroll() { 4 | const [data, setData] = useState({ 5 | x: 0, 6 | y: 0, 7 | lastX: 0, 8 | lastY: 0, 9 | }) 10 | 11 | // set up event listeners 12 | useEffect(() => { 13 | const handleScroll = () => { 14 | setData(last => { 15 | return { 16 | x: window.scrollX, 17 | y: window.scrollY, 18 | lastX: last.x, 19 | lastY: last.y, 20 | } 21 | }) 22 | } 23 | 24 | handleScroll() 25 | window.addEventListener("scroll", handleScroll) 26 | 27 | return () => { 28 | window.removeEventListener("scroll", handleScroll) 29 | } 30 | }, []) 31 | 32 | return data 33 | } 34 | 35 | export const ScrollContext = createContext(null) 36 | -------------------------------------------------------------------------------- /lib/store/client/authStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | import { persist } from "zustand/middleware" 3 | 4 | // Auth Store 5 | type AuthState = { 6 | token: string 7 | setToken: (token: string) => void 8 | removeToken: (token: string) => void 9 | } 10 | 11 | export const useAuthStore = create()( 12 | persist( 13 | set => ({ 14 | token: "", 15 | setToken: token => set({ token }), 16 | removeToken: () => set({ token: "" }), 17 | }), 18 | { 19 | name: "bearer", 20 | } 21 | ) 22 | ) 23 | -------------------------------------------------------------------------------- /lib/store/client/cartStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | import { persist } from "zustand/middleware" 3 | 4 | /* ===== Cart Store ===== */ 5 | type CartItem = { 6 | id: number 7 | quantity: number 8 | timestamp?: number 9 | } 10 | 11 | type CartState = { 12 | cart: CartItem[] 13 | addToCart: (bookObj: CartItem) => void 14 | removeFromCart: (id: number) => void 15 | updateQuantity: (id: number, action: "increase" | "decrease") => void 16 | } 17 | 18 | export const useCartStore = create()( 19 | persist( 20 | set => ({ 21 | cart: [], 22 | addToCart: bookObj => set(state => addCartItem(state.cart, bookObj)), 23 | removeFromCart: id => set(state => removeCartItem(state.cart, id)), 24 | updateQuantity: (id, action) => 25 | set(state => updateItemQuantity(state.cart, id, action)), 26 | }), 27 | { 28 | name: "cart-storage", 29 | } 30 | ) 31 | ) 32 | 33 | /* ===== Cart Store Util Functions ===== */ 34 | function addCartItem(state: CartItem[], bookObj: CartItem) { 35 | const cartArray = state.filter(item => item.id !== bookObj.id) 36 | const newItem = { ...bookObj, timestamp: Date.now() } 37 | return { cart: [...cartArray, newItem] } 38 | } 39 | 40 | function removeCartItem(state: CartItem[], id: number) { 41 | const removedCart = state.filter(item => item.id !== id) 42 | return { cart: [...removedCart] } 43 | } 44 | 45 | function updateItemQuantity( 46 | state: CartItem[], 47 | id: number, 48 | action: "increase" | "decrease" 49 | ) { 50 | const objIndex = state.findIndex(obj => obj.id == id) 51 | 52 | if (action === "increase") { 53 | state[objIndex].quantity = state[objIndex].quantity + 1 54 | } else if (action === "decrease") { 55 | state[objIndex].quantity = 56 | state[objIndex].quantity - (state[objIndex].quantity > 1 ? 1 : 0) 57 | } 58 | 59 | return { cart: [...state] } 60 | } 61 | -------------------------------------------------------------------------------- /lib/store/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./authStore" 2 | export * from "./cartStore" 3 | export * from "./toastStore" 4 | export * from "./wishlistStore" 5 | -------------------------------------------------------------------------------- /lib/store/client/toastStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | 3 | /* ===== Toast Store ===== */ 4 | type ToastObj = { 5 | status: "success" | "info" | "error" | "warning" 6 | message: string 7 | } 8 | 9 | type ToastState = { 10 | toast: boolean 11 | toastObj?: ToastObj 12 | setToast: (obj?: ToastObj) => void 13 | } 14 | 15 | export const useToastStore = create()(set => ({ 16 | toast: false, 17 | toastObj: { status: "info", message: "" }, 18 | setToast: obj => set(state => setToast(state, obj)), 19 | })) 20 | 21 | function setToast(state: ToastState, toastObj?: ToastObj) { 22 | if (!toastObj) return { toast: !state.toastObj } 23 | 24 | return { 25 | toast: !state.toast, 26 | toastObj, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lib/store/client/wishlistStore.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand" 2 | import { persist } from "zustand/middleware" 3 | 4 | /* ===== Wishlist Store ===== */ 5 | export type WishlistItem = { 6 | id: number 7 | timestamp?: number 8 | } 9 | 10 | type WishlistState = { 11 | wishlist: WishlistItem[] 12 | toggleWishlist: (id: number) => void 13 | } 14 | 15 | export const useWishlistStore = create()( 16 | persist( 17 | set => ({ 18 | wishlist: [], 19 | toggleWishlist: (id: number) => 20 | set(state => toggleWishlistItem(state.wishlist, id)), 21 | }), 22 | { 23 | name: "wishlist-storage", 24 | } 25 | ) 26 | ) 27 | 28 | function toggleWishlistItem(wishlist: WishlistItem[], id: number) { 29 | const status = wishlist.some(wItem => wItem.id === id) 30 | const filteredWishlist = wishlist.filter(wItem => wItem.id !== id) 31 | 32 | if (status) return { wishlist: [...filteredWishlist] } 33 | 34 | const newWishlist = { id, timestamp: Date.now() } 35 | return { wishlist: [...filteredWishlist, newWishlist] } 36 | } 37 | -------------------------------------------------------------------------------- /lib/store/server/books/queries.tsx: -------------------------------------------------------------------------------- 1 | import { BookQueryProps, Books } from "./types" 2 | import axios from "@/lib/api/axios" 3 | import { generateBookQuery } from "@/utils/utilFuncs" 4 | import { useQuery } from "@tanstack/react-query" 5 | 6 | /* ========== Get Multiple Books ========== */ 7 | export const getBooks = async (props: BookQueryProps): Promise => { 8 | const queryString = generateBookQuery(props) 9 | const response = await axios.get(`/books?populate=*&${queryString}`) 10 | return response.data 11 | } 12 | 13 | interface UseBooks { 14 | initialData?: Books 15 | filter: BookQueryProps 16 | enabled?: boolean 17 | } 18 | 19 | export const useBooks = ({ initialData, filter, enabled = true }: UseBooks) => 20 | useQuery({ 21 | queryKey: ["books", filter], 22 | queryFn: () => getBooks(filter), 23 | initialData, 24 | enabled: enabled, 25 | }) 26 | 27 | /* ========== Get Single Book ========== */ 28 | export const getBook = async (slug: string): Promise => { 29 | const response = await axios.get( 30 | `/books?filters[slug][$eq]]=${slug}&populate=*` 31 | ) 32 | return response.data 33 | } 34 | 35 | export const useBook = ({ 36 | initialData, 37 | slug, 38 | }: { 39 | initialData: Books 40 | slug: string 41 | }) => 42 | useQuery({ 43 | queryKey: ["books", slug], 44 | queryFn: () => getBook(slug), 45 | initialData, 46 | }) 47 | 48 | /* ========== Get Books by Category ========== */ 49 | export const getBooksByCategory = async ({ 50 | slug, 51 | pageNum = 1, 52 | }: { 53 | slug: string 54 | pageNum?: number 55 | }): Promise => { 56 | const response = await axios.get( 57 | `/books?filters[categories][slug][$eq]]=${slug}&populate=*&pagination[page]=${pageNum}&pagination[pageSize]=10` 58 | ) 59 | return response.data 60 | } 61 | 62 | export const useBooksByCategory = ({ 63 | slug, 64 | pageNum, 65 | initialData, 66 | }: { 67 | slug: string 68 | pageNum?: number 69 | initialData: Books 70 | }) => 71 | useQuery({ 72 | queryKey: ["books", { slug, pageNum }], 73 | queryFn: () => getBooksByCategory({ slug, pageNum }), 74 | initialData, 75 | }) 76 | 77 | /* ========== Get Related Books ========== */ 78 | interface RelatedBooks { 79 | author: number 80 | categories: number[] 81 | } 82 | 83 | export const getRelatedBooks = async ({ 84 | author, 85 | categories, 86 | }: RelatedBooks): Promise => { 87 | const response = await axios.get( 88 | `/book/random?categories=${categories.toString()}&author=${author}` 89 | ) 90 | return response.data 91 | } 92 | 93 | export const useRelatedBooks = (relatedBooks: RelatedBooks) => 94 | useQuery({ 95 | queryKey: ["relatedBooks", relatedBooks], 96 | queryFn: () => getRelatedBooks(relatedBooks), 97 | refetchOnWindowFocus: false, 98 | staleTime: 1000 * 60, 99 | }) 100 | -------------------------------------------------------------------------------- /lib/store/server/books/types.ts: -------------------------------------------------------------------------------- 1 | import { GetResponse } from "@/types/api" 2 | 3 | export interface BookQueryObject { 4 | filters?: { 5 | categories?: { 6 | slug?: { 7 | $eq?: string 8 | } 9 | } 10 | id?: { 11 | $in?: number[] 12 | } 13 | title?: { 14 | $containsi?: string 15 | } 16 | } 17 | populate?: string 18 | pagination?: { 19 | page?: number 20 | pageSize?: number 21 | limit?: number 22 | } 23 | } 24 | 25 | export interface BookQueryProps { 26 | slug?: string 27 | limit?: number 28 | pageNum?: number 29 | ids?: number[] 30 | searchTerm?: string 31 | } 32 | 33 | export type Books = GetResponse 34 | 35 | export interface Book { 36 | id: number 37 | attributes: { 38 | title: string 39 | price: number 40 | description: string 41 | slug: string 42 | in_stock: boolean 43 | createdAt: Date 44 | updatedAt: Date 45 | publishedAt: Date 46 | author_id: { 47 | data: { 48 | id: number 49 | attributes: { 50 | name: string 51 | description: null | string 52 | slug: string 53 | createdAt: Date 54 | updatedAt: Date 55 | } 56 | } 57 | } 58 | categories: { 59 | data: { 60 | id: number 61 | attributes: { 62 | name: string 63 | description: null 64 | slug: string 65 | featured: boolean | null 66 | featured_order: number | null 67 | createdAt: Date 68 | updatedAt: Date 69 | } 70 | }[] 71 | } 72 | image: { 73 | data: { 74 | id: number 75 | attributes: { 76 | name: string 77 | alternativeText: null 78 | caption: null 79 | width: number 80 | height: number 81 | formats: { 82 | small: Small 83 | thumbnail: Small 84 | } 85 | hash: string 86 | ext: ".webp" 87 | mime: "image/webp" 88 | size: number 89 | url: string 90 | previewUrl: null 91 | provider: string 92 | provider_metadata: ProviderMetadata 93 | createdAt: Date 94 | updatedAt: Date 95 | } 96 | }[] 97 | } 98 | } 99 | } 100 | 101 | interface Small { 102 | name: string 103 | hash: string 104 | ext: ".webp" 105 | mime: "image/webp" 106 | path: null 107 | width: number 108 | height: number 109 | size: number 110 | url: string 111 | provider_metadata: ProviderMetadata 112 | } 113 | 114 | interface ProviderMetadata { 115 | public_id: string 116 | resource_type: "image" 117 | } 118 | -------------------------------------------------------------------------------- /lib/store/server/categories/queries.tsx: -------------------------------------------------------------------------------- 1 | import axios from "@/lib/api/axios" 2 | import { useQuery } from "@tanstack/react-query" 3 | import { Categories } from "./types" 4 | 5 | /* ========== Get All/Featured Categories ========== */ 6 | export const getCategories = async ( 7 | featured?: boolean 8 | ): Promise => { 9 | const params = featured 10 | ? "?filters[featured][$eq]=true&sort=featured_order" 11 | : "" 12 | const response = await axios.get(`/categories${params}`) 13 | return response.data 14 | } 15 | 16 | export const useCategories = ({ 17 | categories, 18 | featured = false, 19 | }: { 20 | categories: Categories 21 | featured?: boolean 22 | }) => 23 | useQuery({ 24 | queryKey: featured ? ["categories", { featured: true }] : ["categories"], 25 | queryFn: () => getCategories(featured), 26 | initialData: categories, 27 | select: data => 28 | data.data.map(({ attributes }) => ({ 29 | name: attributes.name, 30 | slug: attributes.slug, 31 | })), 32 | }) 33 | 34 | /* ========== Get Category by Slug ========== */ 35 | export const getCategoryBySlug = async (slug: string): Promise => { 36 | const response = await axios.get( 37 | `/categories?filters[slug][$eq]]=${slug}&populate=*` 38 | ) 39 | return response.data 40 | } 41 | -------------------------------------------------------------------------------- /lib/store/server/categories/types.ts: -------------------------------------------------------------------------------- 1 | import { GetResponse } from "@/types/api" 2 | 3 | export type Categories = GetResponse 4 | 5 | export interface Category { 6 | id: number 7 | attributes: { 8 | name: string 9 | description: null 10 | slug: string 11 | featured: boolean | null 12 | featured_order: number | null 13 | createdAt: Date 14 | updatedAt: Date 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/types/Book.d.ts: -------------------------------------------------------------------------------- 1 | export interface Book { 2 | data: BookDatum[] 3 | meta: Meta 4 | } 5 | 6 | export interface BookDatum { 7 | id: number 8 | attributes: BookAttributes 9 | } 10 | 11 | export interface BookAttributes { 12 | title: string 13 | description: string 14 | price: number 15 | createdAt: Date 16 | updatedAt: Date 17 | publishedAt: Date 18 | slug: string 19 | in_stock: boolean 20 | author_id: AuthorID 21 | categories: Categories 22 | image: Image 23 | } 24 | 25 | export interface AuthorID { 26 | data: Data 27 | } 28 | 29 | export interface Data { 30 | id: number 31 | attributes: DataAttributes 32 | } 33 | 34 | export interface DataAttributes { 35 | name: string 36 | description: null 37 | createdAt: Date 38 | updatedAt: Date 39 | slug: string 40 | } 41 | 42 | export interface Categories { 43 | data: CategoriesDatum[] 44 | } 45 | 46 | export interface CategoriesDatum { 47 | id: number 48 | attributes: CategoryAttributes 49 | } 50 | 51 | export interface CategoryAttributes { 52 | name: string 53 | description: null 54 | createdAt: Date 55 | updatedAt: Date 56 | slug: string 57 | featured: boolean 58 | featured_order: number 59 | } 60 | 61 | export interface Image { 62 | data: ImageDatum[] 63 | } 64 | 65 | export interface ImageDatum { 66 | id: number 67 | attributes: ImageAttributes 68 | } 69 | 70 | export interface ImageAttributes { 71 | name: string 72 | alternativeText: null 73 | caption: null 74 | width: number 75 | height: number 76 | formats: Formats 77 | hash: string 78 | ext: string 79 | mime: string 80 | size: number 81 | url: string 82 | previewUrl: null 83 | provider: string 84 | provider_metadata: ProviderMetadata 85 | createdAt: Date 86 | updatedAt: Date 87 | } 88 | 89 | export interface Formats { 90 | small?: Small 91 | thumbnail: Small 92 | } 93 | 94 | export interface Small { 95 | name: string 96 | hash: string 97 | ext: string 98 | mime: string 99 | path: null 100 | width: number 101 | height: number 102 | size: number 103 | url: string 104 | provider_metadata: ProviderMetadata 105 | } 106 | 107 | export interface ProviderMetadata { 108 | public_id: string 109 | resource_type: string 110 | } 111 | 112 | export interface Meta { 113 | pagination: Pagination 114 | } 115 | 116 | export interface Pagination { 117 | page: number 118 | pageSize: number 119 | pageCount: number 120 | total: number 121 | } 122 | -------------------------------------------------------------------------------- /lib/types/Category.d.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | data: Datum[] 3 | meta: Meta 4 | } 5 | 6 | export interface Datum { 7 | id: number 8 | attributes: Attributes 9 | } 10 | 11 | export interface Attributes { 12 | name: string 13 | description: null 14 | createdAt: Date 15 | updatedAt: Date 16 | slug: string 17 | featured: boolean 18 | featured_order: number 19 | } 20 | 21 | export interface Meta { 22 | pagination: Pagination 23 | } 24 | 25 | export interface Pagination { 26 | page: number 27 | pageSize: number 28 | pageCount: number 29 | total: number 30 | } 31 | -------------------------------------------------------------------------------- /lib/types/api.d.ts: -------------------------------------------------------------------------------- 1 | export interface Meta { 2 | pagination: { 3 | page: number 4 | pageSize: number 5 | pageCount: number 6 | total: number 7 | } 8 | } 9 | 10 | export interface GetResponse { 11 | data: T 12 | meta: Meta 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/navLinks.tsx: -------------------------------------------------------------------------------- 1 | import HeartIcon from "@/icons/HeartIcon" 2 | import UserIcon from "@/icons/UserIcon" 3 | 4 | type Position = "top" | "top-only" | "main" | "main-mobile" 5 | 6 | type NavLinks = { 7 | name: string 8 | href: string 9 | icon: JSX.Element | null 10 | position: Position 11 | }[] 12 | 13 | const navLinks: NavLinks = [ 14 | { 15 | name: "About Us", 16 | href: "/about-us", 17 | icon: null, 18 | position: "top", 19 | }, 20 | { 21 | name: "Contact Us", 22 | href: "/contact", 23 | icon: null, 24 | position: "top", 25 | }, 26 | { 27 | name: "Privacy Policy", 28 | href: "/privacy-policy", 29 | icon: null, 30 | position: "top-only", 31 | }, 32 | { 33 | name: "Account", 34 | href: "/account", 35 | icon: , 36 | position: "main", 37 | }, 38 | { 39 | name: "Wishlist", 40 | href: "/wishlist", 41 | icon: , 42 | position: "main", 43 | }, 44 | ] 45 | 46 | export default navLinks 47 | -------------------------------------------------------------------------------- /lib/utils/scrollToTop.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | const isBrowser = () => typeof window !== "undefined" //The approach recommended by Next.js 4 | export default function scrollToTop() { 5 | if (!isBrowser()) return 6 | setTimeout(() => { 7 | window.document.body.scrollIntoView({ behavior: "smooth" }) 8 | }, 100) 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils/utilFuncs.ts: -------------------------------------------------------------------------------- 1 | import { getBooks } from "@/store/server/books/queries" 2 | import { 3 | BookQueryProps, 4 | BookQueryObject, 5 | Books, 6 | } from "@/store/server/books/types" 7 | import { Categories } from "@/store/server/categories/types" 8 | import { Image } from "@/types/Book" 9 | import qs from "qs" 10 | 11 | export const defaultStroke = (className: string): string => 12 | new RegExp("stroke-*", "g").test(className) ? "" : "stroke-2 stroke-skin-dark" 13 | 14 | export const generateUniqueArray = (num: number) => { 15 | let numbers = new Set() 16 | while (numbers.size < num) { 17 | let randomNum = Math.floor(Math.random() * (num - 1 + 1)) + 1 18 | numbers.add(randomNum) 19 | } 20 | 21 | return Array.from(numbers) 22 | } 23 | 24 | export const getOptimizedImage = (image: Image) => 25 | image.data[0].attributes.formats.small?.url || image.data[0].attributes.url 26 | 27 | export const generateBookQuery = ({ 28 | slug, 29 | limit = 10, 30 | pageNum = 1, 31 | ids = [], 32 | searchTerm = "", 33 | }: BookQueryProps) => { 34 | const queryObject: BookQueryObject = {} 35 | 36 | if (slug) { 37 | queryObject.filters = { 38 | categories: { 39 | slug: { 40 | $eq: slug, 41 | }, 42 | }, 43 | } 44 | } 45 | 46 | if (limit) { 47 | queryObject.pagination = { 48 | limit: limit, 49 | } 50 | } 51 | 52 | if (pageNum) { 53 | queryObject.pagination = { 54 | page: pageNum, 55 | pageSize: limit, 56 | } 57 | } 58 | 59 | if (ids.length > 0) { 60 | queryObject.filters = { 61 | id: { 62 | $in: ids, 63 | }, 64 | ...queryObject.filters, 65 | } 66 | } 67 | 68 | if (searchTerm) { 69 | queryObject.filters = { 70 | title: { 71 | $containsi: searchTerm, 72 | }, 73 | ...queryObject.filters, 74 | } 75 | } 76 | 77 | return qs.stringify(queryObject, { 78 | encodeValuesOnly: true, // prettify URL 79 | }) 80 | } 81 | 82 | export const getInitialBooks = async (categories: Categories) => { 83 | const books: Record = {} 84 | await Promise.all( 85 | categories.data.map(async ({ attributes }) => { 86 | const slug = attributes.slug 87 | const booksRes = await getBooks({ slug, limit: 5 }) 88 | books[slug] = booksRes 89 | }) 90 | ) 91 | return books 92 | } 93 | -------------------------------------------------------------------------------- /next-bookstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/next-bookstore.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["res.cloudinary.com"], 5 | }, 6 | compiler: { 7 | removeConsole: process.env.NODE_ENV === "production", 8 | }, 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-bookstore", 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 | "format:check": "prettier --check .", 11 | "format": "prettier --write ." 12 | }, 13 | "dependencies": { 14 | "@radix-ui/react-alert-dialog": "^1.0.4", 15 | "@radix-ui/react-collapsible": "^1.0.3", 16 | "@radix-ui/react-dialog": "^1.0.4", 17 | "@radix-ui/react-navigation-menu": "^1.1.3", 18 | "@radix-ui/react-radio-group": "^1.1.3", 19 | "@radix-ui/react-toast": "^1.1.4", 20 | "@tanstack/react-query": "^4.29.15", 21 | "@tanstack/react-query-devtools": "^4.29.15", 22 | "axios": "^1.4.0", 23 | "next": "^13.4.7", 24 | "qs": "^6.11.2", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "react-hook-form": "^7.45.0", 28 | "react-markdown": "^8.0.7", 29 | "zustand": "^4.3.8" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "20.3.1", 33 | "@types/qs": "^6.9.7", 34 | "@types/react": "18.2.13", 35 | "@types/react-dom": "18.2.6", 36 | "autoprefixer": "^10.4.14", 37 | "cz-conventional-changelog": "^3.3.0", 38 | "eslint": "^8.43.0", 39 | "eslint-config-next": "^13.4.7", 40 | "postcss": "^8.4.24", 41 | "prettier": "^2.8.8", 42 | "prettier-plugin-tailwindcss": "^0.3.0", 43 | "tailwindcss": "^3.3.2", 44 | "typescript": "^5.1.3" 45 | }, 46 | "config": { 47 | "commitizen": { 48 | "path": "./node_modules/cz-conventional-changelog" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next" 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: "John Doe" }) 13 | } 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/about.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/about.webp -------------------------------------------------------------------------------- /public/books-collection.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/books-collection.webp -------------------------------------------------------------------------------- /public/cafe-book.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/cafe-book.webp -------------------------------------------------------------------------------- /public/default-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/default-og.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/favicon.ico -------------------------------------------------------------------------------- /public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/icons/favicon.ico -------------------------------------------------------------------------------- /public/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/icons/icon.png -------------------------------------------------------------------------------- /public/instagram-photos/ig_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/instagram-photos/ig_1.jpg -------------------------------------------------------------------------------- /public/instagram-photos/ig_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/instagram-photos/ig_2.jpg -------------------------------------------------------------------------------- /public/instagram-photos/ig_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/instagram-photos/ig_3.jpg -------------------------------------------------------------------------------- /public/instagram-photos/ig_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/instagram-photos/ig_4.jpg -------------------------------------------------------------------------------- /public/instagram-photos/ig_5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/instagram-photos/ig_5.jpg -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /public/we-were-liars-book.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/next-bookstore/9ce35642fd660f304fbe7708b8692fef46f7e467/public/we-were-liars-book.jpeg -------------------------------------------------------------------------------- /script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "VERCEL_ENV: $VERCEL_ENV" 4 | 5 | if [[ "$VERCEL_ENV" == "production" ]] ; then 6 | # Proceed with the build 7 | echo "✅ - Build can proceed" 8 | exit 1; 9 | 10 | else 11 | # Don't build 12 | echo "🛑 - Build cancelled" 13 | exit 0; 14 | fi -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | "./app/**/*.{js,ts,jsx,tsx}", 7 | "./pages/**/*.{js,ts,jsx,tsx}", 8 | "./components/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | serif: ["var(--font-fraunces)", ...fontFamily.serif], 14 | sans: ["var(--font-quicksand)", ...fontFamily.sans], 15 | }, 16 | colors: { 17 | skin: { 18 | accent: { DEFAULT: "#53CAB5", dark: "#3CAF9A" }, 19 | base: "#F9FFFF", 20 | muted: "#EDF4F4", 21 | dark: "#363636", 22 | gray: "#D1DBD9", 23 | }, 24 | }, 25 | boxShadow: { 26 | upper: 27 | "0 -1px 3px 0 rgb(0 0 0 / 0.1), 0 -1px 2px -1px rgb(0 0 0 / 0.1)", 28 | "upper-md": 29 | "0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1)", 30 | }, 31 | keyframes: { 32 | hide: { 33 | from: { opacity: 1 }, 34 | to: { opacity: 0 }, 35 | }, 36 | slideIn: { 37 | from: { 38 | transform: "translateX(calc(100% + var(--viewport-padding)))", 39 | }, 40 | to: { transform: "translateX(0))" }, 41 | }, 42 | swipeOut: { 43 | from: { transform: "translateX(var(--radix-toast-swipe-end-x))" }, 44 | to: { transform: "translateX(calc(100% + var(--viewport-padding)))" }, 45 | }, 46 | }, 47 | animation: { 48 | hide: "hide 100ms ease-in", 49 | slideIn: "slideIn 150ms cubic-bezier(0.16, 1, 0.3, 1)", 50 | swipeOut: "swipeOut 100ms ease-out", 51 | }, 52 | }, 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/public/*": ["public/*"], 6 | "@/components/*": ["app/components/*"], 7 | "@/loading-ui/*": ["app/components/loading-ui/*"], 8 | "@/icons/*": ["app/icons/*"], 9 | "@/utils/*": ["lib/utils/*"], 10 | "@/types/*": ["lib/types/*"], 11 | "@/hooks": ["lib/hooks"], 12 | "@/store/*": ["lib/store/*"], 13 | "@/lib/*": ["lib/*"] 14 | }, 15 | "target": "es5", 16 | "lib": ["dom", "dom.iterable", "esnext"], 17 | "allowJs": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "noEmit": true, 22 | "esModuleInterop": true, 23 | "module": "esnext", 24 | "moduleResolution": "node", 25 | "resolveJsonModule": true, 26 | "isolatedModules": true, 27 | "jsx": "preserve", 28 | "incremental": true, 29 | "plugins": [ 30 | { 31 | "name": "next" 32 | } 33 | ] 34 | }, 35 | "include": [ 36 | "next-env.d.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | ".next/types/**/*.ts", 40 | "/Users/satnaing/dev/react/next-bookstore/.next/types/**/*.ts" 41 | ], 42 | "exclude": ["node_modules"] 43 | } 44 | --------------------------------------------------------------------------------