├── .dockerignore ├── .eslintrc.json ├── public ├── og.png ├── googlebbcd930f1ecacd3a.html ├── favicon.ico ├── icon-192x192.png ├── icon-256x256.png ├── icon-384x384.png ├── icon-512x512.png ├── bg-img │ ├── ourshop.png │ ├── monigote.jpg │ ├── monigote-tablet.png │ ├── monigote_mobile.jpg │ ├── banner_minipage1.jpg │ ├── banner_minipage2.jpg │ ├── banner_minipage3.jpg │ ├── curly_hair_girl-1.jpg │ ├── curly_hair_white-1.jpg │ ├── banner_minipage1-tablet.jpg │ ├── curly_hair_girl-1-tablet.png │ ├── curly_hair_girl-1_mobile.jpg │ ├── curly_hair_white-1-tablet.png │ ├── curly_hair_white-1_mobile.jpg │ └── coding.svg ├── maskable_icon.png ├── favicons │ ├── favicon.ico │ ├── favicon_io.zip │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── icons │ ├── Loading.tsx │ ├── heart.svg │ ├── facebook.svg │ ├── RightArrow.tsx │ ├── NextArrow.tsx │ ├── PrevArrow.tsx │ ├── DownArrow.tsx │ ├── UserIcon.tsx │ ├── MenuIcon.tsx │ ├── BagIcon.tsx │ ├── SearchIcon.tsx │ ├── LeftArrow.tsx │ ├── HeartSolid.tsx │ ├── WhistlistIcon.tsx │ ├── FacebookLogo.tsx │ ├── Heart.tsx │ ├── instagram.svg │ ├── Loading.module.css │ └── InstagramLogo.tsx ├── vercel.svg ├── site-logo.svg └── logo.svg ├── styles ├── Test.module.css ├── styles.css ├── Home.module.css └── globals.css ├── postcss.config.js ├── components ├── Util │ ├── utilFunc.ts │ ├── useWindowSize.tsx │ ├── Pagination.tsx │ └── Items.js ├── Card │ ├── Card.module.css │ └── Card.tsx ├── Footer │ ├── Footer.module.css │ └── Footer.tsx ├── Buttons │ ├── TextButton.tsx │ ├── Button.tsx │ ├── GhostButton.tsx │ └── LinkButton.tsx ├── Header │ ├── Header.module.css │ ├── AppHeader.tsx │ ├── TopNav.tsx │ └── Header.tsx ├── HeroSection │ ├── Hero.module.css │ └── Slideshow.tsx ├── Input │ └── Input.tsx ├── OverlayContainer │ ├── OverlayContainer.module.css │ └── OverlayContainer.tsx ├── CartItem │ ├── Item.tsx │ └── CartItem.tsx ├── Auth │ ├── ForgotPassword.tsx │ ├── Login.tsx │ ├── Register.tsx │ └── AuthForm.tsx ├── TestiSlider │ └── TestiSlider.tsx └── SearchForm │ └── SearchForm.tsx ├── Dockerfile ├── next-env.d.ts ├── context ├── cart │ ├── CartContext.ts │ ├── cart-types.ts │ ├── cartReducer.ts │ └── CartProvider.tsx ├── wishlist │ ├── WishlistContext.ts │ ├── wishlist-type.ts │ ├── wishlistReducer.ts │ └── WishlistProvider.tsx ├── Util │ ├── addWishlist.ts │ ├── addItemToCart.ts │ └── removeItemFromCart.ts └── AuthContext.tsx ├── next.config.js ├── .gitignore ├── tsconfig.json ├── package.json ├── pages ├── 404.tsx ├── coming-soon.tsx ├── _app.tsx ├── _document.tsx ├── search.tsx ├── wishlist.tsx ├── index.tsx ├── product-category │ └── [category].tsx └── shopping-cart.tsx ├── tailwind.config.js ├── README.md └── messages └── common ├── en.json └── my.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | Dockerfile 3 | .git -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/og.png -------------------------------------------------------------------------------- /public/googlebbcd930f1ecacd3a.html: -------------------------------------------------------------------------------- 1 | google-site-verification: googlebbcd930f1ecacd3a.html -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/icon-192x192.png -------------------------------------------------------------------------------- /public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/icon-256x256.png -------------------------------------------------------------------------------- /public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/icon-384x384.png -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/icon-512x512.png -------------------------------------------------------------------------------- /public/bg-img/ourshop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/ourshop.png -------------------------------------------------------------------------------- /public/maskable_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/maskable_icon.png -------------------------------------------------------------------------------- /public/bg-img/monigote.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/monigote.jpg -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/favicon.ico -------------------------------------------------------------------------------- /styles/Test.module.css: -------------------------------------------------------------------------------- 1 | .grid::after { 2 | content: ""; 3 | flex: auto; 4 | margin-right: 16px; 5 | } 6 | -------------------------------------------------------------------------------- /public/favicons/favicon_io.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/favicon_io.zip -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/bg-img/monigote-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/monigote-tablet.png -------------------------------------------------------------------------------- /public/bg-img/monigote_mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/monigote_mobile.jpg -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /components/Util/utilFunc.ts: -------------------------------------------------------------------------------- 1 | export const roundDecimal = (num: number) => 2 | (Math.round(num * 100) / 100).toFixed(2); 3 | -------------------------------------------------------------------------------- /public/bg-img/banner_minipage1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/banner_minipage1.jpg -------------------------------------------------------------------------------- /public/bg-img/banner_minipage2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/banner_minipage2.jpg -------------------------------------------------------------------------------- /public/bg-img/banner_minipage3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/banner_minipage3.jpg -------------------------------------------------------------------------------- /public/bg-img/curly_hair_girl-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_girl-1.jpg -------------------------------------------------------------------------------- /public/bg-img/curly_hair_white-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_white-1.jpg -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/bg-img/banner_minipage1-tablet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/banner_minipage1-tablet.jpg -------------------------------------------------------------------------------- /public/bg-img/curly_hair_girl-1-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_girl-1-tablet.png -------------------------------------------------------------------------------- /public/bg-img/curly_hair_girl-1_mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_girl-1_mobile.jpg -------------------------------------------------------------------------------- /public/bg-img/curly_hair_white-1-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_white-1-tablet.png -------------------------------------------------------------------------------- /public/bg-img/curly_hair_white-1_mobile.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/bg-img/curly_hair_white-1_mobile.jpg -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/satnaing/haru-fashion/HEAD/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY package*.json ./ 6 | 7 | RUN npm install 8 | 9 | COPY . . 10 | 11 | RUN npm run build 12 | 13 | EXPOSE 3000 14 | 15 | CMD ["npm","run","start"] -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /public/icons/Loading.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./Loading.module.css"; 2 | 3 | const Loading = () => ( 4 |
5 |
6 |
7 | {/*
*/} 8 |
9 |
10 | ); 11 | 12 | export default Loading; 13 | -------------------------------------------------------------------------------- /context/cart/CartContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { cartType } from "./cart-types"; 3 | 4 | export const initialContextValues: cartType = { 5 | cart: [], 6 | }; 7 | 8 | const CartContext = createContext(initialContextValues); 9 | 10 | export default CartContext; 11 | -------------------------------------------------------------------------------- /context/wishlist/WishlistContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | import { wishlistType } from "./wishlist-type"; 3 | 4 | export const initialContextValues: wishlistType = { 5 | wishlist: [], 6 | }; 7 | 8 | const WishlistContext = createContext(initialContextValues); 9 | 10 | export default WishlistContext; 11 | -------------------------------------------------------------------------------- /public/icons/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/icons/facebook.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/icons/RightArrow.tsx: -------------------------------------------------------------------------------- 1 | const RightArrow = () => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default RightArrow; 19 | -------------------------------------------------------------------------------- /public/icons/NextArrow.tsx: -------------------------------------------------------------------------------- 1 | const NextArrow = () => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default NextArrow; 19 | -------------------------------------------------------------------------------- /public/icons/PrevArrow.tsx: -------------------------------------------------------------------------------- 1 | const PrevArrow = () => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default PrevArrow; 19 | -------------------------------------------------------------------------------- /context/Util/addWishlist.ts: -------------------------------------------------------------------------------- 1 | import { itemType } from "../wishlist/wishlist-type"; 2 | 3 | const addWishlist = (wishlistItems: itemType[], item: itemType) => { 4 | const duplicate = wishlistItems.some( 5 | (wishlistItem) => wishlistItem.id === item!.id 6 | ); 7 | 8 | if (!duplicate) { 9 | return [...wishlistItems, { ...item }]; 10 | } else { 11 | return [...wishlistItems]; 12 | } 13 | }; 14 | 15 | export default addWishlist; 16 | -------------------------------------------------------------------------------- /public/icons/DownArrow.tsx: -------------------------------------------------------------------------------- 1 | const DownArrow = ({ extraClass = "" }) => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default DownArrow; 19 | -------------------------------------------------------------------------------- /public/icons/UserIcon.tsx: -------------------------------------------------------------------------------- 1 | const UserIcon = () => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default UserIcon; 19 | -------------------------------------------------------------------------------- /public/icons/MenuIcon.tsx: -------------------------------------------------------------------------------- 1 | const MenuIcon = ({ size = "md", extraClass = "" }) => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default MenuIcon; 19 | -------------------------------------------------------------------------------- /public/icons/BagIcon.tsx: -------------------------------------------------------------------------------- 1 | const BagIcon = ({ extraClass = "" }) => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default BagIcon; 19 | -------------------------------------------------------------------------------- /public/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | const SearchIcon = ({ extraClass = "" }) => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default SearchIcon; 19 | -------------------------------------------------------------------------------- /public/icons/LeftArrow.tsx: -------------------------------------------------------------------------------- 1 | const LeftArrow = ({ size = "md", extraClass = "" }) => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default LeftArrow; 19 | -------------------------------------------------------------------------------- /components/Util/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useWindowSize = () => { 4 | const [size, setSize] = useState([0, 0]); 5 | useEffect(() => { 6 | function updateSize() { 7 | setSize([window.innerWidth, window.innerHeight]); 8 | } 9 | window.addEventListener("resize", updateSize); 10 | updateSize(); 11 | return () => window.removeEventListener("resize", updateSize); 12 | }, []); 13 | return size; 14 | }; 15 | 16 | export default useWindowSize; 17 | -------------------------------------------------------------------------------- /public/icons/HeartSolid.tsx: -------------------------------------------------------------------------------- 1 | const HeartSolid = ({ extraClass = "" }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default HeartSolid; 14 | -------------------------------------------------------------------------------- /public/icons/WhistlistIcon.tsx: -------------------------------------------------------------------------------- 1 | const WhistlistIcon = () => ( 2 | 9 | 15 | 16 | ); 17 | 18 | export default WhistlistIcon; 19 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const withPWA = require("next-pwa"); 2 | 3 | module.exports = withPWA({ 4 | // module.exports = { 5 | i18n: { 6 | locales: ["en", "my"], 7 | defaultLocale: "en", 8 | }, 9 | reactStrictMode: true, 10 | // swcMinify: true, 11 | compiler: { 12 | removeConsole: true, 13 | }, 14 | images: { 15 | domains: ["robohash.org", "res.cloudinary.com"], 16 | }, 17 | pwa: { 18 | dest: "public", 19 | skipWaiting: true, 20 | disable: process.env.NODE_ENV === "development", 21 | }, 22 | // }; 23 | }); 24 | -------------------------------------------------------------------------------- /components/Card/Card.module.css: -------------------------------------------------------------------------------- 1 | .card { 2 | @apply w-full; 3 | } 4 | 5 | .imageContainer { 6 | @apply relative overflow-hidden mb-1; 7 | } 8 | 9 | .addBtn { 10 | @apply hidden sm:block bg-white text-gray400 hover:text-gray100 hover:bg-gray500 font-medium whitespace-nowrap px-4 py-2 mx-auto absolute bottom-4 md:-bottom-10 right-1/2 transform translate-x-1/2 transition-all duration-500; 11 | } 12 | 13 | .addBtn:focus { 14 | @apply bottom-8 duration-75; 15 | } 16 | 17 | .card:hover .addBtn { 18 | @apply md:bottom-8; 19 | } 20 | 21 | .itemName { 22 | @apply text-xs sm:text-base block no-underline text-gray500 mb-1 truncate; 23 | } 24 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # backups 37 | *.backup.* 38 | 39 | # next-pwa 40 | **/public/workbox-*.js 41 | **/public/sw.js 42 | **/public/worker-*.js 43 | -------------------------------------------------------------------------------- /components/Footer/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footerHead { 2 | @apply text-gray400 text-lg mb-1 md:mb-3; 3 | } 4 | 5 | .column { 6 | @apply flex flex-col; 7 | } 8 | 9 | .column a:hover { 10 | @apply text-gray400 underline; 11 | } 12 | 13 | .column a, 14 | .column span { 15 | @apply py-2; 16 | } 17 | 18 | .footerContainer { 19 | @apply border-t-2 border-gray100 py-16; 20 | } 21 | 22 | .footerContents { 23 | @apply flex flex-col md:flex-row justify-between; 24 | } 25 | 26 | .footerContainer > div { 27 | @apply mb-6 md:mb-0; 28 | } 29 | 30 | .bottomFooter { 31 | @apply border-2 py-1 text-xs border-gray200 text-gray400; 32 | } 33 | 34 | .bottomFooter a { 35 | @apply ml-4; 36 | } 37 | -------------------------------------------------------------------------------- /public/icons/FacebookLogo.tsx: -------------------------------------------------------------------------------- 1 | const FacebookLogo = ({ extraClass = "" }) => ( 2 | 18 | ); 19 | 20 | export default FacebookLogo; 21 | -------------------------------------------------------------------------------- /components/Buttons/TextButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | type Props = { 4 | // size: "small" | "large"; 5 | value: string; 6 | }; 7 | 8 | const TextButton: FC = ({ value }) => ( 9 |
10 | 16 |
17 |
18 | ); 19 | 20 | export default TextButton; 21 | -------------------------------------------------------------------------------- /context/wishlist/wishlist-type.ts: -------------------------------------------------------------------------------- 1 | export const ADD_TO_WISHLIST = "ADD_TO_WISHLIST"; 2 | export const DELETE_WISHLIST_ITEM = "DELETE_WISHLIST_ITEMS"; 3 | export const SET_WISHLIST = "SET_WISHLIST"; 4 | export const CLEAR_WISHLIST = "CLEAR_WISHLIST"; 5 | 6 | export type itemType = { 7 | id: number; 8 | img1?: string; 9 | img2?: string; 10 | name: string; 11 | price: number; 12 | qty?: number; 13 | }; 14 | 15 | export type wishlistType = { 16 | wishlist: itemType[]; 17 | // addItem?: (item: itemType) => void; // delete 18 | addToWishlist?: (item: itemType) => void; 19 | // removeItem?: (item: itemType) => void; // delete 20 | deleteWishlistItem?: (item: itemType) => void; 21 | clearWishlist?: () => void; 22 | }; 23 | -------------------------------------------------------------------------------- /public/icons/Heart.tsx: -------------------------------------------------------------------------------- 1 | const Heart = ({ extraClass = "", white = false }) => ( 2 | 9 | 10 | 11 | ); 12 | 13 | export default Heart; 14 | -------------------------------------------------------------------------------- /components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | /* Top Bar */ 2 | .topLeftMenu { 3 | @apply ml-8; 4 | } 5 | 6 | .topLeftMenu li { 7 | @apply text-xs my-2 mr-4 hover:text-gray300; 8 | } 9 | 10 | .topRightMenu li { 11 | @apply text-xs my-2 ml-4 hover:text-gray300; 12 | } 13 | 14 | /* Main Menu */ 15 | .mainMenu { 16 | @apply py-6; 17 | } 18 | /* .leftMenu { 19 | @apply ml-8 my-6; 20 | } */ 21 | 22 | .leftMenu li { 23 | @apply mr-12 hidden lg:block whitespace-nowrap; 24 | } 25 | 26 | .leftMenu li:hover { 27 | @apply text-gray400; 28 | } 29 | 30 | .leftMenu li:last-child { 31 | @apply mr-0; 32 | } 33 | 34 | .rightMenu li { 35 | @apply ml-12; 36 | } 37 | 38 | .rightMenu li:not(:last-child) { 39 | @apply hidden lg:block; 40 | } 41 | 42 | .rightMenu li:nth-child(3) { 43 | @apply hidden sm:block; 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "importHelpers": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "incremental": true 22 | }, 23 | "typeRoots": [ 24 | "./types", 25 | "./node_modules/@types" 26 | ], 27 | "include": [ 28 | "next-env.d.ts", 29 | "**/*.ts", 30 | "**/*.tsx", 31 | "pages/_app.js" 32 | ], 33 | "exclude": [ 34 | "node_modules" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e-commerce", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "lint": "next lint", 8 | "build": "next build", 9 | "start": "next start" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.0.0", 13 | "animate.css": "^4.1.1", 14 | "axios": "^0.26.0", 15 | "cookies-next": "^2.0.4", 16 | "next": "^12.1.0", 17 | "next-intl": "^2.4.0", 18 | "next-pwa": "^5.4.4", 19 | "nprogress": "^0.2.0", 20 | "react": "17.0.2", 21 | "react-dom": "17.0.2", 22 | "swiper": "^6.6.2" 23 | }, 24 | "devDependencies": { 25 | "@types/nprogress": "^0.2.0", 26 | "@types/react": "^17.0.3", 27 | "autoprefixer": "^10.2.5", 28 | "eslint": "8.9.0", 29 | "eslint-config-next": "12.1.0", 30 | "postcss": "^8.2.9", 31 | "tailwindcss": "^2.1.1", 32 | "typescript": "^4.2.4" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /context/Util/addItemToCart.ts: -------------------------------------------------------------------------------- 1 | import { itemType } from "../cart/cart-types"; 2 | 3 | const addItemToCart = ( 4 | cartItems: itemType[], 5 | item: itemType, 6 | add_one = false 7 | ) => { 8 | const duplicate = cartItems.some((cartItem) => cartItem.id === item.id); 9 | 10 | if (duplicate) { 11 | return cartItems.map((cartItem) => { 12 | let itemQty = 0; 13 | !item.qty || add_one 14 | ? (itemQty = cartItem.qty! + 1) 15 | : (itemQty = item.qty); 16 | 17 | console.log(itemQty); 18 | return cartItem.id === item.id ? { ...cartItem, qty: itemQty } : cartItem; 19 | }); 20 | } 21 | // console.log(itemQty); 22 | let itemQty = 0; 23 | !item.qty ? itemQty++ : (itemQty = item.qty); 24 | return [ 25 | ...cartItems, 26 | { 27 | id: item.id, 28 | name: item.name, 29 | price: item.price, 30 | img1: item.img1, 31 | img2: item.img2, 32 | qty: itemQty, 33 | }, 34 | ]; 35 | }; 36 | 37 | export default addItemToCart; 38 | -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "theme_color": "#282828", 3 | "background_color": "#ffffff", 4 | "display": "standalone", 5 | "scope": "/", 6 | "start_url": "/", 7 | "name": "Haru Fashion ", 8 | "short_name": "Haru", 9 | "description": "Discover affordable and fashionable men's and women's clothing online at Haru Fashion", 10 | "icons": [ 11 | { 12 | "src": "/icon-192x192.png", 13 | "sizes": "192x192", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/icon-256x256.png", 18 | "sizes": "256x256", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/icon-384x384.png", 23 | "sizes": "384x384", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/icon-512x512.png", 28 | "sizes": "512x512", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "/maskable_icon.png", 33 | "sizes": "196x196", 34 | "type": "image/png", 35 | "purpose": "any maskable" 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /components/HeroSection/Hero.module.css: -------------------------------------------------------------------------------- 1 | .rightTextSection { 2 | /* @apply absolute top-1/3 right-8 sm:right-12 md:right-20 lg:right-40 flex flex-col items-end; */ 3 | @apply absolute bg-white p-4 opacity-90 sm:bg-transparent sm:p-0 sm:opacity-100 bottom-10 right-1/2 transform translate-x-1/2 sm:transform-none sm:top-1/3 sm:right-12 md:right-20 lg:right-40 flex flex-col items-center sm:items-end; 4 | } 5 | 6 | .leftTextSection { 7 | @apply absolute bg-white p-4 opacity-90 sm:bg-transparent sm:p-0 sm:opacity-100 bottom-10 right-1/2 transform translate-x-1/2 sm:transform-none sm:top-1/3 sm:left-12 md:left-20 lg:left-40 flex flex-col items-center sm:items-start; 8 | } 9 | 10 | .subtitle { 11 | @apply bg-gray500 text-gray100 inline-block text-base sm:text-xs p-1 rounded-md; 12 | } 13 | 14 | .title { 15 | @apply text-4xl sm:text-5xl md:text-6xl lg:text-7xl my-4; 16 | } 17 | 18 | .arrows { 19 | @apply w-8 h-8 absolute cursor-pointer flex justify-center items-center bg-gray300 rounded-full opacity-60 hover:opacity-100; 20 | } 21 | -------------------------------------------------------------------------------- /components/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import { FC, FormEvent } from "react"; 2 | 3 | type Props = { 4 | type?: string; 5 | name: string; 6 | placeholder?: string; 7 | extraClass?: string; 8 | required?: boolean; 9 | border?: string; 10 | id?: string; 11 | label?: string; 12 | onChange?: (e: FormEvent) => void; 13 | value?: string; 14 | readOnly?: boolean; 15 | }; 16 | 17 | const Input: FC = ({ 18 | type = "text", 19 | name, 20 | placeholder, 21 | extraClass, 22 | required = false, 23 | border = "", 24 | label = "", 25 | onChange, 26 | value, 27 | readOnly = false, 28 | }) => ( 29 | 42 | ); 43 | 44 | export default Input; 45 | -------------------------------------------------------------------------------- /context/Util/removeItemFromCart.ts: -------------------------------------------------------------------------------- 1 | import { itemType } from "../cart/cart-types"; 2 | 3 | const removeItemFromCart = (cartItems: itemType[], item: itemType) => { 4 | // const duplicate = cartItems.some((cartItem) => cartItem.id === item.id); 5 | if (item.qty === 1) { 6 | return cartItems.filter((cartItem) => cartItem.id !== item.id); 7 | } 8 | return cartItems.map((cartItem) => 9 | cartItem.id === item.id ? { ...cartItem, qty: cartItem.qty! - 1 } : cartItem 10 | ); 11 | // if (duplicate) { 12 | // return cartItems.map((cartItem) => 13 | // cartItem.id === item.id 14 | // ? { ...cartItem, qty: cartItem.qty - 1 } 15 | // : cartItem 16 | // ); 17 | // } 18 | // return [ 19 | // ...cartItems, 20 | // { 21 | // id: item.id, 22 | // name: item.name, 23 | // price: item.price, 24 | // img1: item.img1, 25 | // img2: item.img2, 26 | // qty: 1, 27 | // }, 28 | // ]; 29 | }; 30 | 31 | export default removeItemFromCart; 32 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/styles.css: -------------------------------------------------------------------------------- 1 | html { 2 | scroll-behavior: smooth; 3 | } 4 | 5 | .swiper-button-prev::after, 6 | .swiper-button-next::after { 7 | color: #282828; 8 | font-size: 1.5rem !important; 9 | font-weight: bold; 10 | padding: 0.5rem 0.875rem; 11 | border-radius: 100%; 12 | background-color: #ddd; 13 | } 14 | 15 | .swiper-button-prev::after { 16 | margin-left: 5rem; 17 | } 18 | 19 | .swiper-button-next::after { 20 | margin-right: 5rem; 21 | } 22 | 23 | @media only screen and (max-width: 769px) { 24 | .swiper-button-prev::after { 25 | margin-left: 1rem; 26 | } 27 | 28 | .swiper-button-next::after { 29 | margin-right: 1rem; 30 | } 31 | } 32 | 33 | /* ===== Custom Swiper pagination ===== */ 34 | .card-swiper .swiper-pagination-bullets { 35 | bottom: 0 !important; 36 | } 37 | 38 | .swiper-pagination-bullet { 39 | width: 1rem !important; 40 | border-radius: 0 !important; 41 | height: 0.35rem !important; 42 | } 43 | 44 | .swiper-pagination-bullet-active { 45 | background-color: #282828 !important; 46 | } 47 | 48 | /* .swiper-slide { 49 | width: 100% !important; 50 | margin: auto !important; 51 | } */ 52 | -------------------------------------------------------------------------------- /public/icons/instagram.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /context/wishlist/wishlistReducer.ts: -------------------------------------------------------------------------------- 1 | import addWishlist from "../Util/addWishlist"; 2 | import { 3 | ADD_TO_WISHLIST, 4 | DELETE_WISHLIST_ITEM, 5 | CLEAR_WISHLIST, 6 | wishlistType, 7 | itemType, 8 | SET_WISHLIST, 9 | } from "./wishlist-type"; 10 | 11 | type actionType = { 12 | type: string; 13 | payload?: itemType | itemType[]; 14 | }; 15 | 16 | const wishlistReducer = (state: wishlistType, action: actionType) => { 17 | switch (action.type) { 18 | case ADD_TO_WISHLIST: 19 | return { 20 | ...state, 21 | wishlist: addWishlist(state.wishlist, action.payload as itemType), 22 | }; 23 | case DELETE_WISHLIST_ITEM: 24 | return { 25 | ...state, 26 | wishlist: state.wishlist.filter( 27 | (wishlistItem) => wishlistItem.id !== (action.payload as itemType).id 28 | ), 29 | }; 30 | case SET_WISHLIST: 31 | return { 32 | ...state, 33 | wishlist: action.payload as itemType[], 34 | }; 35 | case CLEAR_WISHLIST: 36 | return { 37 | ...state, 38 | wishlist: [], 39 | }; 40 | default: 41 | return state; 42 | } 43 | }; 44 | 45 | export default wishlistReducer; 46 | -------------------------------------------------------------------------------- /components/Buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | 3 | type Props = { 4 | type?: "button" | "submit" | "reset"; 5 | extraClass?: string; 6 | size?: "sm" | "lg" | "xl"; 7 | value: string; 8 | disabled?: boolean; 9 | onClick?: React.MouseEventHandler; 10 | }; 11 | 12 | const Button: FC = ({ 13 | size = "sm", 14 | value, 15 | extraClass, 16 | onClick, 17 | children, 18 | type = "button", 19 | disabled = false, 20 | }) => { 21 | let btnSize = ""; 22 | if (size === "sm") { 23 | btnSize = "py-2 sm:py-1 px-5"; 24 | } else if (size === "lg") { 25 | btnSize = "py-3 sm:py-2 px-6"; 26 | } else { 27 | btnSize = "py-4 sm:py-3 px-7 text-xl"; 28 | } 29 | return ( 30 | 42 | ); 43 | }; 44 | 45 | export default Button; 46 | -------------------------------------------------------------------------------- /components/Buttons/GhostButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC, forwardRef } from "react"; 2 | 3 | type Props = { 4 | extraClass?: string; 5 | size?: "sm" | "normal" | "lg"; 6 | inverted?: boolean; 7 | noBorder?: boolean; 8 | onClick?: React.MouseEventHandler; 9 | }; 10 | 11 | // eslint-disable-next-line react/display-name 12 | const GhostButton: FC = ({ 13 | onClick, 14 | size, 15 | extraClass, 16 | noBorder = false, 17 | inverted = true, 18 | children, 19 | }) => { 20 | let btnSize = ""; 21 | if (size === "sm") { 22 | btnSize = "py-2 sm:py-1 px-5"; 23 | } else if (size === "lg") { 24 | btnSize = "py-4 sm:py-3 px-7 text-xl"; 25 | } else { 26 | btnSize = "py-3 sm:py-2 px-6"; 27 | } 28 | 29 | return ( 30 | 41 | ); 42 | }; 43 | 44 | export default GhostButton; 45 | -------------------------------------------------------------------------------- /public/icons/Loading.module.css: -------------------------------------------------------------------------------- 1 | .ldsEllipsis { 2 | display: inline-block; 3 | position: relative; 4 | width: 30px; 5 | height: 30px; 6 | } 7 | .ldsEllipsis div { 8 | position: absolute; 9 | top: 10px; 10 | width: 8px; 11 | height: 8px; 12 | border-radius: 50%; 13 | background: #686868; 14 | animation-timing-function: cubic-bezier(0, 1, 1, 0); 15 | } 16 | .ldsEllipsis div:nth-child(1) { 17 | left: -4px; 18 | animation: ldsEllipsis1 0.6s infinite; 19 | } 20 | .ldsEllipsis div:nth-child(2) { 21 | left: -8px; 22 | animation: ldsEllipsis2 0.6s infinite; 23 | } 24 | /* .ldsEllipsis div:nth-child(3) { 25 | left: 32px; 26 | animation: ldsEllipsis2 0.6s infinite; 27 | } */ 28 | .ldsEllipsis div:nth-child(3) { 29 | left: 28px; 30 | animation: ldsEllipsis3 0.6s infinite; 31 | } 32 | @keyframes ldsEllipsis1 { 33 | 0% { 34 | transform: scale(0); 35 | } 36 | 100% { 37 | transform: scale(1); 38 | } 39 | } 40 | @keyframes ldsEllipsis3 { 41 | 0% { 42 | transform: scale(1); 43 | } 44 | 100% { 45 | transform: scale(0); 46 | } 47 | } 48 | @keyframes ldsEllipsis2 { 49 | 0% { 50 | transform: translate(0, 0); 51 | } 52 | 100% { 53 | transform: translate(24px, 0); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import AppHeader from "../components/Header/AppHeader"; 6 | import { GetStaticProps } from "next"; 7 | 8 | const Custom404 = () => { 9 | const t = useTranslations("Others"); 10 | return ( 11 | <> 12 | 13 |
14 |

{t("page_not_found")}

15 | 404 Page Not Found 21 | 22 | {t("go_back_to")}{" "} 23 | 24 | home page 25 | 26 | ? 27 | 28 |
29 | 30 | ); 31 | }; 32 | 33 | export const getStaticProps: GetStaticProps = async ({ locale }) => { 34 | return { 35 | props: { 36 | messages: (await import(`../messages/common/${locale}.json`)).default, 37 | }, 38 | }; 39 | }; 40 | 41 | export default Custom404; 42 | -------------------------------------------------------------------------------- /components/Buttons/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import Link from "next/link"; 3 | 4 | type Props = { 5 | extraClass?: string; 6 | href: string; 7 | aria_label?: string; 8 | size?: "sm" | "normal" | "xl"; 9 | inverted?: boolean; 10 | noBorder?: boolean; 11 | onClick?: React.MouseEventHandler; 12 | }; 13 | 14 | const LinkButton: FC = ({ 15 | href, 16 | extraClass, 17 | size, 18 | aria_label, 19 | children, 20 | noBorder = true, 21 | inverted = true, 22 | }) => { 23 | let btnSize = ""; 24 | if (size === "sm") { 25 | btnSize = "py-2 sm:py-1 px-5"; 26 | } else if (size === "xl") { 27 | btnSize = "py-4 sm:py-3 px-7 text-xl"; 28 | } else { 29 | btnSize = "py-3 sm:py-2 px-6"; 30 | } 31 | 32 | return ( 33 | 34 | 43 | {children} 44 | 45 | 46 | ); 47 | }; 48 | 49 | export default LinkButton; 50 | -------------------------------------------------------------------------------- /components/Header/AppHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | type Props = { 5 | title?: string; 6 | desc?: string; 7 | keywords?: string; 8 | }; 9 | 10 | // "Discover affordable and fashionable men's and women's clothing online at Haru Fashion. Free Returns ✓ 1000+ New Arrivals Dropped Daily." 11 | const defaultDesc = 12 | "Haru Fashion e-commerce developed with Next.JS. Coded with 🖤 by Sat Naing (satnaing.dev)."; 13 | const defaultKeywords = 14 | "Haru Fashion, Online Shop, E-commerce, Sat Naing, NextJS"; 15 | 16 | const AppHeader: React.FC = ({ 17 | title = "Haru Fashion", 18 | desc = defaultDesc, 19 | keywords = defaultKeywords, 20 | }) => { 21 | return ( 22 | 23 | {title} 24 | 25 | 26 | 27 | 28 | 29 | 30 | 35 | 36 | ); 37 | }; 38 | 39 | export default AppHeader; 40 | -------------------------------------------------------------------------------- /context/cart/cart-types.ts: -------------------------------------------------------------------------------- 1 | export const ADD_ITEM = "ADD_ITEM"; 2 | export const ADD_ONE = "ADD_ONE"; 3 | export const REMOVE_ITEM = "REMOVE_ITEM"; 4 | export const DELETE_ITEM = "DELETE_ITEM"; 5 | export const SET_CART = "SET_CART"; 6 | export const CLEAR_CART = "CLEAR_CART"; 7 | 8 | export type commonType = { 9 | id: number; 10 | name: string; 11 | price: number; 12 | qty?: number | undefined; 13 | discountPercent?: number; 14 | description?: string; 15 | detail?: string; 16 | categoryId?: number; 17 | stock?: number; 18 | createdAt?: string; 19 | updatedAt?: string | null; 20 | category?: { 21 | id?: number; 22 | name?: string; 23 | description?: string; 24 | thumbnailImage?: string; 25 | createdAt?: string; 26 | updatedAt?: string | null; 27 | }; 28 | }; 29 | 30 | export interface itemType extends commonType { 31 | img1?: string; 32 | img2?: string; 33 | categoryName?: string; 34 | } 35 | 36 | export interface apiProductsType extends commonType { 37 | image1?: string; 38 | image2?: string; 39 | } 40 | 41 | export type cartFuncType = (item: itemType) => void; 42 | 43 | export type cartType = { 44 | cart: itemType[]; 45 | addItem?: cartFuncType; 46 | addOne?: cartFuncType; 47 | removeItem?: cartFuncType; 48 | deleteItem?: cartFuncType; 49 | clearCart?: () => void; 50 | }; 51 | -------------------------------------------------------------------------------- /components/OverlayContainer/OverlayContainer.module.css: -------------------------------------------------------------------------------- 1 | .imgContainer { 2 | transition: opacity 5s; 3 | @apply relative overflow-hidden flex justify-center items-center; 4 | } 5 | 6 | .imgContainer:hover .imgOverlay { 7 | @apply absolute bg-gray500 opacity-50 h-full w-full top-0; 8 | } 9 | 10 | .img { 11 | transition: transform 0.5s; 12 | } 13 | 14 | .imgContainer:hover .img { 15 | transform: scale(1.1, 1.1); 16 | } 17 | 18 | .overlayBorder { 19 | @apply w-0 h-0 absolute; 20 | right: 10%; 21 | bottom: 10%; 22 | } 23 | /* @apply w-4/5 h-4/5 absolute; */ 24 | 25 | .imgContainer:hover .overlayBorder { 26 | transition: all 0.3s; 27 | transition-delay: 0s, 0.3s; 28 | transition-property: width, height; 29 | width: 80%; 30 | height: 80%; 31 | border-left: 1px solid white; 32 | /* border-right: 1px solid white; */ 33 | border-bottom: 1px solid white; 34 | /* border-top: 1px solid white; */ 35 | } 36 | 37 | .overlayBorder2 { 38 | @apply w-0 h-0 absolute; 39 | left: 10%; 40 | top: 10%; 41 | } 42 | 43 | .imgContainer:hover .overlayBorder2 { 44 | transition: all 0.3s; 45 | transition-delay: 0.6s, 0.9s; 46 | transition-property: width, height; 47 | width: 80%; 48 | height: 80%; 49 | /* border-left: 1px solid white; */ 50 | border-right: 1px solid white; 51 | /* border-bottom: 1px solid white; */ 52 | border-top: 1px solid white; 53 | } 54 | -------------------------------------------------------------------------------- /public/icons/InstagramLogo.tsx: -------------------------------------------------------------------------------- 1 | const IgLogo = ({ extraClass = "" }) => ( 2 | 18 | ); 19 | 20 | export default IgLogo; 21 | -------------------------------------------------------------------------------- /pages/coming-soon.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import AppHeader from "../components/Header/AppHeader"; 6 | import { GetStaticProps } from "next"; 7 | 8 | const ComingSoon = () => { 9 | const t = useTranslations("Others"); 10 | return ( 11 | <> 12 | 13 |
14 |

15 | {t("coming_soon")} 16 |

17 |

18 | {t("page_not_created_msg")} 19 |

20 | Not created yet 26 | 27 | {t("go_back_to")}{" "} 28 | 29 | home page 30 | 31 | ? 32 | 33 |
34 | 35 | ); 36 | }; 37 | 38 | export const getStaticProps: GetStaticProps = async ({ locale }) => { 39 | return { 40 | props: { 41 | messages: (await import(`../messages/common/${locale}.json`)).default, 42 | }, 43 | }; 44 | }; 45 | 46 | export default ComingSoon; 47 | -------------------------------------------------------------------------------- /components/OverlayContainer/OverlayContainer.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { FC } from "react"; 3 | import styles from "./OverlayContainer.module.css"; 4 | 5 | type Props = { 6 | imgSrc: string; 7 | imgSrc2?: string; 8 | imgAlt?: string; 9 | }; 10 | 11 | const OverlayContainer: FC = ({ imgSrc, imgSrc2, imgAlt, children }) => ( 12 |
13 | {imgSrc2 ? ( 14 | <> 15 |
16 | {imgAlt} 24 |
25 |
26 | {imgAlt} 34 |
35 | 36 | ) : ( 37 | {imgAlt} 44 | )} 45 | 46 | {children} 47 |
48 |
49 |
50 |
51 | ); 52 | 53 | export default OverlayContainer; 54 | -------------------------------------------------------------------------------- /context/cart/cartReducer.ts: -------------------------------------------------------------------------------- 1 | import addItemToCart from "../Util/addItemToCart"; 2 | import { 3 | ADD_ITEM, 4 | ADD_ONE, 5 | REMOVE_ITEM, 6 | DELETE_ITEM, 7 | cartType, 8 | itemType, 9 | CLEAR_CART, 10 | SET_CART, 11 | } from "./cart-types"; 12 | import removeItemFromCart from "../Util/removeItemFromCart"; 13 | 14 | type actionType = { 15 | type: string; 16 | payload?: itemType | itemType[]; 17 | }; 18 | 19 | const cartReducer = (state: cartType, action: actionType) => { 20 | switch (action.type) { 21 | case ADD_ITEM: 22 | return { 23 | ...state, 24 | cart: addItemToCart(state.cart, action.payload as itemType), 25 | }; 26 | case ADD_ONE: 27 | return { 28 | ...state, 29 | cart: addItemToCart(state.cart, action.payload as itemType, true), 30 | }; 31 | case REMOVE_ITEM: 32 | return { 33 | ...state, 34 | cart: removeItemFromCart(state.cart, action.payload as itemType), 35 | }; 36 | case DELETE_ITEM: 37 | return { 38 | ...state, 39 | cart: state.cart.filter( 40 | (cartItem) => cartItem.id !== (action.payload as itemType).id 41 | ), 42 | }; 43 | case SET_CART: 44 | return { 45 | ...state, 46 | cart: action.payload as itemType[], 47 | }; 48 | case CLEAR_CART: 49 | return { 50 | ...state, 51 | cart: [], 52 | }; 53 | default: 54 | return state; 55 | } 56 | }; 57 | 58 | export default cartReducer; 59 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { NextComponentType, NextPageContext } from "next"; 2 | import Router from "next/router"; 3 | import NProgress from "nprogress"; 4 | import { NextIntlProvider } from "next-intl"; 5 | 6 | import { ProvideCart } from "../context/cart/CartProvider"; 7 | import { ProvideWishlist } from "../context/wishlist/WishlistProvider"; 8 | import { ProvideAuth } from "../context/AuthContext"; 9 | 10 | import "../styles/globals.css"; 11 | import "animate.css"; 12 | import "nprogress/nprogress.css"; 13 | 14 | // Import Swiper styles 15 | import "swiper/swiper.min.css"; 16 | import "swiper/components/navigation/navigation.min.css"; 17 | import "swiper/components/pagination/pagination.min.css"; 18 | import "swiper/components/scrollbar/scrollbar.min.css"; 19 | 20 | Router.events.on("routeChangeStart", () => NProgress.start()); 21 | Router.events.on("routeChangeComplete", () => NProgress.done()); 22 | Router.events.on("routeChangeError", () => NProgress.done()); 23 | 24 | type AppCustomProps = { 25 | Component: NextComponentType; 26 | pageProps: any; 27 | cartState: string; 28 | wishlistState: string; 29 | }; 30 | 31 | const MyApp = ({ Component, pageProps }: AppCustomProps) => { 32 | return ( 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default MyApp; 46 | -------------------------------------------------------------------------------- /public/site-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ["./pages/**/*.{js,ts,jsx,tsx}", "./components/**/*.{js,ts,jsx,tsx}"], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | screens: { 6 | sm: "576px", 7 | md: "768px", 8 | lg: "992px", 9 | xl: "1280px", 10 | }, 11 | fontFamily: { 12 | sans: [ 13 | "Jost", 14 | "ui-sans-serif", 15 | "system-ui", 16 | "-apple-system", 17 | "BlinkMacSystemFont", 18 | '"Segoe UI"', 19 | "Roboto", 20 | '"Helvetica Neue"', 21 | "Arial", 22 | '"Noto Sans"', 23 | "sans-serif", 24 | '"Apple Color Emoji"', 25 | '"Segoe UI Emoji"', 26 | '"Segoe UI Symbol"', 27 | '"Noto Color Emoji"', 28 | ], 29 | }, 30 | colors: { 31 | transparent: "transparent", 32 | current: "currentColor", 33 | 34 | white: "#FFFFFF", 35 | 36 | gray100: "#EEEEEE", 37 | gray200: "#ECECEC", 38 | gray300: "#C1C1C1", 39 | gray400: "#686868", 40 | gray500: "#282828", 41 | 42 | red: "#F05454", 43 | yellow: "#F5B461", 44 | green: "#9BDEAC", 45 | blue: "#66BFBF", 46 | lightgreen: "#F2FDFB", 47 | }, 48 | extend: {}, 49 | }, 50 | variants: { 51 | extend: { 52 | transform: ["group-hover"], 53 | scale: ["group-hover"], 54 | transitionDuration: ["group-hover"], 55 | letterSpacing: ["group-hover"], 56 | width: ["group-hover"], 57 | borderColor: ["group-hover"], 58 | }, 59 | // divideColor: ['group-hover'], 60 | }, 61 | plugins: [], 62 | }; 63 | -------------------------------------------------------------------------------- /components/CartItem/Item.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React, { FC, useContext } from "react"; 3 | import { roundDecimal } from "../Util/utilFunc"; 4 | 5 | type Props = { 6 | img: string; 7 | name: string; 8 | price: number; 9 | qty: number; 10 | onAdd?: () => void; 11 | onRemove?: () => void; 12 | onDelete?: () => void; 13 | }; 14 | 15 | const Item: FC = ({ 16 | img, 17 | name, 18 | price, 19 | qty, 20 | onAdd, 21 | onRemove, 22 | onDelete, 23 | }) => { 24 | return ( 25 |
26 | {name} 27 |
28 | {name} 29 |
30 |
34 | - 35 |
36 |
37 | {qty} 38 |
39 |
43 | + 44 |
45 |
46 |
47 |
48 | 55 | $ {roundDecimal(price)} 56 |
57 |
58 | ); 59 | }; 60 | 61 | export default Item; 62 | -------------------------------------------------------------------------------- /context/wishlist/WishlistProvider.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useReducer } from "react"; 2 | import { getCookie, setCookies } from "cookies-next"; 3 | 4 | import wishlistReducer from "./wishlistReducer"; 5 | import WishlistContext from "./WishlistContext"; 6 | import { 7 | ADD_TO_WISHLIST, 8 | DELETE_WISHLIST_ITEM, 9 | CLEAR_WISHLIST, 10 | itemType, 11 | wishlistType, 12 | SET_WISHLIST, 13 | } from "./wishlist-type"; 14 | 15 | export const ProvideWishlist = ({ 16 | children, 17 | }: { 18 | children: React.ReactNode; 19 | }) => { 20 | const value = useProvideWishlist(); 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }; 27 | 28 | export const useWishlist = () => useContext(WishlistContext); 29 | 30 | const useProvideWishlist = () => { 31 | const initPersistState: wishlistType = { wishlist: [] }; 32 | const [state, dispatch] = useReducer(wishlistReducer, initPersistState); 33 | 34 | useEffect(() => { 35 | const initialWishlist = getCookie("wishlist"); 36 | if (initialWishlist) { 37 | const wishlistItems = JSON.parse(initialWishlist as string); 38 | dispatch({ type: SET_WISHLIST, payload: wishlistItems }); 39 | } 40 | }, []); 41 | 42 | useEffect(() => { 43 | setCookies("wishlist", state.wishlist); 44 | }, [state.wishlist]); 45 | 46 | const addToWishlist = (item: itemType) => { 47 | dispatch({ 48 | type: ADD_TO_WISHLIST, 49 | payload: item, 50 | }); 51 | }; 52 | 53 | const deleteWishlistItem = (item: itemType) => { 54 | dispatch({ 55 | type: DELETE_WISHLIST_ITEM, 56 | payload: item, 57 | }); 58 | }; 59 | 60 | const clearWishlist = () => { 61 | dispatch({ 62 | type: CLEAR_WISHLIST, 63 | }); 64 | }; 65 | 66 | const value: wishlistType = { 67 | wishlist: state.wishlist, 68 | addToWishlist, 69 | deleteWishlistItem, 70 | clearWishlist, 71 | }; 72 | 73 | return value; 74 | }; 75 | -------------------------------------------------------------------------------- /context/cart/CartProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useReducer } from "react"; 2 | import cartReducer from "./cartReducer"; 3 | import CartContext from "./CartContext"; 4 | import { getCookie, setCookies } from "cookies-next"; 5 | import { 6 | ADD_ITEM, 7 | ADD_ONE, 8 | REMOVE_ITEM, 9 | DELETE_ITEM, 10 | itemType, 11 | cartType, 12 | CLEAR_CART, 13 | SET_CART, 14 | } from "./cart-types"; 15 | 16 | export const ProvideCart = ({ children }: { children: React.ReactNode }) => { 17 | const value = useProvideCart(); 18 | return {children}; 19 | }; 20 | 21 | export const useCart = () => useContext(CartContext); 22 | 23 | const useProvideCart = () => { 24 | const initPersistState: cartType = { cart: [] }; 25 | const [state, dispatch] = useReducer(cartReducer, initPersistState); 26 | 27 | useEffect(() => { 28 | const initialCart = getCookie("cart"); 29 | if (initialCart) { 30 | const cartItems = JSON.parse(initialCart as string); 31 | dispatch({ type: SET_CART, payload: cartItems }); 32 | } 33 | }, []); 34 | 35 | useEffect(() => { 36 | setCookies("cart", state.cart); 37 | }, [state.cart]); 38 | 39 | const addItem = (item: itemType) => { 40 | dispatch({ 41 | type: ADD_ITEM, 42 | payload: item, 43 | }); 44 | }; 45 | 46 | const addOne = (item: itemType) => { 47 | dispatch({ 48 | type: ADD_ONE, 49 | payload: item, 50 | }); 51 | }; 52 | 53 | const removeItem = (item: itemType) => { 54 | dispatch({ 55 | type: REMOVE_ITEM, 56 | payload: item, 57 | }); 58 | }; 59 | 60 | const deleteItem = (item: itemType) => { 61 | dispatch({ 62 | type: DELETE_ITEM, 63 | payload: item, 64 | }); 65 | }; 66 | 67 | const clearCart = () => { 68 | dispatch({ 69 | type: CLEAR_CART, 70 | }); 71 | }; 72 | 73 | const value: cartType = { 74 | cart: state.cart, 75 | addItem, 76 | addOne, 77 | removeItem, 78 | deleteItem, 79 | clearCart, 80 | }; 81 | 82 | return value; 83 | }; 84 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/Auth/ForgotPassword.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Dialog } from "@headlessui/react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import { useAuth } from "../../context/AuthContext"; 6 | import Button from "../Buttons/Button"; 7 | import Input from "../Input/Input"; 8 | 9 | type Props = { 10 | onLogin: () => void; 11 | errorMsg: string; 12 | setErrorMsg: React.Dispatch>; 13 | setSuccessMsg: React.Dispatch>; 14 | }; 15 | 16 | const ForgotPassword: React.FC = ({ 17 | onLogin, 18 | errorMsg, 19 | setErrorMsg, 20 | setSuccessMsg, 21 | }) => { 22 | const auth = useAuth(); 23 | const [email, setEmail] = useState(""); 24 | const t = useTranslations("LoginRegister"); 25 | 26 | const handleSubmit = async (e: React.FormEvent) => { 27 | e.preventDefault(); 28 | const forgotPasswordResponse = await auth.forgotPassword!(email); 29 | console.log(forgotPasswordResponse); 30 | if (forgotPasswordResponse.success) { 31 | setSuccessMsg("login_successful"); 32 | } else { 33 | setErrorMsg("incorrect_email_password"); 34 | } 35 | }; 36 | 37 | return ( 38 | <> 39 | 43 | {t("forgot_password")} 44 | 45 |
46 | setEmail((e.target as HTMLInputElement).value)} 54 | value={email} 55 | /> 56 | {errorMsg !== "" && ( 57 |
58 | {t(errorMsg)} 59 |
60 | )} 61 |
75 | 82 | 83 | 84 |
85 | 86 | {name} 87 | 88 |
$ {price}
89 | 96 |
97 | 98 | ); 99 | }; 100 | 101 | export default Card; 102 | -------------------------------------------------------------------------------- /pages/search.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | import { GetServerSideProps, GetStaticPaths, GetStaticProps } from "next"; 3 | import { useRouter } from "next/router"; 4 | import Link from "next/link"; 5 | import { useTranslations } from "next-intl"; 6 | 7 | import Header from "../components/Header/Header"; 8 | import Footer from "../components/Footer/Footer"; 9 | import Card from "../components/Card/Card"; 10 | import Pagination from "../components/Util/Pagination"; 11 | import useWindowSize from "../components/Util/useWindowSize"; 12 | import { apiProductsType, itemType } from "../context/cart/cart-types"; 13 | import axios from "axios"; 14 | 15 | type Props = { 16 | items: itemType[]; 17 | searchWord: string; 18 | }; 19 | 20 | const Search: React.FC = ({ items, searchWord }) => { 21 | const t = useTranslations("Search"); 22 | 23 | return ( 24 |
25 | {/* ===== Head Section ===== */} 26 |
27 | 28 |
29 | {/* ===== Breadcrumb Section ===== */} 30 |
31 |
32 |
33 | 34 | {t("home")} 35 | {" "} 36 | / {t("search_results")} 37 |
38 |
39 |
40 | 41 | {/* ===== Heading & Filter Section ===== */} 42 |
43 |

44 | {t("search_results")}: "{searchWord}" 45 |

46 | {items.length > 0 && ( 47 |
48 | 49 | {t("showing_results", { 50 | products: items.length, 51 | })} 52 | 53 |
54 | )} 55 |
56 | 57 | {/* ===== Main Content Section ===== */} 58 |
59 | {items.length < 1 ? ( 60 |
61 | {t("no_result")} 62 |
63 | ) : ( 64 |
65 | {items.map((item) => ( 66 | 67 | ))} 68 |
69 | )} 70 |
71 |
72 | 73 | {/* ===== Footer Section ===== */} 74 |
75 |
76 | ); 77 | }; 78 | 79 | export const getServerSideProps: GetServerSideProps = async ({ 80 | locale, 81 | query: { q = "" }, 82 | }) => { 83 | const res = await axios.get( 84 | `${process.env.NEXT_PUBLIC_PROD_BACKEND_URL}/api/v1/products/search?q=${q}` 85 | ); 86 | const fetchedProducts: apiProductsType[] = res.data.data.map( 87 | (product: apiProductsType) => ({ 88 | ...product, 89 | img1: product.image1, 90 | img2: product.image2, 91 | }) 92 | ); 93 | 94 | let items: apiProductsType[] = []; 95 | fetchedProducts.forEach((product: apiProductsType) => { 96 | items.push(product); 97 | }); 98 | 99 | return { 100 | props: { 101 | messages: (await import(`../messages/common/${locale}.json`)).default, 102 | items, 103 | searchWord: q, 104 | }, 105 | }; 106 | }; 107 | 108 | export default Search; 109 | -------------------------------------------------------------------------------- /components/Auth/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Dialog } from "@headlessui/react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import { useAuth } from "../../context/AuthContext"; 6 | import Button from "../Buttons/Button"; 7 | import Input from "../Input/Input"; 8 | 9 | type Props = { 10 | onRegister: () => void; 11 | onForgotPassword: () => void; 12 | errorMsg: string; 13 | setErrorMsg: React.Dispatch>; 14 | setSuccessMsg: React.Dispatch>; 15 | }; 16 | 17 | const Login: React.FC = ({ 18 | onRegister, 19 | onForgotPassword, 20 | errorMsg, 21 | setErrorMsg, 22 | setSuccessMsg, 23 | }) => { 24 | const auth = useAuth(); 25 | const [email, setEmail] = useState(""); 26 | const [password, setPassword] = useState(""); 27 | const t = useTranslations("LoginRegister"); 28 | 29 | const handleSubmit = async (e: React.FormEvent) => { 30 | e.preventDefault(); 31 | const loginResponse = await auth.login!(email, password); 32 | if (loginResponse.success) { 33 | setSuccessMsg("login_successful"); 34 | } else { 35 | setErrorMsg("incorrect_email_password"); 36 | } 37 | }; 38 | 39 | return ( 40 | <> 41 | 45 | {t("login")} 46 | 47 |
48 | setEmail((e.target as HTMLInputElement).value)} 56 | value={email} 57 | /> 58 | setPassword((e.target as HTMLInputElement).value)} 66 | value={password} 67 | /> 68 | {errorMsg !== "" && ( 69 |
70 | {t(errorMsg)} 71 |
72 | )} 73 |
74 |
75 | 81 | 84 |
85 | 89 | {t("forgot_password")} 90 | 91 |
92 |
78 | 79 | {(midPageNumbers || endPageNumbers) && ( 80 |
  • 81 | ... 82 |
  • 83 | )} 84 | {pageNumbers.map((num) => { 85 | return ( 86 |
  • 87 | 100 |
  • 101 | ); 102 | })} 103 | {(midPageNumbers || startPageNumbers) && ( 104 |
  • 105 | ... 106 |
  • 107 | )} 108 |
  • 109 | 127 |
  • 128 | 129 | 130 | ); 131 | }; 132 | 133 | export default Pagination; 134 | -------------------------------------------------------------------------------- /components/Auth/Register.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, useState } from "react"; 2 | import { Dialog } from "@headlessui/react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import Button from "../Buttons/Button"; 6 | import Input from "../Input/Input"; 7 | import { useAuth } from "../../context/AuthContext"; 8 | 9 | type Props = { 10 | onLogin: () => void; 11 | errorMsg: string; 12 | setErrorMsg: React.Dispatch>; 13 | setSuccessMsg: React.Dispatch>; 14 | }; 15 | 16 | const Register: React.FC = ({ 17 | onLogin, 18 | errorMsg, 19 | setErrorMsg, 20 | setSuccessMsg, 21 | }) => { 22 | const auth = useAuth(); 23 | const [name, setName] = useState(""); 24 | const [email, setEmail] = useState(""); 25 | const [password, setPassword] = useState(""); 26 | const [address, setAddress] = useState(""); 27 | const [phone, setPhone] = useState(""); 28 | const t = useTranslations("LoginRegister"); 29 | 30 | const handleSubmit = async (e: React.FormEvent) => { 31 | e.preventDefault(); 32 | const regResponse = await auth.register!( 33 | email, 34 | name, 35 | password, 36 | address, 37 | phone 38 | ); 39 | if (regResponse.success) { 40 | setSuccessMsg("register_successful"); 41 | } else { 42 | if (regResponse.message === "alreadyExists") { 43 | setErrorMsg("email_already_exists"); 44 | } else { 45 | setErrorMsg("error_occurs"); 46 | } 47 | } 48 | }; 49 | 50 | auth.user ? console.log(auth.user) : console.log("No User"); 51 | 52 | return ( 53 | <> 54 | 58 | {t("register")} 59 | 60 |
    61 | setName((e.target as HTMLInputElement).value)} 69 | value={name} 70 | /> 71 | setEmail((e.target as HTMLInputElement).value)} 79 | value={email} 80 | /> 81 | setPassword((e.target as HTMLInputElement).value)} 89 | value={password} 90 | /> 91 | setAddress((e.target as HTMLInputElement).value)} 98 | value={address} 99 | /> 100 | setPhone((e.target as HTMLInputElement).value)} 107 | value={phone} 108 | /> 109 | {errorMsg !== "" && ( 110 |
    111 | {t(errorMsg)} 112 |
    113 | )} 114 |
    115 |

    {t("register_desc")}

    116 |
    117 |
    164 | {/* */} 165 | 166 | 167 |
  • 168 | 169 |
  • 170 | 171 | 172 | 173 | 174 | 175 | ); 176 | }; 177 | 178 | export default Header; 179 | -------------------------------------------------------------------------------- /components/CartItem/CartItem.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import { Fragment, useCallback, useEffect, useState } from "react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import BagIcon from "../../public/icons/BagIcon"; 6 | import Button from "../Buttons/Button"; 7 | import Item from "./Item"; 8 | import LinkButton from "../Buttons/LinkButton"; 9 | import { roundDecimal } from "../Util/utilFunc"; 10 | import { useCart } from "../../context/cart/CartProvider"; 11 | import { useRouter } from "next/router"; 12 | 13 | export default function CartItem() { 14 | const router = useRouter(); 15 | const t = useTranslations("CartWishlist"); 16 | const [open, setOpen] = useState(false); 17 | const [animate, setAnimate] = useState(""); 18 | const { cart, addOne, removeItem, deleteItem } = useCart(); 19 | 20 | let subtotal = 0; 21 | 22 | let noOfItems = 0; 23 | cart.forEach((item) => { 24 | noOfItems += item.qty!; 25 | }); 26 | 27 | const handleAnimate = useCallback(() => { 28 | if (noOfItems === 0) return; 29 | setAnimate("animate__animated animate__headShake"); 30 | // setTimeout(() => { 31 | // setAnimate(""); 32 | // }, 0.1); 33 | }, [noOfItems, setAnimate]); 34 | 35 | // Set animate when no of items changes 36 | useEffect(() => { 37 | handleAnimate(); 38 | setTimeout(() => { 39 | setAnimate(""); 40 | }, 1000); 41 | }, [handleAnimate]); 42 | 43 | function closeModal() { 44 | setOpen(false); 45 | } 46 | 47 | function openModal() { 48 | setOpen(true); 49 | } 50 | 51 | return ( 52 | <> 53 |
    54 | 64 |
    65 | 66 | 74 |
    75 | 84 | 85 | 86 | 87 | {/* This element is to trick the browser into centering the modal contents. */} 88 | {/* */} 94 | 103 |
    107 |
    108 |

    109 | {t("cart")} ({noOfItems}) 110 |

    111 | 118 |
    119 | 120 |
    121 |
    122 | {cart.map((item) => { 123 | subtotal += item.price * item.qty!; 124 | return ( 125 | addOne!(item)} 132 | onRemove={() => removeItem!(item)} 133 | onDelete={() => deleteItem!(item)} 134 | /> 135 | ); 136 | })} 137 |
    138 |
    139 |
    140 | {t("subtotal")} 141 | $ {roundDecimal(subtotal)} 142 |
    143 | 149 | {t("view_cart")} 150 | 151 |
    159 |
    160 |
    161 |
    162 |
    163 |
    164 |
    165 | 166 | ); 167 | } 168 | -------------------------------------------------------------------------------- /pages/wishlist.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import { GetStaticProps } from "next"; 4 | import { useTranslations } from "next-intl"; 5 | 6 | import Header from "../components/Header/Header"; 7 | import Footer from "../components/Footer/Footer"; 8 | import LeftArrow from "../public/icons/LeftArrow"; 9 | import Button from "../components/Buttons/Button"; 10 | import GhostButton from "../components/Buttons/GhostButton"; 11 | import { useCart } from "../context/cart/CartProvider"; 12 | import { useWishlist } from "../context/wishlist/WishlistProvider"; 13 | 14 | // let w = window.innerWidth; 15 | 16 | const Wishlist = () => { 17 | const t = useTranslations("CartWishlist"); 18 | const { addOne } = useCart(); 19 | const { wishlist, deleteWishlistItem, clearWishlist } = useWishlist(); 20 | 21 | let subtotal = 0; 22 | 23 | return ( 24 |
    25 | {/* ===== Head Section ===== */} 26 |
    27 | 28 |
    29 | {/* ===== Heading & Continue Shopping */} 30 |
    31 |

    32 | {t("wishlist")} 33 |

    34 | 42 |
    43 | 44 | {/* ===== Wishlist Table Section ===== */} 45 |
    46 |
    47 | 48 | 49 | 50 | 53 | 56 | 59 | 66 | 69 | 72 | 75 | 76 | 77 | 78 | {wishlist.length === 0 ? ( 79 | 80 | 81 | 82 | ) : ( 83 | wishlist.map((item) => { 84 | subtotal += item.price * item.qty!; 85 | return ( 86 | 87 | 103 | 106 | 109 | 116 | 133 | 134 | ); 135 | }) 136 | )} 137 | 138 |
    51 | {t("product_image")} 52 | 54 | {t("product_name")} 55 | 57 | {t("product_details")} 58 | 64 | {t("unit_price")} 65 | 67 | {t("add")} 68 | 70 | {t("remove")} 71 | 73 | {t("actions")} 74 |
    {t("wishlist_is_empty")}
    88 | 91 | 92 | {item.name} 99 | 100 | 101 | {item.name} 102 | 104 | {item.name} 105 | 107 | $ {item.price} 108 | 110 | 120 | 132 |
    139 |
    140 | 144 | {t("clear_wishlist")} 145 | 146 |
    147 |
    148 |
    149 |
    150 | 151 | {/* ===== Footer Section ===== */} 152 |
    153 |
    154 | ); 155 | }; 156 | 157 | export const getStaticProps: GetStaticProps = async ({ locale }) => { 158 | return { 159 | props: { 160 | messages: (await import(`../messages/common/${locale}.json`)).default, 161 | }, 162 | }; 163 | }; 164 | 165 | export default Wishlist; 166 | -------------------------------------------------------------------------------- /components/SearchForm/SearchForm.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect, useState } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import SearchIcon from "../../public/icons/SearchIcon"; 6 | import axios from "axios"; 7 | import { apiProductsType } from "../../context/cart/cart-types"; 8 | import { itemType } from "../../context/wishlist/wishlist-type"; 9 | import Card from "../Card/Card"; 10 | import Loading from "../../public/icons/Loading"; 11 | import GhostButton from "../Buttons/GhostButton"; 12 | import { useRouter } from "next/router"; 13 | 14 | export default function SearchForm() { 15 | const t = useTranslations("Navigation"); 16 | const router = useRouter(); 17 | const [open, setOpen] = useState(false); 18 | const [searchValue, setSearchValue] = useState(""); 19 | const [searchItems, setSearchItems] = useState([]); 20 | const [isFetching, setIsFetching] = useState(false); 21 | const [noResult, setNoResult] = useState(false); 22 | const [moreThanFour, setMoreThanFour] = useState(false); 23 | 24 | function closeModal() { 25 | setOpen(false); 26 | setSearchItems([]); 27 | setNoResult(false); 28 | setMoreThanFour(false); 29 | } 30 | 31 | function openModal() { 32 | setOpen(true); 33 | } 34 | 35 | useEffect(() => { 36 | if (!isFetching) return; 37 | const fetchData = async () => { 38 | const res = await axios.get( 39 | `${process.env.NEXT_PUBLIC_PROD_BACKEND_URL}/api/v1/products/search?q=${searchValue}` 40 | ); 41 | const fetchedProducts: apiProductsType[] = res.data.data.map( 42 | (product: apiProductsType) => ({ 43 | ...product, 44 | img1: product.image1, 45 | img2: product.image2, 46 | }) 47 | ); 48 | if (fetchedProducts.length < 1) setNoResult(true); 49 | fetchedProducts.map((product, index) => { 50 | if (index < 4) { 51 | setSearchItems((prevProduct) => [...prevProduct, product]); 52 | } else { 53 | setMoreThanFour(true); 54 | } 55 | }); 56 | setIsFetching(false); 57 | }; 58 | fetchData(); 59 | }, [isFetching, searchValue]); 60 | 61 | const handleSubmit = (e: React.FormEvent) => { 62 | e.preventDefault(); 63 | setSearchItems([]); 64 | setIsFetching(true); 65 | }; 66 | 67 | const handleChange = (e: React.FormEvent) => { 68 | setSearchValue((e.target as HTMLInputElement).value); 69 | setSearchItems([]); 70 | setNoResult(false); 71 | setMoreThanFour(false); 72 | }; 73 | 74 | return ( 75 | <> 76 |
    77 | 80 |
    81 | 82 | 90 |
    91 | 100 | 101 | 102 | 103 | {/* This element is to trick the browser into centering the modal contents. */} 104 | {/* */} 110 | 119 |
    120 |
    121 |
    122 |
    123 | 130 |
    131 |
    135 | {isFetching ? ( 136 | 137 | ) : ( 138 | 139 | )} 140 | 146 | 147 |
    148 |
    149 | {noResult ? ( 150 |
    151 | {t("no_result")} 152 |
    153 | ) : ( 154 |
    155 |
    162 | {searchItems.map((item) => ( 163 | 164 | ))} 165 |
    166 | {moreThanFour && ( 167 | 169 | router.push(`/search?q=${searchValue}`) 170 | } 171 | > 172 | {t("view_all")} 173 | 174 | )} 175 |
    176 | )} 177 |
    178 |
    179 |
    180 |
    181 |
    182 |
    183 | 184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /components/Auth/AuthForm.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment, useState, FC } from "react"; 2 | import { Dialog, Transition } from "@headlessui/react"; 3 | import { useTranslations } from "next-intl"; 4 | 5 | import { useAuth } from "../../context/AuthContext"; 6 | import Button from "../Buttons/Button"; 7 | import Login from "./Login"; 8 | import Register from "./Register"; 9 | import ForgotPassword from "./ForgotPassword"; 10 | 11 | type CurrentPage = "login" | "register" | "forgot-password"; 12 | 13 | type Props = { 14 | extraClass?: string; 15 | children: any; 16 | }; 17 | 18 | const LoginForm: FC = ({ extraClass, children }) => { 19 | const auth = useAuth(); 20 | const [currentPage, setCurrentPage] = useState("login"); 21 | const [open, setOpen] = useState(false); 22 | const [errorMsg, setErrorMsg] = useState(""); 23 | const [successMsg, setSuccessMsg] = useState(""); 24 | const t = useTranslations("LoginRegister"); 25 | 26 | let modalBox: JSX.Element; 27 | if (auth.user) { 28 | modalBox = ( 29 | 30 | ); 31 | } else { 32 | if (currentPage === "login") { 33 | modalBox = ( 34 | setCurrentPage("register")} 36 | onForgotPassword={() => setCurrentPage("forgot-password")} 37 | errorMsg={errorMsg} 38 | setErrorMsg={setErrorMsg} 39 | setSuccessMsg={setSuccessMsg} 40 | /> 41 | ); 42 | } else if (currentPage === "register") { 43 | modalBox = ( 44 | setCurrentPage("login")} 46 | errorMsg={errorMsg} 47 | setErrorMsg={setErrorMsg} 48 | setSuccessMsg={setSuccessMsg} 49 | /> 50 | ); 51 | } else { 52 | modalBox = ( 53 | setCurrentPage("login")} 55 | errorMsg={errorMsg} 56 | setErrorMsg={setErrorMsg} 57 | setSuccessMsg={setSuccessMsg} 58 | /> 59 | ); 60 | } 61 | } 62 | 63 | function closeModal() { 64 | setOpen(false); 65 | setErrorMsg(""); 66 | setTimeout(() => { 67 | setSuccessMsg("profile"); 68 | }, 100); 69 | } 70 | 71 | function openModal() { 72 | setOpen(true); 73 | } 74 | 75 | return ( 76 | <> 77 |
    78 | 86 |
    87 | 88 | 96 |
    97 | 106 | 107 | 108 | 109 | {/* This element is to trick the browser into centering the modal contents. */} 110 | 116 | 125 |
    126 | 133 | {modalBox} 134 | {/* {auth.user ? ( 135 | 139 | ) : (if (currentPage === "login") {( 140 | setCurrentPage("login")} 142 | errorMsg={errorMsg} 143 | setErrorMsg={setErrorMsg} 144 | setSuccessMsg={setSuccessMsg} 145 | /> 146 | )} else if (currentPage === "register") {( 147 | setCurrentPage("register")} 149 | errorMsg={errorMsg} 150 | setErrorMsg={setErrorMsg} 151 | setSuccessMsg={setSuccessMsg} 152 | /> 153 | )} else {( 154 | setCurrentPage("login")} 155 | errorMsg={errorMsg} 156 | setErrorMsg={setErrorMsg} 157 | setSuccessMsg={setSuccessMsg} /> 158 | )})} */} 159 |
    160 |
    161 |
    162 |
    163 |
    164 | 165 | ); 166 | }; 167 | 168 | const SuccessModal = ({ 169 | successMsg, 170 | setSuccessMsg, 171 | }: { 172 | successMsg: string; 173 | setSuccessMsg: React.Dispatch>; 174 | }) => { 175 | const t = useTranslations("LoginRegister"); 176 | const auth = useAuth(); 177 | 178 | const handleLogout = () => { 179 | auth.logout!(); 180 | setSuccessMsg(""); 181 | }; 182 | return ( 183 | <> 184 | 188 | {/* {t("login_successful")} */} 189 | {/* {t("register_successful")} */} 190 | {successMsg !== "" ? t(successMsg) : t("profile")} 191 | 192 |
    193 |
    194 | {t("name")} - {auth.user?.fullname} 195 |
    196 |
    197 | {t("email_address")} - {auth.user?.email} 198 |
    199 |
    200 | {t("phone")} - {auth.user?.phone && auth.user?.phone} 201 |
    202 |
    203 | {t("shipping_address")} -{" "} 204 | {auth.user?.shippingAddress && auth.user?.shippingAddress} 205 |
    206 |
    207 |
    208 |
    210 | 211 | ); 212 | }; 213 | 214 | export default LoginForm; 215 | -------------------------------------------------------------------------------- /messages/common/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "Navigation": { 3 | "skip_to_main_content": "Skip to main content", 4 | "eng": "Eng", 5 | "myn": "Myn", 6 | "english": "English", 7 | "myanmar": "Myanmar", 8 | "usd": "USD", 9 | "mmk": "MMK", 10 | "men": "Men", 11 | "women": "Women", 12 | "bags": "Bags", 13 | "blogs": "Blogs", 14 | "login": "Login", 15 | "profile": "Profile", 16 | "wishlist": "Wishlist", 17 | "send": "Send", 18 | "careers": "Careers", 19 | "company": "Company", 20 | "help": "Help", 21 | "store": "Store", 22 | "keep_in_touch": "Keep In Touch", 23 | "about_us": "About Us", 24 | "contact_us": "Contact Us", 25 | "our_policy": "Our Policy", 26 | "store_location": "Store Location", 27 | "order_tracking": "Order Tracking", 28 | "faqs": "FAQs", 29 | "privacy_policy": "Privacy Policy", 30 | "terms_conditions": "Terms & Conditions", 31 | "search_anything": "Search anything ...", 32 | "address_full": "No(7), Ground Floor, Malikha Building, Yadanar Road, Thingangyun, Yangon", 33 | "address": { 34 | "detail": "No(7), Ground Floor,", 35 | "road": "Malikha Building, Yadanar Road,", 36 | "city": "Thingangyun, Yangon" 37 | }, 38 | "phone_number": "+95 95 096 051", 39 | "open_all_days": "Open All Days", 40 | "opening_hours": "9:00 AM ~ 11:00 PM", 41 | "newsletter": "Newsletter", 42 | "newsletter_desc": "Be the first to know about new arrivals, sales & promos!", 43 | "all_rights_reserved": "All rights reserved.", 44 | "follow_us_on_social_media": "Follow us on Social Media", 45 | "view_all": "View All", 46 | "no_result": "No Result", 47 | "sort_by_latest": "Sort by latest", 48 | "sort_by_price": "Sort by price: low to high", 49 | "sort_by_price_desc": "Sort by price: high to low" 50 | }, 51 | "CartWishlist": { 52 | "wishlist": "Wishlist", 53 | "continue_shopping": "Continue Shopping", 54 | "product": "Product", 55 | "product_image": "Product Image", 56 | "product_name": "Product Name", 57 | "product_details": "Product Details", 58 | "unit_price": "Unit Price", 59 | "quantity": "Quantity", 60 | "add": "Add", 61 | "remove": "Remove", 62 | "actions": "Actions", 63 | "wishlist_is_empty": "Wishlist is empty!", 64 | "add_to_cart": "Add To Cart", 65 | "clear_wishlist": "Clear Wishlist", 66 | 67 | "cart": "Cart", 68 | "subtotal": "Subtotal", 69 | "checkout": "Checkout", 70 | "view_cart": "View Cart", 71 | "amount": "Amount", 72 | "delivery": "Delivery", 73 | "free": "Free", 74 | "store_pickup": "Store Pickup", 75 | "within_yangon": "Within Yangon", 76 | "other_cities": "Other Cities", 77 | "grand_total": "Grand Total", 78 | "proceed_to_checkout": "Proceed to Checkout", 79 | "cart_totals": "Cart Totals", 80 | "clear_cart": "Clear Cart", 81 | "cart_is_empty": "Cart is empty!", 82 | "shopping_cart": "Shopping Cart", 83 | 84 | "place_order": "Place Order", 85 | "cash_on_delivery": "Cash on Delivery", 86 | "bank_transfer": "Bank Transfer", 87 | "bank_transfer_desc": "Make your payment directly into our CB, AYA, Kpay.", 88 | "name": "Name", 89 | "password": "Password", 90 | "different_shipping_address": "Send to different shipping address", 91 | "address": "Address", 92 | "shipping_address": "Shipping Address", 93 | "phone": "Phone Number", 94 | "email_already_exists": "Email already exists", 95 | "error_occurs": "An error occurs", 96 | "email_address": "Email Address", 97 | "form_note": "Note! By submitting this form, you will automatically sign up for Haru Fashion Shop", 98 | 99 | "thank_you_note": "Thank you. We receive your order", 100 | "order_id": "Order ID", 101 | "order_date": "Order Date", 102 | "delivery_date": "Delivery Date", 103 | "payment_method": "Payment Method", 104 | "delivery_method": "Delivery Method", 105 | "total": "Total Amount", 106 | "your_order_received": "Your order has been received. ", 107 | "bank_transfer_note": "Please send the bank slip screenshot by sending to our email (satnaingdev@gmail.com) or our Facebook page messenger after you've transferred the amount to our bank account.", 108 | "cash_delivery_note": "Your purchased items will be delivered within a week.", 109 | "store_pickup_note": "You can pickup your purchased item(s) in our store within 7 days.", 110 | "thank_you_for_purchasing": " Thank you for purchasing with us.", 111 | "our_banking_details": "Our Banking Details", 112 | "send_order_email": "Send order detail to my email" 113 | }, 114 | "Others": { 115 | "page_not_found": "Oops! Page Not Found!", 116 | "coming_soon": "Coming Soon!", 117 | "page_not_created_msg": "This page has not been created yet!", 118 | "go_back_to": "Go back to" 119 | }, 120 | "Index": { 121 | "new_arrivals": "New Arrivals", 122 | "women_collection": "Women Collection", 123 | "men_collection": "Men Collection", 124 | "best_selling": "Best Selling", 125 | "best_selling_desc": "Here are some of our best selling products. Explore yourself in the latest trends.", 126 | "add_to_cart": "Add to Cart", 127 | "testimonial": "Testimonial", 128 | "featured_products": "Featured Products", 129 | "see_more": "See More", 130 | "loading": "Loading...", 131 | "shop_now": "Shop Now", 132 | "our_shop": "Our Shop", 133 | "our_shop_desc": "Stop by our stores to learn the stories behind our products, get a personal styling session, or shop the latest in person. See our store locations." 134 | }, 135 | "Category": { 136 | "home": "Home", 137 | "men": "Men", 138 | "women": "Women", 139 | "bags": "Bags", 140 | "new-arrivals": "New Arrivals", 141 | "filter_by": "Filter by", 142 | "sort_by": "Sort by", 143 | "showing_from_to": "Showing {from} ~ {to} of {all}", 144 | "availability": "Availability", 145 | "in_stock": "In Stock", 146 | "size": "Size", 147 | "add_to_cart": "Add To Cart", 148 | "add_to_wishlist": "Add To Wishlist", 149 | "details": "Details", 150 | "share": "Share", 151 | "you_may_also_like": "You may also like" 152 | }, 153 | "LoginRegister": { 154 | "login": "Login", 155 | "logout": "Logout", 156 | "register": "Register", 157 | "submit": "Submit", 158 | "username": "Username", 159 | "name": "Name", 160 | "shipping_address": "Shipping Address", 161 | "phone": "Phone", 162 | "email_address": "Email Address", 163 | "password": "Password", 164 | "remember_me": "Remember me?", 165 | "forgot_password": "Forgot your password?", 166 | "not_member": "Not a member?", 167 | "go_back_to": "Go back to", 168 | "register_desc": "Your personal data will be used to support your experience throughout this website, to manage access to your account, and for other purposes described in our Privacy Policy", 169 | "already_member": "Already a member?", 170 | "email_already_exists": "Email already exists", 171 | "error_occurs": "An error occurs", 172 | "incorrect_email_password": "Incorrect email or password", 173 | "login_successful": "Login Successful", 174 | "register_successful": "Register Successful", 175 | "profile": "Profile" 176 | }, 177 | "Search": { 178 | "home": "Home", 179 | "search_results": "Search results", 180 | "no_result": "No Result", 181 | "showing_results": "Showing {products} result(s)" 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /public/bg-img/coding.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { GetStaticProps } from "next"; 3 | import Image from "next/image"; 4 | import { useTranslations } from "next-intl"; 5 | import axios from "axios"; 6 | 7 | import Header from "../components/Header/Header"; 8 | import Footer from "../components/Footer/Footer"; 9 | import Button from "../components/Buttons/Button"; 10 | import Slideshow from "../components/HeroSection/Slideshow"; 11 | import OverlayContainer from "../components/OverlayContainer/OverlayContainer"; 12 | import Card from "../components/Card/Card"; 13 | import TestiSlider from "../components/TestiSlider/TestiSlider"; 14 | import { apiProductsType, itemType } from "../context/cart/cart-types"; 15 | import LinkButton from "../components/Buttons/LinkButton"; 16 | 17 | // /bg-img/ourshop.png 18 | import ourShop from "../public/bg-img/ourshop.png"; 19 | 20 | type Props = { 21 | products: itemType[]; 22 | }; 23 | 24 | const Home: React.FC = ({ products }) => { 25 | const t = useTranslations("Index"); 26 | const [currentItems, setCurrentItems] = useState(products); 27 | const [isFetching, setIsFetching] = useState(false); 28 | 29 | useEffect(() => { 30 | if (!isFetching) return; 31 | const fetchData = async () => { 32 | const res = await axios.get( 33 | `${process.env.NEXT_PUBLIC_PROD_BACKEND_URL}/api/v1/products?order_by=createdAt.desc&offset=${currentItems.length}&limit=10` 34 | ); 35 | const fetchedProducts = res.data.data.map((product: apiProductsType) => ({ 36 | ...product, 37 | img1: product.image1, 38 | img2: product.image2, 39 | })); 40 | setCurrentItems((products) => [...products, ...fetchedProducts]); 41 | setIsFetching(false); 42 | }; 43 | fetchData(); 44 | }, [isFetching, currentItems.length]); 45 | 46 | const handleSeemore = async ( 47 | e: React.MouseEvent 48 | ) => { 49 | e.preventDefault(); 50 | setIsFetching(true); 51 | }; 52 | 53 | return ( 54 | <> 55 | {/* ===== Header Section ===== */} 56 |
    57 | 58 | {/* ===== Carousel Section ===== */} 59 | 60 | 61 |
    62 | {/* ===== Category Section ===== */} 63 |
    64 |
    65 |
    66 | 71 | 75 | {t("new_arrivals")} 76 | 77 | 78 |
    79 |
    80 | 84 | 88 | {t("women_collection")} 89 | 90 | 91 |
    92 |
    93 | 97 | 101 | {t("men_collection")} 102 | 103 | 104 |
    105 |
    106 |
    107 | 108 | {/* ===== Best Selling Section ===== */} 109 |
    110 |
    111 |
    112 |

    {t("best_selling")}

    113 | {t("best_selling_desc")} 114 |
    115 |
    116 |
    117 | 118 | 119 | 120 | 121 |
    122 |
    123 | 124 | {/* ===== Testimonial Section ===== */} 125 |
    126 |

    {t("testimonial")}

    127 | 128 |
    129 | 130 | {/* ===== Featured Products Section ===== */} 131 |
    132 |
    133 |

    {t("featured_products")}

    134 |
    135 |
    136 | {currentItems.map((item) => ( 137 | 138 | ))} 139 |
    140 |
    141 |
    146 |
    147 | 148 |
    149 | 150 | {/* ===== Our Shop Section */} 151 |
    152 |
    153 |

    {t("our_shop")}

    154 | {t("our_shop_desc")} 155 |
    156 |
    157 | Our Shop 158 |
    159 |
    160 |
    161 | 162 | {/* ===== Footer Section ===== */} 163 |