├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── .prettierrc
├── README.md
├── additional.d.ts
├── components
├── ErrorBoundary
│ └── index.tsx
├── ImageComponent
│ └── index.tsx
├── attribute
│ ├── attribute-delete-view.tsx
│ ├── attribute-form.tsx
│ └── attribute-list.tsx
├── auth
│ ├── change-password-from.tsx
│ ├── forget-password
│ │ ├── enter-email-view.tsx
│ │ ├── enter-new-password-view.tsx
│ │ ├── enter-token-view.tsx
│ │ └── forget-password.tsx
│ ├── login-form.tsx
│ └── profile-update-form.tsx
├── category
│ ├── category-form.tsx
│ ├── category-icons.ts
│ ├── category-list.tsx
│ └── category-validation-schema.ts
├── common
│ ├── access-denied.tsx
│ ├── action-buttons.tsx
│ ├── avatar.tsx
│ ├── card.tsx
│ ├── confirmation-card.tsx
│ ├── notification-menu.tsx
│ ├── search.tsx
│ ├── sort-form.tsx
│ └── uploader.tsx
├── dashboard
│ └── index.tsx
├── hero-carousel
│ ├── hero-banner-card.tsx
│ ├── hero-carousel-list.tsx
│ ├── hero-slide-form.tsx
│ ├── hero-slider-validation-schema.ts
│ └── slider-delete-view.tsx
├── icons
│ ├── add.tsx
│ ├── arrow-down.tsx
│ ├── arrow-next.tsx
│ ├── arrow-prev.tsx
│ ├── arrow-up.tsx
│ ├── ban-user.tsx
│ ├── bell.tsx
│ ├── cart-icon-bag.tsx
│ ├── category
│ │ ├── accessories.tsx
│ │ ├── bath-oil.tsx
│ │ ├── beauty-health.tsx
│ │ ├── bed.tsx
│ │ ├── beverage.tsx
│ │ ├── book-shelf.tsx
│ │ ├── breakfast.tsx
│ │ ├── center-table.tsx
│ │ ├── chair.tsx
│ │ ├── cooking.tsx
│ │ ├── dairy.tsx
│ │ ├── deodorant.tsx
│ │ ├── dressing-table.tsx
│ │ ├── eyes.tsx
│ │ ├── face.tsx
│ │ ├── facial-care.tsx
│ │ ├── fruits-vegetable.tsx
│ │ ├── hand-bag.tsx
│ │ ├── home-cleaning.tsx
│ │ ├── index.tsx
│ │ ├── laptop-bag.tsx
│ │ ├── lips.tsx
│ │ ├── meat-fish.tsx
│ │ ├── oral-care.tsx
│ │ ├── outer-wear.tsx
│ │ ├── pants.tsx
│ │ ├── pet-care.tsx
│ │ ├── purse.tsx
│ │ ├── reading-table.tsx
│ │ ├── relax-chair.tsx
│ │ ├── shaving-needs.tsx
│ │ ├── shirts.tsx
│ │ ├── shoulder-bag.tsx
│ │ ├── skirts.tsx
│ │ ├── snacks.tsx
│ │ ├── sofa.tsx
│ │ ├── storage.tsx
│ │ ├── table.tsx
│ │ ├── tools.tsx
│ │ ├── tops.tsx
│ │ ├── wallet.tsx
│ │ └── women-dress.tsx
│ ├── checkmark-circle-fill.tsx
│ ├── checkmark-circle.tsx
│ ├── checkmark.tsx
│ ├── close-fill.tsx
│ ├── close-icon.tsx
│ ├── coin-icon.tsx
│ ├── copy.tsx
│ ├── coupon-icon.tsx
│ ├── delivery-icon.tsx
│ ├── dot.tsx
│ ├── download-icon.tsx
│ ├── edit copy.tsx
│ ├── edit.tsx
│ ├── expand-less-icon.tsx
│ ├── expand-more-icon.tsx
│ ├── eye-icon.tsx
│ ├── eye-off-icon.tsx
│ ├── ios-arrow-down.tsx
│ ├── ios-arrow-up.tsx
│ ├── map-pin.tsx
│ ├── members-icon.tsx
│ ├── minus.tsx
│ ├── more-icon.tsx
│ ├── navbar-icon.tsx
│ ├── order-icon.tsx
│ ├── phone.tsx
│ ├── product-icon.tsx
│ ├── refund.tsx
│ ├── revenue.tsx
│ ├── save-icon.tsx
│ ├── search-icon.tsx
│ ├── shops
│ │ ├── cube.tsx
│ │ ├── dollar.tsx
│ │ ├── grocery-basket.tsx
│ │ ├── percentage.tsx
│ │ └── price-wallet.tsx
│ ├── sidebar-category-icon.tsx
│ ├── sidebar
│ │ ├── affiliate.tsx
│ │ ├── apps.tsx
│ │ ├── attribute-value.tsx
│ │ ├── attribute.tsx
│ │ ├── categories.tsx
│ │ ├── coupons.tsx
│ │ ├── dashboard.tsx
│ │ ├── image-multiple.tsx
│ │ ├── index.tsx
│ │ ├── my-shop.tsx
│ │ ├── order-status.tsx
│ │ ├── orders.tsx
│ │ ├── products.tsx
│ │ ├── settings.tsx
│ │ ├── shippings.tsx
│ │ ├── shop.tsx
│ │ ├── staffs.tsx
│ │ ├── suppliers.tsx
│ │ ├── support.tsx
│ │ ├── tags.tsx
│ │ ├── taxes.tsx
│ │ ├── types.tsx
│ │ ├── users.tsx
│ │ └── withdraw.tsx
│ ├── site-settings-icon.tsx
│ ├── social
│ │ ├── facebook.tsx
│ │ ├── index.tsx
│ │ ├── instagram.tsx
│ │ ├── twitter.tsx
│ │ └── youtube.tsx
│ ├── trash.tsx
│ ├── type
│ │ ├── bakery-icon.tsx
│ │ ├── book-icon.tsx
│ │ ├── dress-icon.tsx
│ │ ├── facial-care.tsx
│ │ ├── fruits-vegetable.tsx
│ │ ├── furniture-icon.tsx
│ │ ├── handbag-icon.tsx
│ │ ├── index.tsx
│ │ ├── medicine-icon.tsx
│ │ └── restaurant-icon.tsx
│ ├── upload-icon copy.tsx
│ ├── upload-icon.tsx
│ └── user-icon.tsx
├── layouts
│ └── app.tsx
├── navigation
│ ├── index.tsx
│ ├── menu.tsx
│ ├── mobile-navigation.tsx
│ ├── navbar.tsx
│ ├── scss
│ │ └── index.module.scss
│ └── sidebar
│ │ ├── index.tsx
│ │ ├── sidebar-item-mini.tsx
│ │ ├── sidebar-item.tsx
│ │ └── sidebar-mini.tsx
├── order
│ ├── invoice-pdf.tsx
│ ├── order-list.tsx
│ └── recent-orders.tsx
├── product
│ ├── product-category-input.tsx
│ ├── product-delete-view.tsx
│ ├── product-form.tsx
│ ├── product-info-form.tsx
│ ├── product-list.tsx
│ ├── product-validation-schema.ts
│ ├── product-variable-form
│ │ ├── cartesian-product-component.tsx
│ │ ├── index.tsx
│ │ └── variation-images.tsx
│ ├── variablesSubmission.ts
│ └── variations-reducer.ts
├── settings
│ ├── settings-form.tsx
│ └── settings-validation-schema.ts
├── staff
│ ├── staff-ban-view.tsx
│ ├── staff-delete-view.tsx
│ ├── staff-form.tsx
│ ├── staff-list.tsx
│ └── staff-validation-schema.ts
├── ui
│ ├── activeLink.tsx
│ ├── alert.tsx
│ ├── badge
│ │ └── badge.tsx
│ ├── button.tsx
│ ├── chart.tsx
│ ├── checkbox
│ │ ├── checkbox.module.css
│ │ └── index.tsx
│ ├── color-picker
│ │ ├── color-picker.tsx
│ │ └── display-color-code.tsx
│ ├── date-picker.tsx
│ ├── default-seo.tsx
│ ├── description.tsx
│ ├── drawer-wrapper.tsx
│ ├── drawer.tsx
│ ├── editor
│ │ ├── editor.tsx
│ │ └── index.tsx
│ ├── error-message.tsx
│ ├── file-input.tsx
│ ├── form-validation-error.tsx
│ ├── form
│ │ └── form.tsx
│ ├── import-csv.tsx
│ ├── input-phone-number.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── link-button.tsx
│ ├── link.tsx
│ ├── loader
│ │ ├── loader.module.css
│ │ └── loader.tsx
│ ├── loading-bar
│ │ ├── index.tsx
│ │ └── loading-bar.module.scss
│ ├── loading-progress
│ │ ├── index.tsx
│ │ └── loading-bar.module.scss
│ ├── logo.tsx
│ ├── modal
│ │ ├── managed-modal.tsx
│ │ ├── modal.context.tsx
│ │ └── modal.tsx
│ ├── notification-card.tsx
│ ├── page-loader
│ │ ├── page-loader.module.css
│ │ └── page-loader.tsx
│ ├── pagination.tsx
│ ├── password-input.tsx
│ ├── progress-box
│ │ ├── progress-box.module.css
│ │ └── progress-box.tsx
│ ├── radio-card
│ │ ├── radio-card.module.css
│ │ └── radio-card.tsx
│ ├── radio
│ │ ├── index.tsx
│ │ └── radio.module.css
│ ├── scrollable-content.tsx
│ ├── scrollbar.tsx
│ ├── select-input.tsx
│ ├── select
│ │ ├── select.styles.ts
│ │ └── select.tsx
│ ├── sidebar-menu.tsx
│ ├── switch-input.tsx
│ ├── table.tsx
│ ├── text-area.tsx
│ ├── title.tsx
│ ├── truncate-scroll.tsx
│ └── truncate.tsx
└── widgets
│ ├── column-chart.tsx
│ ├── donut-chart.tsx
│ ├── gradient-graph-chart.tsx
│ ├── graph-chart.tsx
│ ├── line-chart.tsx
│ ├── map-widget.tsx
│ ├── radial-bar-bhart.tsx
│ └── sticker-card.tsx
├── contexts
├── settings.context.tsx
├── staff.context.tsx
├── time.context.tsx
└── ui.context.tsx
├── hooks
├── index.ts
├── use-price.ts
├── use-store.ts
├── useErrorLogger.ts
├── useGetStaff.ts
├── useId.tsx
├── useLocalStorage.ts
├── useMediaQuery.ts
├── useStorage.ts
├── useTime.ts
├── useWarnIfUnsavedChanges.ts
└── use_percent-decrease.ts
├── init.sql
├── jest.config.js
├── jest.setup.js
├── lib
├── S3
│ ├── Date.ts
│ ├── ErrorThrower.ts
│ ├── Policy.ts
│ ├── Signature.ts
│ ├── react-aws-s3.ts
│ └── types.ts
├── ca-certificate.crt
├── conn.ts
├── database.ts
├── index.ts
├── notify.ts
├── sentry.ts
└── sql
│ ├── attribute.sql.ts
│ ├── carousel.sql.ts
│ ├── category.sql.ts
│ ├── coupon.sql.ts
│ ├── dashboard.sql.ts
│ ├── index.ts
│ ├── login.sql.ts
│ ├── order.sql.ts
│ ├── orderStatus.sql.ts
│ ├── product.sql.ts
│ ├── settings.sql.ts
│ ├── shipping.sql.ts
│ ├── staff.sql.ts
│ ├── supplier.sql.ts
│ └── tag.sql.ts
├── middleware.ts
├── middleware
├── jwt.keys.ts
├── jwtRS256.key.pub
└── utils.ts
├── next-env.d.ts
├── next-i18next.config.js
├── next.config.js
├── package.json
├── pages
├── [slug].tsx
├── _app.tsx
├── _document.tsx
├── _error.js
├── admin
│ ├── attributes
│ │ ├── create.tsx
│ │ ├── edit
│ │ │ └── [attributeId].tsx
│ │ └── index.tsx
│ ├── categories
│ │ ├── create.tsx
│ │ ├── edit
│ │ │ └── [categoryId].tsx
│ │ └── index.tsx
│ ├── dashboard.tsx
│ ├── hero-carousel
│ │ ├── create.tsx
│ │ ├── edit
│ │ │ └── [sliderId].tsx
│ │ └── index.tsx
│ ├── login.tsx
│ ├── logout.tsx
│ ├── orders
│ │ ├── [orderId]
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── products
│ │ ├── create.tsx
│ │ ├── edit
│ │ │ └── [productId].tsx
│ │ └── index.tsx
│ ├── settings
│ │ └── index.tsx
│ └── staffs
│ │ ├── create.tsx
│ │ ├── edit
│ │ └── [staffId].tsx
│ │ └── index.tsx
├── api
│ ├── admin
│ │ ├── attribute
│ │ │ ├── [id].ts
│ │ │ ├── attributes
│ │ │ │ ├── [page].ts
│ │ │ │ └── select
│ │ │ │ │ └── all.ts
│ │ │ ├── create.ts
│ │ │ ├── delete.ts
│ │ │ ├── update.ts
│ │ │ └── value
│ │ │ │ └── delete.ts
│ │ ├── banner
│ │ │ ├── [id].ts
│ │ │ ├── banners
│ │ │ │ └── index.ts
│ │ │ ├── create.ts
│ │ │ ├── delete.ts
│ │ │ └── update.ts
│ │ ├── category
│ │ │ ├── [id].ts
│ │ │ ├── categories
│ │ │ │ ├── [page].ts
│ │ │ │ └── select
│ │ │ │ │ ├── [id].ts
│ │ │ │ │ ├── all.ts
│ │ │ │ │ └── index.ts
│ │ │ ├── create.ts
│ │ │ └── update.ts
│ │ ├── dashboard
│ │ │ └── index.ts
│ │ ├── media
│ │ │ └── delete.ts
│ │ ├── order
│ │ │ ├── [id].ts
│ │ │ ├── orders
│ │ │ │ └── [page].ts
│ │ │ └── update-status.ts
│ │ ├── product
│ │ │ ├── [id].ts
│ │ │ ├── create.ts
│ │ │ ├── delete.ts
│ │ │ ├── products
│ │ │ │ └── [page].ts
│ │ │ └── update.ts
│ │ ├── settings
│ │ │ ├── index.ts
│ │ │ └── update.ts
│ │ └── staff
│ │ │ ├── [id].ts
│ │ │ ├── block.ts
│ │ │ ├── create.ts
│ │ │ ├── delete.ts
│ │ │ ├── login.ts
│ │ │ ├── staffs
│ │ │ └── [page].ts
│ │ │ └── update.ts
│ ├── order.ts
│ └── store
│ │ ├── category-request
│ │ └── [name].ts
│ │ ├── home-request.ts
│ │ └── product
│ │ └── [slug].ts
├── category
│ └── [name].tsx
├── faq.tsx
├── index.tsx
└── terms.tsx
├── postcss.config.js
├── public
├── arrow-next.svg
├── arrow-previous.svg
├── category-placeholder.jpg
├── favicons
│ ├── android-chrome-192x192.png
│ ├── android-chrome-256x256.png
│ ├── apple-touch-icon.png
│ ├── browserconfig.xml
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── favicon.ico
│ ├── mstile-150x150.png
│ └── safari-pinned-tab.svg
├── icons
│ ├── apple-icon-180.png
│ ├── manifest-icon-192.png
│ └── manifest-icon-512.png
├── image
│ ├── card-argon.png
│ ├── card-helium.png
│ ├── card-krypton.png
│ ├── card-neon.png
│ ├── card-xenon.png
│ ├── layout-classic.png
│ ├── layout-modern.png
│ └── layout-standard.png
├── locales
│ ├── ar
│ │ ├── banner.json
│ │ ├── common.json
│ │ ├── form.json
│ │ ├── table.json
│ │ └── widgets.json
│ ├── en
│ │ ├── banner.json
│ │ ├── common.json
│ │ ├── error.json
│ │ ├── form.json
│ │ ├── table.json
│ │ └── widgets.json
│ └── fr
│ │ ├── banner.json
│ │ ├── common.json
│ │ ├── form.json
│ │ ├── table.json
│ │ └── widgets.json
├── logo.svg
├── manifest.json
├── placeholders
│ ├── avatar.jpg
│ ├── avatar.svg
│ ├── avatar__placeholder.png
│ ├── image.jpg
│ ├── image__placeholder.png
│ └── no-image.svg
├── robots.txt
├── shop.jpg
├── sw.js
└── workbox-4a677df8.js
├── redux
├── card
│ └── index.ts
└── index.ts
├── settings
├── seo.settings.ts
└── site.settings.ts
├── store
├── assets
│ ├── icons
│ │ ├── arrow-left.tsx
│ │ ├── arrow-right.tsx
│ │ ├── cart-icon.tsx
│ │ ├── categories.tsx
│ │ ├── chevron-down.tsx
│ │ ├── chevron-left.tsx
│ │ ├── chevron-right.tsx
│ │ ├── close.tsx
│ │ ├── empty-svg.tsx
│ │ ├── home.tsx
│ │ ├── medi-icon-one.tsx
│ │ ├── medi-icon-three.tsx
│ │ ├── medi-icon-two.tsx
│ │ ├── minus-icon.tsx
│ │ ├── not-found.tsx
│ │ ├── phone.tsx
│ │ ├── plus-icon.tsx
│ │ ├── quote.tsx
│ │ ├── search-icon.tsx
│ │ ├── social-icons.tsx
│ │ ├── success-tick.tsx
│ │ └── trash.tsx
│ ├── image
│ │ ├── fb.svg
│ │ ├── git.svg
│ │ ├── hero-banner-img.jpg
│ │ ├── inst.svg
│ │ ├── link.svg
│ │ ├── twt.svg
│ │ └── ytb.svg
│ └── styles
│ │ ├── index.css
│ │ ├── rc-collapse.css
│ │ └── scrollbar.css
├── components
│ ├── CategoryHeader.tsx
│ ├── CategorySlider.tsx
│ ├── accordion.tsx
│ ├── active-link.tsx
│ ├── animated-counter.tsx
│ ├── banner.tsx
│ ├── breadcrumb.tsx
│ ├── button.tsx
│ ├── carousel
│ │ ├── carousel.tsx
│ │ ├── slider.tsx
│ │ └── thumbnail-carousel.tsx
│ ├── cart-item.tsx
│ ├── counter.tsx
│ ├── feature-block.tsx
│ ├── icon-button.tsx
│ ├── input.tsx
│ ├── instagram-card.tsx
│ ├── item-card.tsx
│ ├── scrollbar.tsx
│ ├── search-outline.tsx
│ ├── search.tsx
│ ├── section-title.tsx
│ ├── testimonial-carousel.tsx
│ ├── textarea.tsx
│ └── utils
│ │ ├── prop-types.ts
│ │ └── theme.ts
├── containers
│ ├── banner
│ │ ├── hero-banner-card.tsx
│ │ └── hero-block.tsx
│ ├── drawer
│ │ ├── drawer.tsx
│ │ └── views
│ │ │ ├── cart.tsx
│ │ │ ├── checkout.tsx
│ │ │ ├── menus.tsx
│ │ │ ├── no-item.tsx
│ │ │ └── order-submit.tsx
│ ├── how-it-works.tsx
│ ├── instagram-review.tsx
│ ├── layout
│ │ ├── footer.tsx
│ │ ├── header.tsx
│ │ ├── layout.tsx
│ │ └── menu
│ │ │ └── index.tsx
│ ├── product
│ │ ├── ProductTopSells.tsx
│ │ ├── attribute-value-label.tsx
│ │ ├── product-attributes.tsx
│ │ ├── product-details.tsx
│ │ └── variation-price.tsx
│ ├── products.tsx
│ └── term
│ │ ├── data.ts
│ │ └── terms.tsx
├── contexts
│ ├── drawer
│ │ └── drawer.provider.tsx
│ ├── search
│ │ └── use-search.tsx
│ └── sticky
│ │ └── sticky.provider.tsx
└── helpers
│ ├── constants.tsx
│ ├── get-initials-or-option.tsx
│ ├── get-products.tsx
│ ├── use-media.ts
│ ├── use-ref-scroll.tsx
│ ├── use-searchable.tsx
│ ├── use-storage.tsx
│ └── useOnClickOutside.tsx
├── styles
├── color-picker.module.css
├── custom-classes.css
├── custom-plugins.css
└── main.css
├── tailwind.config.js
├── ts-types
├── constants.ts
├── custom.types.ts
├── enums.ts
├── generated.ts
└── index.ts
├── tsconfig.json
├── utils
├── api
│ ├── endpoints.ts
│ └── http.ts
├── auth-utils.ts
├── cartesian.ts
├── constants.ts
├── cookies.ts
├── currency.ts
├── data-mappers.ts
├── form-error.tsx
├── format-address.tsx
├── is-loggedin.ts
├── locals.tsx
├── motion
│ ├── fade-in-left.ts
│ ├── fade-in-out.ts
│ ├── fade-in-right.ts
│ ├── height-collapse.ts
│ ├── zoom-in-bottom.ts
│ ├── zoom-in-out.ts
│ └── zoom-out-in.ts
├── parse-cookie.ts
├── private-route.tsx
├── routes.ts
├── use-breadcrumb.ts
├── use-click-outside.ts
├── use-price.ts
└── utils.ts
└── yarn.lock
/.eslintignore:
--------------------------------------------------------------------------------
1 | .next
2 | .now
3 | .vercel
4 | node_modules
5 | .vscode
6 | .husky
7 | .github
8 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: ['simple-import-sort'],
3 | root: true,
4 | parserOptions: {
5 | ecmaVersion: 2020,
6 | sourceType: 'module',
7 | warnOnUnsupportedTypeScriptVersion: false,
8 | ecmaFeatures: {
9 | jsx: true
10 | }
11 | },
12 | settings: {
13 | react: {
14 | version: 'detect'
15 | }
16 | },
17 | env: {
18 | browser: true,
19 | amd: true,
20 | node: true
21 | },
22 | extends: [
23 | 'next',
24 | 'next/core-web-vitals',
25 | 'eslint:recommended',
26 | 'plugin:jsx-a11y/recommended'
27 | ],
28 | rules: {
29 | 'simple-import-sort/imports': 'error',
30 | 'simple-import-sort/exports': 'error',
31 | 'react/react-in-jsx-scope': 'off',
32 | 'jsx-a11y/anchor-is-valid': [
33 | 'error',
34 | {
35 | components: ['Link'],
36 | specialLink: ['hrefLeft', 'hrefRight'],
37 | aspects: ['invalidHref', 'preferButton']
38 | }
39 | ]
40 | },
41 | overrides: [
42 | Object.assign(
43 | {
44 | files: ['**/__tests__/*-spec.tsx', '**/__mocks__/*.ts'],
45 | env: { jest: true },
46 | plugins: ['jest']
47 | },
48 | require('eslint-plugin-jest').configs.recommended
49 | )
50 | ]
51 | };
52 |
--------------------------------------------------------------------------------
/.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 | tsconfig.tsbuildinfo
8 |
9 | # volumes
10 | /web-root/
11 | /dhparam/
12 | /pgadmin/
13 | /database/
14 |
15 | # testing
16 | /coverage
17 |
18 | # next.js
19 | /.next/
20 | /out/
21 | .vscode
22 |
23 | # production
24 | /build
25 |
26 | # misc
27 | .DS_Store
28 | *.pem
29 |
30 | # debug
31 | npm-debug.log*
32 | yarn-debug.log*
33 | yarn-error.log*
34 |
35 | # local env files
36 | .env
37 | .env.local
38 | .env.development.local
39 | .env.test.local
40 | .env.production.local
41 |
42 | # vercel
43 | .vercel
44 |
45 | .vercel
46 |
47 | # Sentry
48 | .sentryclirc
49 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | echo '🏗️👷 Styling, testing and building your project before committing'
5 |
6 | # Check Prettier standards
7 | # yarn check-format ||
8 | # (
9 | # echo '🤢🤮🤢🤮 Its FOKING RAW - Your styling looks disgusting. 🤢🤮🤢🤮
10 | # Prettier Check Failed. Run yarn format, add changes and try commit again.'
11 | # false
12 | # )
13 |
14 | # Check ESLint Standards
15 | # yarn lint ||
16 | # (
17 | # echo '😤🏀👋😤 Get that weak shit out of here! 😤🏀👋😤
18 | # ESLint Check Failed. Make the required changes listed above, add changes and try to commit again.'
19 | # false
20 | # )
21 |
22 | # Check tsconfig standards
23 | # yarn check-types ||
24 | # (
25 | # echo '🤡😂❌🤡 Failed Type check. 🤡😂❌🤡
26 | # Are you seriously trying to write that? Make the changes required above.'
27 | # false
28 | # )
29 |
30 | # If everything passes... Now we can commit
31 | echo '🤔🤔🤔🤔... Alright.... Code looks good to me... Trying to build now. 🤔🤔🤔🤔'
32 |
33 | # yarn build ||
34 | # (
35 | # echo '❌👷🔨❌ Better call Bob... Because your build failed ❌👷🔨❌
36 | # Next build failed: View the errors above to see why.'
37 | # false
38 | # )
39 |
40 | # If everything passes... Now we can commit
41 | echo '✅✅✅✅ You win this time... I am committing this now. ✅✅✅✅'
42 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .next
2 | .now
3 | .vercel
4 | .storybook
5 | coverage
6 | node_modules
7 | next-env.d.ts
8 | yarn.lock
9 | ./utils/cities.min.json
10 | utils/cities.min.json
11 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "trailingComma": "none",
8 | "bracketSpacing": true,
9 | "jsxBracketSameLine": false,
10 | "arrowParens": "always",
11 | "proseWrap": "always"
12 | }
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### ------------------------
2 | Generate jwt public and private keys
3 |
4 | ```bash
5 | ssh-keygen -t rsa -b 4096 -m PEM -f jwtRS256.key
6 | # Don't add passphrase
7 | openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub
8 | cat jwtRS256.key
9 | cat jwtRS256.key.pub
10 | ```
11 | 
12 |
13 |
14 | 
15 |
16 |
--------------------------------------------------------------------------------
/additional.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | interface Number {
3 | toCommas(): string;
4 | secondsToHm(): string;
5 | }
6 |
--------------------------------------------------------------------------------
/components/ErrorBoundary/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class ErrorBoundary extends React.Component {
4 | constructor(props) {
5 | super(props);
6 |
7 | // Define a state variable to track whether is an error or not
8 | this.state = { hasError: false };
9 | }
10 | // eslint-disable-next-line no-unused-vars
11 | static getDerivedStateFromError(error) {
12 | // Update state so the next render will show the fallback UI
13 |
14 | return { hasError: true };
15 | }
16 | componentDidCatch(error, errorInfo) {
17 | // You can use your own error logging service here
18 | console.log({ error, errorInfo });
19 | }
20 | render() {
21 | // Check if the error is thrown
22 | if ((this.state as { hasError: boolean }).hasError) {
23 | // You can render any custom fallback UI
24 | return (
25 |
26 |
Oops, there is an error!
27 |
33 |
34 | );
35 | }
36 |
37 | // Return children components in case of no error
38 |
39 | return this.props.children;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/components/ImageComponent/index.tsx:
--------------------------------------------------------------------------------
1 | import Image, { ImageProps } from 'next/image';
2 | import React, { memo } from 'react';
3 |
4 | const ImageComponent = (props: ImageProps) => {
5 | const Base64Placeholder =
6 | 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8+utrPQAJNQNlcqdyCgAAAABJRU5ErkJggg==';
7 |
8 | return (
9 |
15 | );
16 | };
17 |
18 | export default memo(ImageComponent);
19 |
--------------------------------------------------------------------------------
/components/auth/forget-password/enter-email-view.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@components/ui/button';
2 | import Input from '@components/ui/input';
3 | import { useForm } from 'react-hook-form';
4 | import { yupResolver } from '@hookform/resolvers/yup';
5 | import * as yup from 'yup';
6 | import { useTranslation } from 'next-i18next';
7 | interface Props {
8 | onSubmit: (values: { email: string }) => void;
9 | loading: boolean;
10 | }
11 | const schema = yup.object().shape({
12 | email: yup
13 | .string()
14 | .email('form:error-email-format')
15 | .required('form:error-email-required')
16 | });
17 |
18 | const EnterEmailView = ({ onSubmit, loading }: Props) => {
19 | const { t } = useTranslation();
20 | const {
21 | register,
22 | handleSubmit,
23 |
24 | formState: { errors }
25 | } = useForm<{ email: string }>({ resolver: yupResolver(schema) });
26 |
27 | return (
28 |
42 | );
43 | };
44 |
45 | export default EnterEmailView;
46 |
--------------------------------------------------------------------------------
/components/auth/forget-password/enter-new-password-view.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@components/ui/button';
2 | import PasswordInput from '@components/ui/password-input';
3 | import { useForm } from 'react-hook-form';
4 | import { yupResolver } from '@hookform/resolvers/yup';
5 | import * as yup from 'yup';
6 | import { useTranslation } from 'next-i18next';
7 | interface Props {
8 | onSubmit: (values: { password: string }) => void;
9 | loading: boolean;
10 | }
11 | const schema = yup.object().shape({
12 | password: yup.string().required('form:error-password-required')
13 | });
14 |
15 | const EnterNewPasswordView = ({ onSubmit, loading }: Props) => {
16 | const { t } = useTranslation();
17 | const {
18 | register,
19 | handleSubmit,
20 | formState: { errors }
21 | } = useForm<{ password: string }>({ resolver: yupResolver(schema) });
22 |
23 | return (
24 |
37 | );
38 | };
39 |
40 | export default EnterNewPasswordView;
41 |
--------------------------------------------------------------------------------
/components/auth/forget-password/enter-token-view.tsx:
--------------------------------------------------------------------------------
1 | import Button from '@components/ui/button';
2 | import Input from '@components/ui/input';
3 | import { useForm } from 'react-hook-form';
4 | import { yupResolver } from '@hookform/resolvers/yup';
5 | import * as yup from 'yup';
6 | import { useTranslation } from 'next-i18next';
7 | interface Props {
8 | onSubmit: (values: { token: string }) => void;
9 | loading: boolean;
10 | }
11 | const schema = yup.object().shape({
12 | token: yup.string().required('form:error-token-required')
13 | });
14 |
15 | const EnterTokenView = ({ onSubmit, loading }: Props) => {
16 | const { t } = useTranslation();
17 | const {
18 | register,
19 | handleSubmit,
20 | formState: { errors }
21 | } = useForm<{ token: string }>({ resolver: yupResolver(schema) });
22 |
23 | return (
24 |
36 | );
37 | };
38 |
39 | export default EnterTokenView;
40 |
--------------------------------------------------------------------------------
/components/category/category-validation-schema.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | export const categoryValidationSchema = yup.object().shape({
4 | name: yup.string().required('form:error-name-required')
5 | });
6 |
--------------------------------------------------------------------------------
/components/common/access-denied.tsx:
--------------------------------------------------------------------------------
1 | import Link from '@components/ui/link';
2 | import Image from 'next/image';
3 | import { useTranslation } from 'next-i18next';
4 |
5 | const AccessDeniedPage = () => {
6 | const { t } = useTranslation('common');
7 |
8 | return (
9 |
10 |
11 |
16 |
17 |
18 |
19 | {t('text-access-denied')}
20 |
21 |
22 | {t('text-access-denied-message')}
23 |
24 |
28 | {t('text-return-home')}
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default AccessDeniedPage;
36 |
--------------------------------------------------------------------------------
/components/common/avatar.tsx:
--------------------------------------------------------------------------------
1 | // import Image from 'next/image';
2 | import ImageComponent from '@components/ImageComponent/index';
3 | import cn from 'classnames';
4 | import React from 'react';
5 |
6 | type AvatarProps = {
7 | className?: string;
8 | src: string;
9 | alt?: string;
10 | width?: number;
11 | height?: number;
12 | };
13 |
14 | const Avatar: React.FC = ({
15 | src,
16 | className,
17 | alt = 'Avatar',
18 | ...rest
19 | }) => {
20 | return (
21 |
28 |
35 |
36 | );
37 | };
38 |
39 | export default Avatar;
40 |
--------------------------------------------------------------------------------
/components/common/card.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import React from 'react';
3 |
4 | type Props = {
5 | className?: string;
6 | [key: string]: unknown;
7 | };
8 |
9 | const Card: React.FC = ({ className, ...props }) => {
10 | return ;
11 | };
12 |
13 | export default Card;
14 |
--------------------------------------------------------------------------------
/components/hero-carousel/hero-slider-validation-schema.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | export const categoryValidationSchema = yup.object().shape({
4 | // name: yup.string().required('form:error-name-required'),
5 | // icon: yup.object().nullable().required('form:error-icon-required')
6 | });
7 |
--------------------------------------------------------------------------------
/components/icons/add.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Add = ({
4 | color = 'currentColor',
5 | width = '12px',
6 | height = '12px',
7 | ...props
8 | }) => {
9 | return (
10 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/icons/arrow-down.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const ArrowDown = ({
3 | color = 'currentColor',
4 | width = '12px',
5 | height = '12px',
6 | ...props
7 | }) => {
8 | return (
9 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/icons/arrow-next.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowNext = ({ ...props }) => {
2 | return (
3 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/components/icons/arrow-prev.tsx:
--------------------------------------------------------------------------------
1 | export const ArrowPrev = ({ ...props }) => {
2 | return (
3 |
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/components/icons/arrow-up.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const ArrowUp = ({
3 | color = 'currentColor',
4 | width = '12px',
5 | height = '12px',
6 | ...props
7 | }) => {
8 | return (
9 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/components/icons/ban-user.tsx:
--------------------------------------------------------------------------------
1 | export const BanUser = ({ ...props }) => {
2 | return (
3 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/components/icons/bell.tsx:
--------------------------------------------------------------------------------
1 | export const Bell = ({ ...props }) => {
2 | return (
3 |
11 | );
12 | };
13 |
--------------------------------------------------------------------------------
/components/icons/category/accessories.tsx:
--------------------------------------------------------------------------------
1 | export const Accessories: React.FC> = (props) => {
2 | return (
3 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/components/icons/category/bed.tsx:
--------------------------------------------------------------------------------
1 | export const Bed: React.FC> = (props) => (
2 |
17 | );
18 |
--------------------------------------------------------------------------------
/components/icons/category/hand-bag.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const HandBags: React.FC> = (props) => {
3 | return (
4 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/icons/category/laptop-bag.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const LaptopBags: React.FC> = (props) => {
3 | return (
4 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/icons/category/lips.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const Lips: React.FC> = (props) => {
3 | return (
4 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/icons/category/wallet.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const Wallet: React.FC> = (props) => {
3 | return (
4 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/icons/checkmark-circle-fill.tsx:
--------------------------------------------------------------------------------
1 | export const CheckMarkFill = ({ ...props }) => {
2 | return (
3 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/components/icons/checkmark-circle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const CheckMarkCircle = ({ ...props }) => {
3 | return (
4 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/icons/checkmark.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const CheckMark = ({ ...props }) => {
3 | return (
4 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/components/icons/close-fill.tsx:
--------------------------------------------------------------------------------
1 | export const CloseFillIcon: React.FC> = (props) => (
2 |
12 | );
13 |
--------------------------------------------------------------------------------
/components/icons/close-icon.tsx:
--------------------------------------------------------------------------------
1 | export const CloseIcon: React.FC> = (props) => (
2 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/icons/copy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const CopyIcon = (props) => {
3 | return (
4 |
21 | );
22 | };
23 |
--------------------------------------------------------------------------------
/components/icons/dot.tsx:
--------------------------------------------------------------------------------
1 | export const Dot = ({ ...props }) => {
2 | return (
3 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/components/icons/download-icon.tsx:
--------------------------------------------------------------------------------
1 | export const DownloadIcon = ({ ...rest }) => {
2 | return (
3 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/icons/edit copy.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const EditIcon = ({ ...props }) => {
4 | return (
5 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/icons/edit.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Edit = ({ ...props }) => {
4 | return (
5 |
22 | );
23 | };
24 |
25 | export default Edit;
26 |
--------------------------------------------------------------------------------
/components/icons/expand-less-icon.tsx:
--------------------------------------------------------------------------------
1 | export const ExpandLessIcon = () => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/expand-more-icon.tsx:
--------------------------------------------------------------------------------
1 | export const ExpandMoreIcon = () => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/eye-icon.tsx:
--------------------------------------------------------------------------------
1 | export const Eye: React.FC> = (props) => (
2 |
22 | );
23 |
--------------------------------------------------------------------------------
/components/icons/eye-off-icon.tsx:
--------------------------------------------------------------------------------
1 | export const EyeOff: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/ios-arrow-down.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const IosArrowDown = ({
3 | color = 'currentColor',
4 | width = '7px',
5 | height = '10px',
6 | ...props
7 | }) => {
8 | return (
9 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/icons/ios-arrow-up.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const IosArrowUp = ({
3 | color = 'currentColor',
4 | width = '7px',
5 | height = '10px',
6 | ...props
7 | }) => {
8 | return (
9 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/components/icons/map-pin.tsx:
--------------------------------------------------------------------------------
1 | export const MapPin = ({ ...props }) => {
2 | return (
3 |
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/components/icons/minus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const Minus = ({
4 | color = 'currentColor',
5 | width = '12px',
6 | height = '12px',
7 | ...props
8 | }) => {
9 | return (
10 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/components/icons/more-icon.tsx:
--------------------------------------------------------------------------------
1 | export const MoreIcon: React.FC> = (props) => (
2 |
12 | );
13 |
--------------------------------------------------------------------------------
/components/icons/navbar-icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const NavbarIcon: React.FC> = (props) => (
4 |
29 | );
30 |
--------------------------------------------------------------------------------
/components/icons/order-icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const OrderIcon = ({ width = '11.321', height = '13' }) => {
3 | return (
4 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/components/icons/phone.tsx:
--------------------------------------------------------------------------------
1 | export const PhoneIcon = ({ ...props }) => {
2 | return (
3 |
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/components/icons/product-icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const ProductIcon = ({ width = '15.6', height = '13' }) => {
3 | return (
4 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/components/icons/save-icon.tsx:
--------------------------------------------------------------------------------
1 | export const SaveIcon: React.FC> = (props) => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/icons/search-icon.tsx:
--------------------------------------------------------------------------------
1 | export const SearchIcon: React.FC> = (props) => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/icons/shops/cube.tsx:
--------------------------------------------------------------------------------
1 | export const CubeIcon: React.FC> = (props) => {
2 | return (
3 |
14 | );
15 | };
16 |
--------------------------------------------------------------------------------
/components/icons/sidebar-category-icon.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | export const SidebarCategoryIcon = ({ width = '12.958', height = '13' }) => {
3 | return (
4 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/components/icons/sidebar/affiliate.tsx:
--------------------------------------------------------------------------------
1 | export const AffiliateIcon: React.FC> = (props) => (
2 |
25 | );
26 |
--------------------------------------------------------------------------------
/components/icons/sidebar/apps.tsx:
--------------------------------------------------------------------------------
1 | export const AppsIcon: React.FC> = (props) => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/icons/sidebar/categories.tsx:
--------------------------------------------------------------------------------
1 | export const CategoriesIcon: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/sidebar/coupons.tsx:
--------------------------------------------------------------------------------
1 | export const CouponsIcon: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/sidebar/image-multiple.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export const ImageMultipleIcon = (props) => (
4 |
17 | );
18 |
--------------------------------------------------------------------------------
/components/icons/sidebar/order-status.tsx:
--------------------------------------------------------------------------------
1 | export const OrdersStatusIcon: React.FC> = (props) => (
2 |
17 | );
18 |
--------------------------------------------------------------------------------
/components/icons/sidebar/orders.tsx:
--------------------------------------------------------------------------------
1 | export const OrdersIcon: React.FC> = (props) => (
2 |
26 | );
27 |
--------------------------------------------------------------------------------
/components/icons/sidebar/products.tsx:
--------------------------------------------------------------------------------
1 | export const ProductsIcon: React.FC> = (props) => (
2 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/icons/sidebar/settings.tsx:
--------------------------------------------------------------------------------
1 | export const SettingsIcon: React.FC> = (props) => (
2 |
22 | );
23 |
--------------------------------------------------------------------------------
/components/icons/sidebar/shippings.tsx:
--------------------------------------------------------------------------------
1 | export const ShippingsIcon: React.FC> = (props) => (
2 |
20 | );
21 |
--------------------------------------------------------------------------------
/components/icons/sidebar/shop.tsx:
--------------------------------------------------------------------------------
1 | export const ShopIcon: React.FC> = (props) => (
2 |
14 | );
15 |
--------------------------------------------------------------------------------
/components/icons/sidebar/staffs.tsx:
--------------------------------------------------------------------------------
1 | export const StaffsIcon: React.FC> = (props) => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/icons/sidebar/suppliers.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export const SuppliersIcon: React.FC> = (props) => (
4 |
19 | );
20 |
--------------------------------------------------------------------------------
/components/icons/sidebar/support.tsx:
--------------------------------------------------------------------------------
1 | export const SupportIcon: React.FC> = (props) => (
2 |
15 | );
16 |
--------------------------------------------------------------------------------
/components/icons/sidebar/tags.tsx:
--------------------------------------------------------------------------------
1 | export const TagIcon: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/sidebar/taxes.tsx:
--------------------------------------------------------------------------------
1 | export const TaxesIcon: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/sidebar/users.tsx:
--------------------------------------------------------------------------------
1 | export const UsersIcon: React.FC> = (props) => (
2 |
16 | );
17 |
--------------------------------------------------------------------------------
/components/icons/sidebar/withdraw.tsx:
--------------------------------------------------------------------------------
1 | export const WithdrawIcon: React.FC> = (props) => (
2 |
24 | );
25 |
--------------------------------------------------------------------------------
/components/icons/social/facebook.tsx:
--------------------------------------------------------------------------------
1 | export const FacebookIcon: React.FC> = (props) => (
2 |
9 | );
10 |
--------------------------------------------------------------------------------
/components/icons/social/index.tsx:
--------------------------------------------------------------------------------
1 | export { FacebookIcon } from './facebook';
2 | export { InstagramIcon } from './instagram';
3 | export { TwitterIcon } from './twitter';
4 | export { YouTubeIcon } from './youtube';
5 |
--------------------------------------------------------------------------------
/components/icons/social/instagram.tsx:
--------------------------------------------------------------------------------
1 | export const InstagramIcon: React.FC> = (props) => (
2 |
19 | );
20 |
--------------------------------------------------------------------------------
/components/icons/social/twitter.tsx:
--------------------------------------------------------------------------------
1 | export const TwitterIcon: React.FC> = (props) => (
2 |
9 | );
10 |
--------------------------------------------------------------------------------
/components/icons/social/youtube.tsx:
--------------------------------------------------------------------------------
1 | export const YouTubeIcon: React.FC> = (props) => (
2 |
8 | );
9 |
--------------------------------------------------------------------------------
/components/icons/trash.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Trash = ({ ...props }) => {
4 | return (
5 |
31 | );
32 | };
33 |
34 | export default Trash;
35 |
--------------------------------------------------------------------------------
/components/icons/type/index.tsx:
--------------------------------------------------------------------------------
1 | export { FruitsVegetable } from './fruits-vegetable';
2 | export { FacialCare } from './facial-care';
3 | export { Handbag } from './handbag-icon';
4 | export { DressIcon } from './dress-icon';
5 | export { FurnitureIcon } from './furniture-icon';
6 | export { BookIcon } from './book-icon';
7 | export { MedicineIcon } from './medicine-icon';
8 | export { Restaurant } from './restaurant-icon';
9 | export { Bakery } from './bakery-icon';
10 |
--------------------------------------------------------------------------------
/components/icons/upload-icon copy.tsx:
--------------------------------------------------------------------------------
1 | export const UploadIcon = ({
2 | color = 'currentColor',
3 | width = '41px',
4 | height = '30px',
5 | ...rest
6 | }) => {
7 | return (
8 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/icons/upload-icon.tsx:
--------------------------------------------------------------------------------
1 | export const UploadIcon = ({
2 | color = 'currentColor',
3 | width = '41px',
4 | height = '30px',
5 | ...rest
6 | }) => {
7 | return (
8 |
24 | );
25 | };
26 |
--------------------------------------------------------------------------------
/components/navigation/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Navbar } from './navbar';
2 | export { default as Sidebar } from './sidebar';
3 | export { default as SidebarMini } from './sidebar/sidebar-mini';
4 |
--------------------------------------------------------------------------------
/components/navigation/sidebar/sidebar-mini.tsx:
--------------------------------------------------------------------------------
1 | import Scrollbar from '@components/ui/scrollbar';
2 | import { siteSettings } from '@settings/site.settings';
3 | import { useTranslation } from 'next-i18next';
4 | import React from 'react';
5 |
6 | import SidebarItem from './sidebar-item-mini';
7 |
8 | const SidebarMini: React.FC = () => {
9 | const { t } = useTranslation();
10 |
11 | return (
12 |
31 | );
32 | };
33 | export default SidebarMini;
34 |
--------------------------------------------------------------------------------
/components/product/product-validation-schema.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | export const productValidationSchema = yup.object().shape({
4 | // thumbnail
5 | // gallery
6 | name: yup.string().required('form:error-product-name-required'),
7 | shortDescription: yup
8 | .string()
9 | .test(
10 | 'len',
11 | 'Description Must be less than 160 characters',
12 | (val) => val.length < 160
13 | )
14 | .required('form:error-short-description-required'),
15 | description: yup.string().required('form:error-product-description-required'),
16 | // salePrice: yup
17 | // .number()
18 | // .typeError('form:error-amount-must-number')
19 | // .positive('form:error-price-must-positive')
20 | // .required('form:error-sale-price-required'),
21 | // comparePrice: yup
22 | // .number()
23 | // .typeError('form:error-amount-must-number')
24 | // .transform((value) => (isNaN(value) ? null : value)),
25 | // quantity: yup
26 | // .number()
27 | // .typeError('form:error-amount-must-number')
28 | // .required('form:error-quantity-required'),
29 | categories: yup.array().min(1, 'Category Required')
30 | });
31 |
--------------------------------------------------------------------------------
/components/settings/settings-validation-schema.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 | export const settingsValidationSchema = yup.object().shape({
3 | currency: yup.object().nullable().required('form:error-currency-required'),
4 | minimumOrderAmount: yup
5 | .number()
6 | .transform((value) => (isNaN(value) ? undefined : value))
7 | .moreThan(-1, 'form:error-sale-price-must-positive'),
8 | deliveryTime: yup
9 | .array()
10 | .min(1, 'add-at-least-one-delivery-time')
11 | .of(
12 | yup.object().shape({
13 | title: yup.string().required('form:error-title-required')
14 | })
15 | )
16 | });
17 |
--------------------------------------------------------------------------------
/components/staff/staff-validation-schema.ts:
--------------------------------------------------------------------------------
1 | import * as yup from 'yup';
2 |
3 | export const staffValidationSchema = yup.object().shape({
4 | firstName: yup.string().required('form:error-last-name-required'),
5 | lastName: yup.string().required('form:error-first-name-required'),
6 | email: yup
7 | .string()
8 | .email('form:error-email-format')
9 | .required('form:error-email-required'),
10 | confirmPassword: yup
11 | .string()
12 | .oneOf([yup.ref('password'), null], 'form:error-match-passwords')
13 | .required('form:error-confirm-password'),
14 | password: yup.string().required('form:error-password-required')
15 | });
16 |
--------------------------------------------------------------------------------
/components/ui/activeLink.tsx:
--------------------------------------------------------------------------------
1 | import NextLink, { LinkProps as NextLinkProps } from 'next/link';
2 | import { useRouter } from 'next/router';
3 | import React, { useMemo } from 'react';
4 |
5 | type Props = {
6 | activeClassName: string;
7 | includes: string;
8 | className: string;
9 | };
10 |
11 | const ActiveLink: React.FC = ({
12 | href,
13 | children,
14 | activeClassName,
15 | className,
16 | includes,
17 | ...props
18 | }) => {
19 | const { asPath } = useRouter();
20 |
21 | const A = useMemo(() => asPath?.split('/'), [asPath]);
22 | const B = useMemo(() => includes?.split('/'), [includes]);
23 |
24 | const class_name =
25 | asPath === href ||
26 | asPath === props.as ||
27 | A[A?.length - 1] === B[B?.length - 1]
28 | ? `${className} ${activeClassName}`.trim()
29 | : className;
30 |
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | };
37 |
38 | export default ActiveLink;
39 |
--------------------------------------------------------------------------------
/components/ui/badge/badge.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import { useTranslation } from 'next-i18next';
3 | import React from 'react';
4 |
5 | type BadgeProps = {
6 | className?: string;
7 | color?: string;
8 | textColor?: string;
9 | text?: string;
10 | textKey?: string;
11 | };
12 |
13 | const Badge: React.FC = (props) => {
14 | const { t } = useTranslation();
15 | const {
16 | className,
17 | color: colorOverride,
18 | textColor: textColorOverride,
19 | text,
20 | textKey
21 | } = props;
22 |
23 | const classes = {
24 | root: 'px-3 py-1 rounded-sm text-xs whitespace-nowrap shadow-sm',
25 | default: 'bg-accent',
26 | text: 'text-light'
27 | };
28 |
29 | return (
30 |
42 | {textKey ? t(textKey) : text}
43 |
44 | );
45 | };
46 |
47 | export default Badge;
48 |
--------------------------------------------------------------------------------
/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | import dynamic from 'next/dynamic';
2 |
3 | const Charts = dynamic(() => import('react-apexcharts'), { ssr: false });
4 |
5 | const Chart = ({ ...props }) => {
6 | return ;
7 | };
8 |
9 | export default Chart;
10 |
--------------------------------------------------------------------------------
/components/ui/checkbox/checkbox.module.css:
--------------------------------------------------------------------------------
1 | .checkbox {
2 | position: absolute;
3 | opacity: 0;
4 | }
5 |
6 | .checkbox + label {
7 | position: relative;
8 | cursor: pointer;
9 | padding: 0;
10 | display: inline-flex;
11 | align-items: center;
12 | }
13 |
14 | .checkbox + label:before {
15 | content: '';
16 | margin-right: 10px;
17 | display: inline-flex;
18 | width: 18px;
19 | height: 18px;
20 | border-radius: 3px;
21 | background-color: #ffffff;
22 | border: 1px solid rgb(var(--color-gray-300));
23 | }
24 |
25 | .checkbox:focus + label:before {
26 | border-color: rgb(var(--color-accent));
27 | }
28 |
29 | .checkbox:checked + label:before {
30 | background-color: rgb(var(--color-accent));
31 | border-color: rgb(var(--color-accent));
32 | }
33 |
34 | .checkbox:disabled + label {
35 | color: rgb(var(--text-base));
36 | cursor: auto;
37 | }
38 |
39 | .checkbox:disabled + label:before {
40 | box-shadow: none;
41 | background: rgb(var(--color-gray-300));
42 | }
43 |
44 | .checkbox:checked + label:after {
45 | content: '';
46 | position: absolute;
47 | left: 4px;
48 | top: 9px;
49 | background: #ffffff;
50 | width: 2px;
51 | height: 2px;
52 | box-shadow: 2px 0 0 #ffffff, 4px 0 0 #ffffff, 4px -2px 0 #ffffff,
53 | 4px -4px 0 #ffffff, 4px -6px 0 #ffffff, 4px -8px 0 #ffffff;
54 | transform: rotate(45deg);
55 | }
56 |
--------------------------------------------------------------------------------
/components/ui/checkbox/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { InputHTMLAttributes } from 'react';
2 |
3 | import styles from './checkbox.module.css';
4 |
5 | export interface Props extends InputHTMLAttributes {
6 | className?: string;
7 | label?: string;
8 | name: string;
9 | error?: string;
10 | inputClassName?: string;
11 | }
12 |
13 | const Checkbox = React.forwardRef(
14 | (
15 | { className, inputClassName, style, label, id, name, error, ...rest },
16 | ref
17 | ) => {
18 | return (
19 |
20 |
21 |
29 |
30 |
33 |
34 |
35 | {error &&
{error}
}
36 |
37 | );
38 | }
39 | );
40 |
41 | Checkbox.displayName = 'Checkbox';
42 |
43 | export default Checkbox;
44 |
--------------------------------------------------------------------------------
/components/ui/color-picker/display-color-code.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useWatch } from 'react-hook-form';
3 |
4 | const DisplayColorCode = ({ control }: any) => {
5 | const color = useWatch({
6 | control,
7 | name: 'color',
8 | defaultValue: '#9cd864' // default value before the render
9 | });
10 | return (
11 | <>
12 | {color !== null && (
13 |
14 | {color}
15 |
16 | )}
17 | >
18 | );
19 | };
20 |
21 | export default DisplayColorCode;
22 |
--------------------------------------------------------------------------------
/components/ui/date-picker.tsx:
--------------------------------------------------------------------------------
1 | import 'react-datepicker/dist/react-datepicker.css';
2 | export { default as DatePicker } from 'react-datepicker';
3 |
--------------------------------------------------------------------------------
/components/ui/description.tsx:
--------------------------------------------------------------------------------
1 | type Props = {
2 | className?: string;
3 | title?: string;
4 | details?: string | JSX.Element;
5 | [key: string]: unknown;
6 | };
7 |
8 | const Description: React.FC = ({
9 | title,
10 | details,
11 | className,
12 | ...props
13 | }) => {
14 | return (
15 |
16 | {title && (
17 |
{title}
18 | )}
19 | {details &&
{details}
}
20 |
21 | );
22 | };
23 |
24 | export default Description;
25 |
--------------------------------------------------------------------------------
/components/ui/editor/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from 'react';
2 | import { Controller } from 'react-hook-form';
3 |
4 | import EditorComponent from './editor';
5 |
6 | interface EditorInputProps {
7 | control: any;
8 | className?: string;
9 | name: string;
10 | [key: string]: unknown;
11 | }
12 |
13 | const Editor = ({ className, control, name, ...rest }: EditorInputProps) => {
14 | return (
15 | (
20 |
21 | )}
22 | />
23 | );
24 | };
25 |
26 | export default memo(Editor);
27 |
--------------------------------------------------------------------------------
/components/ui/error-message.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'next-i18next';
2 | interface Props {
3 | message?: string | undefined;
4 | }
5 |
6 | export const Error = ({ message }: Props) => {
7 | const { t } = useTranslation('common');
8 | return {t(message!)}
;
9 | };
10 |
11 | const ErrorMessage = ({ message }: Props) => {
12 | const { t } = useTranslation('common');
13 | return (
14 |
15 | {t(message!)}
16 |
17 | );
18 | };
19 |
20 | export default ErrorMessage;
21 |
--------------------------------------------------------------------------------
/components/ui/file-input.tsx:
--------------------------------------------------------------------------------
1 | import Uploader from '@components/common/uploader';
2 | import { Controller } from 'react-hook-form';
3 |
4 | interface FileInputProps {
5 | control: any;
6 | name: string;
7 | multiple?: boolean;
8 | }
9 |
10 | const FileInput = ({ control, name, multiple = true }: FileInputProps) => {
11 | return (
12 | (
18 |
19 | )}
20 | />
21 | );
22 | };
23 |
24 | export default FileInput;
25 |
--------------------------------------------------------------------------------
/components/ui/form-validation-error.tsx:
--------------------------------------------------------------------------------
1 | interface Props {
2 | message: string | undefined;
3 | }
4 |
5 | const ValidationError = ({ message }: Props) => {
6 | return {message}
;
7 | };
8 |
9 | export default ValidationError;
10 |
--------------------------------------------------------------------------------
/components/ui/form/form.tsx:
--------------------------------------------------------------------------------
1 | import { SubmitHandler, useForm, UseFormReturn } from 'react-hook-form';
2 |
3 | type FormProps = {
4 | onSubmit: SubmitHandler;
5 | children: (methods: UseFormReturn) => React.ReactNode;
6 | };
7 |
8 | export const Form = <
9 | TFormValues extends Record = Record
10 | >({
11 | onSubmit,
12 | children
13 | }: FormProps) => {
14 | const methods = useForm();
15 | return (
16 |
17 | );
18 | };
19 |
--------------------------------------------------------------------------------
/components/ui/import-csv.tsx:
--------------------------------------------------------------------------------
1 | import { UploadIcon } from '@components/icons/upload-icon';
2 | import { useDropzone } from 'react-dropzone';
3 |
4 | export default function ImportCsv({ onDrop, loading, title }: any) {
5 | const { getRootProps, getInputProps } = useDropzone({
6 | accept: '.csv',
7 | multiple: false,
8 | onDrop
9 | });
10 |
11 | return (
12 |
13 |
19 |
20 | {loading && (
21 |
27 | )}
28 | {!loading &&
}
29 |
30 | {title}
31 |
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import { LabelHTMLAttributes } from 'react';
3 |
4 | export interface Props extends LabelHTMLAttributes {
5 | className?: string;
6 | }
7 |
8 | const Label: React.FC = ({ className, ...rest }) => {
9 | return (
10 |
17 | );
18 | };
19 |
20 | export default Label;
21 |
--------------------------------------------------------------------------------
/components/ui/link.tsx:
--------------------------------------------------------------------------------
1 | import NextLink, { LinkProps as NextLinkProps } from 'next/link';
2 |
3 | const Link: React.FC<
4 | NextLinkProps & { className?: string; title?: string }
5 | > = ({ href, children, ...props }) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | export default Link;
14 |
--------------------------------------------------------------------------------
/components/ui/loader/loader.module.css:
--------------------------------------------------------------------------------
1 | @-webkit-keyframes spin {
2 | 0% {
3 | transform: rotate(0deg);
4 | }
5 | 100% {
6 | transform: rotate(360deg);
7 | }
8 | }
9 | @keyframes spin {
10 | 0% {
11 | transform: rotate(0deg);
12 | }
13 | 100% {
14 | transform: rotate(360deg);
15 | }
16 | }
17 | @-webkit-keyframes pulse {
18 | 50% {
19 | background: white;
20 | }
21 | }
22 | @keyframes pulse {
23 | 50% {
24 | background: white;
25 | }
26 | }
27 |
28 | .loading {
29 | border-radius: 50%;
30 | width: 40px;
31 | height: 40px;
32 | margin-bottom: 12px;
33 | border: 0.25rem solid rgba(0, 159, 127, 0.3);
34 | border-top-color: rgba(0, 159, 127, 1);
35 | -webkit-animation: spin 1s infinite linear;
36 | animation: spin 1s infinite linear;
37 | }
38 |
39 | .simple_loading {
40 | border-radius: 50%;
41 | border: 3px solid rgba(0, 159, 127, 0.3);
42 | border-top-color: rgba(0, 159, 127, 1);
43 | -webkit-animation: spin 1s infinite linear;
44 | animation: spin 1s infinite linear;
45 | }
46 |
--------------------------------------------------------------------------------
/components/ui/loader/loader.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 |
3 | import styles from './loader.module.css';
4 |
5 | interface Props {
6 | className?: string;
7 | text?: string;
8 | showText?: boolean;
9 | simple?: boolean;
10 | height?: string;
11 | borderColor?: string;
12 | }
13 |
14 | const Loader = (props: Props) => {
15 | const {
16 | className,
17 | showText = true,
18 | text = 'Loading...',
19 | simple,
20 | height = 'calc(100vh - 200px)',
21 | borderColor
22 | } = props;
23 | return (
24 | <>
25 | {simple ? (
26 |
30 | ) : (
31 |
38 |
39 |
40 | {showText && (
41 |
{text}
42 | )}
43 |
44 | )}
45 | >
46 | );
47 | };
48 |
49 | export default Loader;
50 |
--------------------------------------------------------------------------------
/components/ui/loading-bar/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames/bind';
2 | import { useRouter } from 'next/router';
3 | import React, { useEffect, useRef, useState } from 'react';
4 |
5 | import styles from './loading-bar.module.scss';
6 |
7 | let cx = classNames.bind(styles);
8 |
9 | const LoadingBar = () => {
10 | const Router = useRouter();
11 | const [Loading, setLoading] = useState(false);
12 | const LoadingStateCache = useRef(false);
13 | LoadingStateCache.current = Loading;
14 |
15 | useEffect(() => {
16 | Router.events.on('routeChangeStart', () => {
17 | if (!LoadingStateCache.current) setLoading(true);
18 | });
19 | Router.events.on('routeChangeComplete', () => {
20 | if (LoadingStateCache.current) {
21 | setLoading(false);
22 | }
23 | });
24 | Router.events.on('routeChangeError', () => {
25 | setLoading(false);
26 | // if (LoadingStateCache.current) {
27 |
28 | // }
29 | });
30 | return () => setLoading(false);
31 | // eslint-disable-next-line react-hooks/exhaustive-deps
32 | }, [Router.events]);
33 |
34 | return ;
35 | };
36 |
37 | export default LoadingBar;
38 |
--------------------------------------------------------------------------------
/components/ui/loading-bar/loading-bar.module.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .container {
4 | display: none;
5 | height: 4px;
6 | background: #27c4f5
7 | linear-gradient(to right, #27c4f5, #a307ba, #fd8d32, #70c050, #27c4f5);
8 | background-size: 200%;
9 | animation: LoadingBarProgress 1.5s linear infinite;
10 | transform-origin: left;
11 | width: 100%;
12 | left: 0;
13 | position: fixed;
14 | right: 0;
15 | top: 0;
16 | z-index: 999;
17 |
18 | @keyframes LoadingBarProgress {
19 | 0% {
20 | background-position: 0% 0%;
21 | }
22 |
23 | 100% {
24 | background-position: -200% 0%;
25 | }
26 | }
27 | }
28 |
29 | .show{
30 | display: block;
31 | }
--------------------------------------------------------------------------------
/components/ui/loading-progress/index.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames/bind';
2 | import React from 'react';
3 |
4 | import styles from './loading-bar.module.scss';
5 |
6 | let cx = classNames.bind(styles);
7 |
8 | const LoadingProgress = () => {
9 | return ;
10 | };
11 |
12 | export default LoadingProgress;
13 |
--------------------------------------------------------------------------------
/components/ui/loading-progress/loading-bar.module.scss:
--------------------------------------------------------------------------------
1 |
2 |
3 | .container {
4 | height: 4px;
5 | background: #27c4f5
6 | linear-gradient(to right, #27c4f5, #a307ba, #fd8d32, #70c050, #27c4f5);
7 | background-size: 200%;
8 | animation: LoadingBarProgress 1.5s linear infinite;
9 | transform-origin: left;
10 | width: 100%;
11 | left: 0;
12 | position: absolute;
13 | right: 0;
14 | top: 0;
15 | z-index: 999;
16 |
17 | @keyframes LoadingBarProgress {
18 | 0% {
19 | background-position: 0% 0%;
20 | }
21 |
22 | 100% {
23 | background-position: -200% 0%;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/components/ui/logo.tsx:
--------------------------------------------------------------------------------
1 | import Link from '@components/ui/link';
2 | import { siteSettings } from '@settings/site.settings';
3 | import cn from 'classnames';
4 | import Image from 'next/image';
5 | import React from 'react';
6 |
7 | const Logo: React.FC> = ({
8 | className,
9 | ...props
10 | }) => {
11 | return (
12 |
17 |
24 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default Logo;
37 |
--------------------------------------------------------------------------------
/components/ui/notification-card.tsx:
--------------------------------------------------------------------------------
1 | type NotificationCardType = {
2 | src?: string;
3 | text?: string | React.ReactNode;
4 | time?: string;
5 | };
6 |
7 | const NotificationCard: React.FC = ({
8 | src,
9 | text,
10 | time
11 | }) => {
12 | return (
13 |
17 |
22 |
23 |
24 |
{text}
25 |
{time}
26 |
27 |
28 | );
29 | };
30 |
31 | export default NotificationCard;
32 |
--------------------------------------------------------------------------------
/components/ui/page-loader/page-loader.module.css:
--------------------------------------------------------------------------------
1 | .page_loader {
2 | margin: 100px auto;
3 | width: 120px;
4 | height: 120px;
5 | border: 16px solid #f3f3f3;
6 | border-top: 16px solid #00d2a8;
7 | border-radius: 50%;
8 | animation: spin 2s linear infinite, heart-beat 2s linear infinite;
9 | background-color: #fff;
10 | text-align: center;
11 | line-height: 120px;
12 | }
13 |
14 | @keyframes spin {
15 | 0% {
16 | transform: rotate(0deg);
17 | }
18 | 100% {
19 | transform: rotate(360deg);
20 | }
21 | }
22 |
23 | @keyframes heart-beat {
24 | 55% {
25 | background-color: #fff;
26 | }
27 | 60% {
28 | background-color: #00d2a8;
29 | }
30 | 65% {
31 | background-color: #fff;
32 | }
33 | 70% {
34 | background-color: #00d2a8;
35 | }
36 | 100% {
37 | background-color: #fff;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/components/ui/page-loader/page-loader.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import styles from './page-loader.module.css';
3 | import { useTranslation } from 'next-i18next';
4 |
5 | const PageLoader = () => {
6 | const { t } = useTranslation('common');
7 | return (
8 |
13 |
14 |
15 |
16 | {t('text-loading')}
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default PageLoader;
24 |
--------------------------------------------------------------------------------
/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import 'rc-pagination/assets/index.css';
2 |
3 | import { ArrowNext } from '@components/icons/arrow-next';
4 | import { ArrowPrev } from '@components/icons/arrow-prev';
5 | import RCPagination, { PaginationProps } from 'rc-pagination';
6 | import React from 'react';
7 |
8 | const Pagination: React.FC = (props) => {
9 | return (
10 | }
13 | prevIcon={}
14 | {...props}
15 | />
16 | );
17 | };
18 |
19 | export default Pagination;
20 |
--------------------------------------------------------------------------------
/components/ui/progress-box/progress-box.module.css:
--------------------------------------------------------------------------------
1 | .progress_container {
2 | @apply flex flex-shrink-0 mb-7 last:mb-0 md:w-1/3 lg:w-1/4 xl:w-1/5 2xl:w-1/5 md:mb-0 md:flex-col items-center;
3 | }
4 |
5 | .progress_container:only-child {
6 | @apply md:mx-auto;
7 | }
8 |
9 | .bar {
10 | @apply absolute bg-gray-100 w-1 h-double start-1/2 -top-1/2 -ms-px md:w-full md:h-1 md:top-1/2 md:start-0 md:-mt-px;
11 | }
12 |
13 | .progress_container:only-child .bar {
14 | @apply w-1 h-full hidden md:mx-auto top-1/2 md:w-1/2 md:start-1/2 md:h-1;
15 | }
16 |
17 | .progress_container:first-child .bar {
18 | @apply w-1 h-full top-1/2 md:w-1/2 md:start-1/2 md:h-1;
19 | }
20 |
21 | .progress_container:last-child .bar {
22 | @apply w-1 h-full -top-1/2 md:w-1/2 md:h-1 md:top-1/2;
23 | }
24 |
25 | .progress_wrapper {
26 | @apply relative flex items-center justify-center md:w-full md:mb-4;
27 | }
28 |
29 | .checked {
30 | }
31 |
32 | .progress_wrapper.checked .bar {
33 | @apply bg-accent;
34 | }
35 |
36 | .status_wrapper {
37 | @apply text-sm font-bold text-accent w-9 h-9 flex items-center justify-center rounded-full bg-light border border-dashed border-accent z-10;
38 | }
39 |
40 | .progress_wrapper.checked .status_wrapper {
41 | @apply bg-accent text-light;
42 | }
43 |
--------------------------------------------------------------------------------
/components/ui/radio/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { InputHTMLAttributes } from 'react';
2 |
3 | import styles from './radio.module.css';
4 |
5 | export interface Props extends InputHTMLAttributes {
6 | className?: string;
7 | label?: string;
8 | name: string;
9 | id: string;
10 | error?: string;
11 | }
12 |
13 | const Radio = React.forwardRef(
14 | ({ style, className, label, name, id, error, ...rest }, ref) => {
15 | return (
16 |
17 |
18 |
26 |
27 |
30 |
31 |
32 | {error &&
{error}
}
33 |
34 | );
35 | }
36 | );
37 |
38 | Radio.displayName = 'Radio';
39 |
40 | export default Radio;
41 |
--------------------------------------------------------------------------------
/components/ui/scrollbar.tsx:
--------------------------------------------------------------------------------
1 | import 'overlayscrollbars/overlayscrollbars.css';
2 |
3 | import cn from 'classnames';
4 | import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
5 |
6 | type ScrollbarProps = {
7 | options?: any;
8 | children: React.ReactNode;
9 | style?: React.CSSProperties;
10 | className?: string;
11 | };
12 |
13 | const Scrollbar: React.FC = ({
14 | options,
15 | children,
16 | style,
17 | className,
18 | ...props
19 | }) => {
20 | return (
21 |
32 | {children}
33 |
34 | );
35 | };
36 |
37 | export default Scrollbar;
38 |
--------------------------------------------------------------------------------
/components/ui/select/select.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactSelect, { Props } from 'react-select';
3 |
4 | import { selectStyles } from './select.styles';
5 |
6 | export type Ref = any;
7 |
8 | export const Select = React.forwardRef[((props, ref) => (
9 |
10 | ));
11 |
12 | Select.displayName = 'Select';
13 |
14 | export default Select;
15 |
--------------------------------------------------------------------------------
/components/ui/switch-input.tsx:
--------------------------------------------------------------------------------
1 | import { Control, Controller, FieldErrors } from 'react-hook-form';
2 | import { Switch } from '@headlessui/react';
3 | import ValidationError from './form-validation-error';
4 | import { useTranslation } from 'next-i18next';
5 |
6 | interface Props {
7 | control: Control;
8 | errors: FieldErrors;
9 | label: string;
10 | name: string;
11 | }
12 |
13 | const SwitchInput = ({ control, label, name, errors }: Props) => {
14 | const { t } = useTranslation();
15 | return (
16 | ]
17 |
{label}
18 |
(
22 |
29 | Enable {label}
30 |
35 |
36 | )}
37 | />
38 |
39 |
40 | );
41 | };
42 |
43 | export default SwitchInput;
44 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import 'rc-table/assets/index.css';
2 | export { default as Table } from 'rc-table';
3 |
--------------------------------------------------------------------------------
/components/ui/title.tsx:
--------------------------------------------------------------------------------
1 | import cn from 'classnames';
2 | import { LabelHTMLAttributes } from 'react';
3 |
4 | export interface Props extends LabelHTMLAttributes {
5 | className?: string;
6 | }
7 |
8 | const Title: React.FC = ({ className = 'mb-3', ...rest }) => {
9 | return (
10 |
17 | );
18 | };
19 |
20 | export default Title;
21 |
--------------------------------------------------------------------------------
/components/ui/truncate-scroll.tsx:
--------------------------------------------------------------------------------
1 | type ReadMoreProps = {
2 | onClick?: (event: React.MouseEvent) => void;
3 | buttonText?: string;
4 | character: number;
5 | children: string;
6 | hideButton?: boolean;
7 | };
8 |
9 | const Truncate: React.FC = ({
10 | children,
11 | onClick,
12 | character = 200,
13 | buttonText = 'See More',
14 | hideButton = false
15 | }) => {
16 | if (!children) return null;
17 |
18 | return (
19 | <>
20 | {children && children.length < character
21 | ? children
22 | : children.substring(0, character) + '...'}
23 | {!hideButton && children.length > character && (
24 | <>
25 | ...
26 |
32 | >
33 | )}
34 | >
35 | );
36 | };
37 |
38 | export default Truncate;
39 |
--------------------------------------------------------------------------------
/contexts/settings.context.tsx:
--------------------------------------------------------------------------------
1 | import { Settings } from '@ts-types/generated';
2 | import React, { Dispatch, SetStateAction, useMemo } from 'react';
3 | export interface State extends Settings {
4 | updateSettings: Dispatch>;
5 | }
6 |
7 | const initialState = {
8 | currency: {
9 | code: 'USD',
10 | symbol: '$'
11 | },
12 | seo: {},
13 | socials: {},
14 | updateSettings: () => undefined
15 | };
16 |
17 | export const SettingsContext = React.createContext(initialState);
18 |
19 | SettingsContext.displayName = 'SettingsContext';
20 |
21 | export const SettingsProvider: React.FC = ({ ...props }) => {
22 | const [state, updateSettings] = React.useState(initialState);
23 | const value = useMemo(
24 | () => ({
25 | ...state,
26 | updateSettings
27 | }),
28 | [state]
29 | );
30 | return ;
31 | };
32 |
33 | export const useSettings = () => {
34 | const context = React.useContext(SettingsContext);
35 | if (context === undefined) {
36 | throw new Error(`useSettings must be used within a SettingsProvider`);
37 | }
38 | return context;
39 | };
40 |
--------------------------------------------------------------------------------
/contexts/staff.context.tsx:
--------------------------------------------------------------------------------
1 | import type { StaffType } from '@ts-types/generated';
2 | import React, { Dispatch, SetStateAction, useState } from 'react';
3 |
4 | export interface State {
5 | staffInfo: StaffType;
6 | setStaffInfo: Dispatch>;
7 | }
8 |
9 | const initialState = {
10 | staffInfo: {} as StaffType,
11 | setStaffInfo: () => undefined
12 | };
13 |
14 | export const StaffInfoContext = React.createContext(initialState);
15 |
16 | StaffInfoContext.displayName = 'StaffInfoContext';
17 |
18 | export const StaffInfoProvider: React.FC = ({ ...props }) => {
19 | const [staffInfo, setStaffInfo] = useState({} as StaffType);
20 | return (
21 |
22 | );
23 | };
24 |
--------------------------------------------------------------------------------
/contexts/time.context.tsx:
--------------------------------------------------------------------------------
1 | import React, { Dispatch, SetStateAction, useState } from 'react';
2 |
3 | export interface State {
4 | current: number;
5 | setTime: Dispatch>;
6 | }
7 |
8 | const initialState = {
9 | current: Date.now(),
10 | setTime: () => undefined
11 | };
12 |
13 | export const TimeCacheContext = React.createContext(initialState);
14 |
15 | TimeCacheContext.displayName = 'TimeCacheContext';
16 |
17 | export const TimeCacheProvider: React.FC = ({ ...props }) => {
18 | const [{ current }, setTime] = useState<{ current: number }>({
19 | current: Date.now()
20 | });
21 | return ;
22 | };
23 |
--------------------------------------------------------------------------------
/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export { useErrorLogger } from './useErrorLogger';
2 | export { useGetStaff } from './useGetStaff';
3 | export { useId } from './useId';
4 | export { useLocalStorage } from './useLocalStorage';
5 | export { useMediaQuery } from './useMediaQuery';
6 | export { useWarnIfUnsavedChanges } from './useWarnIfUnsavedChanges';
7 |
--------------------------------------------------------------------------------
/hooks/use-price.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | export function usePrice({
4 | amount,
5 | currencyCode,
6 | locale
7 | }: {
8 | amount: number;
9 | currencyCode: string;
10 | locale: string;
11 | }) {
12 | const formatCurrency = useMemo(
13 | () =>
14 | new Intl.NumberFormat(locale, {
15 | style: 'currency',
16 | currency: currencyCode
17 | }),
18 | [locale, currencyCode]
19 | );
20 |
21 | return formatCurrency.format(amount);
22 | }
23 |
--------------------------------------------------------------------------------
/hooks/useErrorLogger.ts:
--------------------------------------------------------------------------------
1 | import isEmpty from 'lodash/isEmpty';
2 | import { useEffect } from 'react';
3 |
4 | export function useErrorLogger(error?: any) {
5 | useEffect(() => {
6 | if (!isEmpty(error)) {
7 | console.log({ error });
8 | }
9 | }, [error]);
10 | }
11 |
--------------------------------------------------------------------------------
/hooks/useId.tsx:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid';
2 | import { useEffect, useState } from 'react';
3 |
4 | export function useId() {
5 | const [id, setId] = useState(null);
6 |
7 | useEffect(() => {
8 | if (!id) setId(nanoid());
9 | }, []);
10 |
11 | return id;
12 | }
13 |
--------------------------------------------------------------------------------
/hooks/useLocalStorage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export function useLocalStorage(key: string, initialValue) {
4 | const [StoredValue, setStoredValue] = useState(initialValue);
5 |
6 | const setLocalStorage = (value) => {
7 | try {
8 | setStoredValue(value);
9 | window?.localStorage.setItem(key, JSON.stringify(value));
10 | } catch (error) {
11 | console.log(error);
12 | }
13 | };
14 |
15 | useEffect(() => {
16 | try {
17 | const item = window?.localStorage.getItem(key);
18 | if (item) setStoredValue(JSON.parse(item));
19 | } catch (error) {
20 | console.log(error);
21 | }
22 | }, [key]);
23 |
24 | return [StoredValue, setLocalStorage];
25 | }
26 |
--------------------------------------------------------------------------------
/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export const useMediaQuery = (query: string, value: number) => {
4 | const [mediaQueryMatches, setMediaQueryMatches] = useState(false);
5 |
6 | useEffect(() => {
7 | setMediaQueryMatches(window.innerWidth <= value);
8 | let mql = window.matchMedia(`(${query}: ${value}px)`);
9 | mql.addEventListener('change', (e) => handler(e));
10 | const handler = (e) => {
11 | if (e.matches) setMediaQueryMatches(true);
12 | else setMediaQueryMatches(false);
13 | };
14 | return () => mql.removeEventListener('change', (e) => handler(e));
15 | }, [query, value]);
16 |
17 | return mediaQueryMatches;
18 | };
19 |
--------------------------------------------------------------------------------
/hooks/useTime.ts:
--------------------------------------------------------------------------------
1 | import { TimeCacheContext } from '@contexts/time.context';
2 | import { useContext } from 'react';
3 |
4 | export function useTime() {
5 | const { current, setTime } = useContext(TimeCacheContext);
6 | const revalidate = () => {
7 | setTime({ current: Date.now() });
8 | };
9 | return { current, revalidate };
10 | }
11 |
--------------------------------------------------------------------------------
/hooks/useWarnIfUnsavedChanges.ts:
--------------------------------------------------------------------------------
1 | import Router from 'next/router';
2 | import { useEffect } from 'react';
3 |
4 | export const useWarnIfUnsavedChanges = (
5 | unsavedChanges: boolean,
6 | callback: () => boolean
7 | ) => {
8 | useEffect(() => {
9 | const routeChangeStart = () => {
10 | if (unsavedChanges) {
11 | const ok = callback();
12 | if (!ok) {
13 | Router.events.emit('routeChangeError');
14 | // INFO ignore this error with sentry
15 | throw 'Abort route change. Please ignore this error.';
16 | }
17 | }
18 | };
19 | Router.events.on('routeChangeStart', routeChangeStart);
20 | return () => {
21 | Router.events.off('routeChangeStart', routeChangeStart);
22 | };
23 | }, [unsavedChanges]);
24 | };
25 |
--------------------------------------------------------------------------------
/hooks/use_percent-decrease.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 |
3 | export const usePercentDecrease = ({
4 | comparePrice,
5 | salePrice
6 | }: {
7 | comparePrice: number;
8 | salePrice: number;
9 | }) => {
10 | const percentDecrease = useMemo(
11 | () => -(((comparePrice - salePrice) / comparePrice) * 100),
12 | [comparePrice, salePrice]
13 | );
14 |
15 | return `${percentDecrease?.toFixed(0)}%`;
16 | };
17 |
--------------------------------------------------------------------------------
/jest.setup.js:
--------------------------------------------------------------------------------
1 | // Optional: configure or set up a testing framework before each test.
2 | // If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
3 |
4 | // Used for __tests__/testing-library.js
5 | // Learn more: https://github.com/testing-library/jest-dom
6 | import '@testing-library/jest-dom/extend-expect';
7 |
--------------------------------------------------------------------------------
/lib/S3/Date.ts:
--------------------------------------------------------------------------------
1 | import { DateISOString, DateYMD, XAmzDate } from './types';
2 |
3 | export const dateISOString: DateISOString = new Date(
4 | +new Date() + 864e5
5 | ).toISOString();
6 | export const xAmzDate: XAmzDate = dateISOString
7 | .split('-')
8 | .join('')
9 | .split(':')
10 | .join('')
11 | .split('.')
12 | .join('');
13 | export const dateYMD: DateYMD = dateISOString.split('T')[0].split('-').join('');
14 |
--------------------------------------------------------------------------------
/lib/S3/ErrorThrower.ts:
--------------------------------------------------------------------------------
1 | import { IConfig } from './types';
2 |
3 | export const throwError = (config: IConfig, file: File) => {
4 | if (config.bucketName === null || config.bucketName === '') {
5 | throw new Error(`Your bucketName cannot be empty `);
6 | }
7 | if (config.region === null || config.region === '') {
8 | throw new Error(`Must provide a valide region in order to use your bucket`);
9 | }
10 | if (config.accessKeyId === null || config.accessKeyId === '') {
11 | throw new Error(`Must provide accessKeyId`);
12 | }
13 | if (config.secretAccessKey === null || config.secretAccessKey === '') {
14 | throw new Error(`Must provide secretAccessKey`);
15 | }
16 | if (!file) {
17 | throw new Error(`File cannot be empty`);
18 | }
19 | };
20 |
--------------------------------------------------------------------------------
/lib/S3/Policy.ts:
--------------------------------------------------------------------------------
1 | import { dateISOString, dateYMD, xAmzDate } from './Date';
2 | import { IConfig, Policy as PolicyType } from './types';
3 |
4 | export default class Policy {
5 | public static getPolicy(config: IConfig): string {
6 | const policy = (): PolicyType => {
7 | return {
8 | expiration: dateISOString,
9 | conditions: [
10 | { acl: 'public-read' },
11 | { bucket: config.bucketName },
12 | [
13 | 'starts-with',
14 | '$key',
15 | `${config.dirName ? config.dirName + '/' : ''}`
16 | ],
17 | ['starts-with', '$Content-Type', ''],
18 | ['starts-with', '$x-amz-meta-tag', ''],
19 | { 'x-amz-algorithm': 'AWS4-HMAC-SHA256' },
20 | {
21 | 'x-amz-credential': `${config.accessKeyId}/${dateYMD}/${config.region}/s3/aws4_request`
22 | },
23 | { 'x-amz-date': xAmzDate },
24 | { 'x-amz-meta-uuid': '14365123651274' },
25 | { 'x-amz-server-side-encryption': 'AES256' }
26 | ]
27 | };
28 | };
29 | //Returns a base64 policy;
30 | return new Buffer(JSON.stringify(policy()))
31 | .toString('base64')
32 | .replace(/\n|\r/, '');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/S3/Signature.ts:
--------------------------------------------------------------------------------
1 | import Crypto, { WordArray } from 'crypto-js';
2 |
3 | import { DateYMD, IConfig } from './types';
4 |
5 | export default class Signature {
6 | public static getSignature(
7 | config: IConfig,
8 | date: DateYMD,
9 | policyBase64: string
10 | ): string {
11 | const getSignatureKey = (
12 | key: string,
13 | dateStamp: DateYMD,
14 | regionName: string
15 | ): WordArray => {
16 | const kDate: WordArray = Crypto.HmacSHA256(dateStamp, 'AWS4' + key);
17 | const kRegion: WordArray = Crypto.HmacSHA256(regionName, kDate);
18 | const kService: WordArray = Crypto.HmacSHA256('s3', kRegion);
19 | const kSigning: WordArray = Crypto.HmacSHA256('aws4_request', kService);
20 | return kSigning;
21 | };
22 | const signature = (policyEncoded: string): string => {
23 | return Crypto.HmacSHA256(
24 | policyEncoded,
25 | getSignatureKey(config.secretAccessKey, date, config.region)
26 | ).toString(Crypto.enc.Hex);
27 | };
28 | return signature(policyBase64);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/S3/types.ts:
--------------------------------------------------------------------------------
1 | export type DateISOString = string; // "2019-01-27T11:07:43.102Z"
2 | export type XAmzDate = string; // "20190127T110743102Z"
3 | export type DateYMD = string; // "20190127"
4 |
5 | export interface IConfig {
6 | bucketName: string;
7 | dirName?: string;
8 | region: string;
9 | accessKeyId: string;
10 | secretAccessKey: string;
11 | s3Url?: string;
12 | }
13 |
14 | type GenericType = {
15 | [key: string]: string;
16 | };
17 |
18 | type Conditions = [
19 | GenericType,
20 | GenericType,
21 | string[],
22 | string[],
23 | string[],
24 | GenericType,
25 | GenericType,
26 | GenericType,
27 | GenericType,
28 | GenericType
29 | ];
30 |
31 | export type Policy = {
32 | conditions: Conditions;
33 | expiration: DateISOString;
34 | };
35 |
36 | export type UploadResponse = {
37 | bucket: string;
38 | key: string;
39 | location: string;
40 | status: number;
41 | };
42 |
--------------------------------------------------------------------------------
/lib/conn.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import path from 'path';
3 | import ServerlessClient from 'serverless-postgres';
4 |
5 | const databaseConn = new ServerlessClient({
6 | host: process.env.DATABASE_END_POINT,
7 | port: Number(process.env.PORT),
8 | database: process.env.POSTGRES_DB,
9 | user: process.env.POSTGRES_USER,
10 | password: process.env.POSTGRES_PASSWORD,
11 | ssl: {
12 | rejectUnauthorized: false,
13 | ca: fs
14 | .readFileSync(path.join(process.cwd(), 'lib', 'ca-certificate.crt'))
15 | .toString()
16 | },
17 | delayMs: 3000
18 | });
19 |
20 | export default databaseConn;
21 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export { notify } from './notify';
2 | export { sentry } from './sentry';
3 |
--------------------------------------------------------------------------------
/lib/sentry.ts:
--------------------------------------------------------------------------------
1 | interface LogsType {
2 | message: string | null;
3 | error: Error | string;
4 | }
5 |
6 | export function sentry({ message, error }: LogsType) {
7 | console.error(`<==: ${message} :==>`);
8 | if (message === 'graphQLClient.request') {
9 | console.error(JSON.stringify(error, undefined, 2));
10 | } else {
11 | console.error(error);
12 | }
13 | console.error(`<==:============:==>`);
14 | return;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/sql/dashboard.sql.ts:
--------------------------------------------------------------------------------
1 | export function getTodaysRevenue(): string {
2 | return `SELECT SUM((SELECT SUM("unit_price"*"quantity")::FLOAT FROM order_items WHERE order_id = o.id)::FLOAT)
3 | FROM orders AS o WHERE o.order_status = 'complete' AND DATE(o.created_at) BETWEEN NOW() - INTERVAL '24 HOURS' AND NOW()`;
4 | }
5 |
6 | export function getTotal30DaysRevenue(): string {
7 | return `SELECT SUM((SELECT SUM("unit_price"*"quantity")::FLOAT FROM order_items WHERE order_id = o.id)::FLOAT)
8 | FROM orders AS o WHERE o.order_status = 'complete' AND o.created_at >= date_trunc('month', current_date - interval '1' month) AND o.created_at <= now()`;
9 | }
10 |
11 | export function getTotalOrders(): string {
12 | return `SELECT count(id)::INTEGER FROM orders o WHERE created_at >= date_trunc('month', current_date - interval '1' month) AND created_at <= now()
13 | `;
14 | }
15 |
16 | export function getSalesHistory(): string {
17 | return `SELECT (SELECT COUNT(o.id) FROM orders as o WHERE EXTRACT(MONTH FROM o.created_at) = EXTRACT(MONTH FROM f) AND EXTRACT(YEAR FROM o.created_at) = EXTRACT(YEAR FROM f)) as sales, f as dt from
18 | generate_series( (select(now() - interval '1 year'))::date, now()::date, '1 month'::interval) as f`;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/sql/index.ts:
--------------------------------------------------------------------------------
1 | export * as attributeQueries from './attribute.sql';
2 | export * as carouselQueries from './carousel.sql';
3 | export * as categoryQueries from './category.sql';
4 | export * as couponQueries from './coupon.sql';
5 | export * as dashboardQueries from './dashboard.sql';
6 | export * as loginQueries from './login.sql';
7 | export * as orderQueries from './order.sql';
8 | export * as orderStatusQueries from './orderStatus.sql';
9 | export * as productQueries from './product.sql';
10 | export * as settingsQueries from './settings.sql';
11 | export * as shippingQueries from './shipping.sql';
12 | export * as staffQueries from './staff.sql';
13 | export * as supplierQueries from './supplier.sql';
14 | export * as tagQueries from './tag.sql';
15 |
--------------------------------------------------------------------------------
/lib/sql/login.sql.ts:
--------------------------------------------------------------------------------
1 | export function staffLogin(): string {
2 | return `SELECT id, email, password_hash AS "passwordHash", active FROM staff_accounts WHERE email = $1`;
3 | }
4 |
5 | export function staff(): string {
6 | return `SELECT id, active, is_admin as "isAdmin" FROM staff_accounts WHERE id = $1`;
7 | }
8 |
--------------------------------------------------------------------------------
/lib/sql/settings.sql.ts:
--------------------------------------------------------------------------------
1 | export function getSettings(): string {
2 | return `SELECT (SELECT json_build_object('image', logo_image_path)) AS "logo", (SELECT json_build_object('image', favicon_image_path)) AS "favicon", currency, socials,
3 | store_name AS "storeName", store_email AS "storeEmail", store_number AS "storeNumber",
4 | max_checkout_quantity AS "maxCheckoutQuantity",
5 | (SELECT json_build_object(
6 | 'metaTitle', meta_title, 'metaDescription', meta_description,
7 | 'metaTags', meta_tags, 'ogTitle', og_title,
8 | 'ogDescription', og_description, 'ogImage', (SELECT json_build_object('image', og_image_path)),
9 | 'twitterHandle', twitter_handle)) AS "seo" FROM settings WHERE id = 'store'`;
10 | }
11 |
12 | export function updateSettings(): string {
13 | return `UPDATE settings SET logo_image_path = $1, favicon_image_path = $2, currency = $3,
14 | meta_title = $4, meta_description = $5, meta_tags = $6, og_title = $7,
15 | og_description = $8, og_image_path = $9, twitter_handle = $10, socials = $11, max_checkout_quantity = $12,
16 | store_email = $13, store_name = $14, store_number = $15 WHERE id = 'store' RETURNING id`;
17 | }
18 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | // import { CookieNames } from '@ts-types/enums';
2 | import type { NextRequest } from 'next/server';
3 | import { NextResponse } from 'next/server';
4 |
5 | export async function middleware(req: NextRequest) {
6 | // const url = req.nextUrl;
7 | // const staff_token = req.cookies[CookieNames.STAFF_TOKEN_NAME];
8 |
9 | // if (
10 | // !staff_token &&
11 | // url.pathname !== '/login' &&
12 | // url.pathname !== '/shop.jpg' &&
13 | // url.pathname.indexOf('/favicons/') === -1 &&
14 | // url.pathname !== '/logo.svg' &&
15 | // url.pathname !== '/robots.txt' &&
16 | // url.pathname !== '/manifest.json' &&
17 | // url.pathname !== '/sw.js' &&
18 | // url.pathname !== '/workbox-4a677df8.js'
19 | // ) {
20 | // return NextResponse.redirect('/login');
21 | // }
22 | return NextResponse.next();
23 | }
24 |
--------------------------------------------------------------------------------
/middleware/jwtRS256.key.pub:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnfkZsavcOjyk4j9svXqH
3 | TLrvFHXgYCL/gcNAkwRbLrydz6m6RFmTOrwvx1G5t+QO0tF/oojnYAxhzP6PLEGO
4 | 5YShB0w5UR2cRIGRJYn6EDAFjgskUU6ZCxUjc43++IQlqy9OAA9YBvS9aiC3/+K2
5 | cyoul1IFiom/CmAiLCFE2MUBYXaHdy+12KmIA0gtKOOuYyiqk2uZxM+pHdirMCF0
6 | ftEOX1ZTZVNbxG5jgcrjE8UtbHriHqUNYFQcwpaUkvkK07w7jdzfxjoOCzRV+Ujt
7 | EfkhTHsrSd5Q9+wEX7vmZuhNNjS2z+Tzj+bmcWk59a707ceIDWM1SsbW0fI9OvUO
8 | LTIxJnvytxVdhBt0hfor3TEVKKeo+62uj76JE43wzq0TIANpdxl5w2rt0iM+XrFx
9 | bL5m3pcUqHeYHOwaqb/4Uyn/ru3M+OBq0+WO9Av411wcZpd69E5HYtIga7su8BMR
10 | O4lhse46AwEiWWa2eBroHgaWYo0hK2Wz+srClXxw8868kv3HVjnDXng0vnOzfWLz
11 | R2Uur499aab7J9gAOu3pYFm8ELSzBfxG5+/1ajq9hjymIi4l5oseE6vJRSXoANGe
12 | 2Cx7frPuluUVqckN6Ubs3PgKgFTzmx60U68/GKh06nSQW7cgeEWto5t+JwAPAmcw
13 | t4rWeA3O8nhD7ZPauT4CqrECAwEAAQ==
14 | -----END PUBLIC KEY-----
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/next-i18next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | i18n: {
5 | locales: ['en', 'ar', 'fr'],
6 | defaultLocale: 'en'
7 | // localeDetection: false,
8 | },
9 | localePath: path.resolve('./public/locales')
10 | };
11 |
--------------------------------------------------------------------------------
/pages/_document.tsx:
--------------------------------------------------------------------------------
1 | import Document, {
2 | DocumentContext,
3 | Head,
4 | Html,
5 | Main,
6 | NextScript
7 | } from 'next/document';
8 | import { i18n } from 'next-i18next';
9 |
10 | export default class CustomDocument extends Document {
11 | static async getInitialProps(ctx: DocumentContext) {
12 | return Document.getInitialProps(ctx);
13 | }
14 | render() {
15 | const { locale } = this.props.__NEXT_DATA__;
16 | const dir = locale === 'ar' ? 'rtl' : 'ltr';
17 | if (process.env.NODE_ENV !== 'production') {
18 | i18n?.reloadResources(locale);
19 | }
20 |
21 | return (
22 |
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/pages/admin/dashboard.tsx:
--------------------------------------------------------------------------------
1 | import AppLayout from '@components/layouts/app';
2 | import { useGetStaff } from '@hooks/index';
3 | import { verifyAuth } from '@middleware/utils';
4 | import { SSRProps } from '@ts-types/custom.types';
5 | import { ROUTES } from '@utils/routes';
6 | import type { GetServerSideProps } from 'next';
7 | import dynamic from 'next/dynamic';
8 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
9 |
10 | const AdminDashboard = dynamic(() => import('@components/dashboard'));
11 |
12 | export default function Dashboard({ client }: SSRProps) {
13 | useGetStaff(client);
14 | return ;
15 | }
16 |
17 | Dashboard.Layout = AppLayout;
18 |
19 | export const getServerSideProps: GetServerSideProps = async (context) => {
20 | const { locale } = context;
21 | const { client } = verifyAuth(context);
22 |
23 | if (!client) {
24 | return {
25 | redirect: {
26 | permanent: false,
27 | destination: ROUTES.LOGIN
28 | }
29 | };
30 | }
31 |
32 | return {
33 | props: {
34 | ...(await serverSideTranslations(locale!, [
35 | 'common',
36 | 'table',
37 | 'widgets'
38 | ])),
39 | client
40 | }
41 | };
42 | };
43 |
--------------------------------------------------------------------------------
/pages/admin/logout.tsx:
--------------------------------------------------------------------------------
1 | import Loader from '@components/ui/loader/loader';
2 | // import { useLogoutMutation } from "@data/user/use-logout.mutation";
3 | import { useTranslation } from 'next-i18next';
4 | import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
5 | import { useEffect } from 'react';
6 |
7 | function SignOut() {
8 | const { t } = useTranslation();
9 | // const { mutate: logout } = useLogoutMutation();
10 |
11 | useEffect(() => {
12 | // logout();
13 | }, []);
14 |
15 | return ;
16 | }
17 |
18 | export default SignOut;
19 |
20 | export const getStaticProps = async ({ locale }: any) => ({
21 | props: {
22 | ...(await serverSideTranslations(locale, ['common']))
23 | }
24 | });
25 |
--------------------------------------------------------------------------------
/pages/api/admin/attribute/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { attributeQueries } from '@lib/sql';
3 | import { Category } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { query, method } = req;
13 | const id = query.id as string;
14 | try {
15 | switch (method) {
16 | case this.GET: {
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 |
20 | const { rows } = await client.query(
21 | attributeQueries.getAttributeForAdmin(),
22 | [id]
23 | );
24 | return { attribute: rows[0] };
25 | });
26 | return res.status(200).json(results);
27 | }
28 | default:
29 | res.setHeader('Allow', ['GET']);
30 | res.status(405).end(`There was some error!`);
31 | }
32 | } catch (error) {
33 | return res.status(500).json({
34 | error: {
35 | type: this.ErrorNames.SERVER_ERROR,
36 | message: error?.message,
37 | from: 'attribute'
38 | }
39 | });
40 | }
41 | };
42 | }
43 |
44 | export default new Handler().execute;
45 |
--------------------------------------------------------------------------------
/pages/api/admin/banner/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { carouselQueries } from '@lib/sql';
3 | import { HeroCarouselType } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { query, method } = req;
13 | const id = query.id as string;
14 | try {
15 | switch (method) {
16 | case this.GET: {
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 | const { rows } = await client.query(
20 | carouselQueries.getHeroSlideForAdmin(),
21 | [id]
22 | );
23 | return { banner: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['GET']);
29 | res.status(405).end(`There was some error!`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'banner'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/banner/delete.ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { carouselQueries } from '@lib/sql';
3 | import { HeroCarouselType } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { method, body } = req;
13 | try {
14 | switch (method) {
15 | case this.POST: {
16 | const { id } = body;
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 | const { rows } = await client.query(
20 | carouselQueries.deleteSlide(),
21 | [id]
22 | );
23 | return { banner: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['POST']);
29 | res.status(405).end(`There was some error`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'deleteBanner'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/category/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { categoryQueries } from '@lib/sql';
3 | import { Category } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { query, method } = req;
13 | const id = query.id as string;
14 | try {
15 | switch (method) {
16 | case this.GET: {
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 | const { rows } = await client.query(
20 | categoryQueries.getCategoryForAdmin(),
21 | [id]
22 | );
23 | return { category: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['GET']);
29 | res.status(405).end(`There was some error!`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'category'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/order/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { orderQueries } from '@lib/sql';
3 | import type { NextApiRequest, NextApiResponse } from 'next';
4 |
5 | class Handler extends PostgresClient {
6 | constructor() {
7 | super();
8 | }
9 |
10 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
11 | const { query, method } = req;
12 | const id = query.id as string;
13 | try {
14 | switch (method) {
15 | case this.GET: {
16 | const results = await this.tx(async (client) => {
17 | await this.authorization(client, req, res);
18 | const { rows } = await client.query(
19 | orderQueries.getOrder(),
20 | [id]
21 | );
22 | return { order: rows[0] };
23 | });
24 | return res.status(200).json(results);
25 | }
26 | default:
27 | res.setHeader('Allow', ['GET']);
28 | res.status(405).end(`There was some error!`);
29 | }
30 | } catch (error) {
31 | return res.status(500).json({
32 | error: {
33 | type: this.ErrorNames.SERVER_ERROR,
34 | message: error?.message,
35 | from: 'order'
36 | }
37 | });
38 | }
39 | };
40 | }
41 |
42 | export default new Handler().execute;
43 |
--------------------------------------------------------------------------------
/pages/api/admin/order/update-status.ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { orderQueries } from '@lib/sql';
3 | import type { NextApiRequest, NextApiResponse } from 'next';
4 |
5 | class Handler extends PostgresClient {
6 | constructor() {
7 | super();
8 | }
9 |
10 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
11 | const { method, body } = req;
12 | try {
13 | switch (method) {
14 | case this.POST: {
15 | const { id, order_status = 'pending' } = body;
16 | const results = await this.tx(async (client) => {
17 | await this.authorization(client, req, res);
18 | const { rows } = await client.query(
19 | orderQueries.updateOrderStatus(),
20 | [id, order_status]
21 | );
22 | return { order: rows[0] };
23 | });
24 | return res.status(200).json(results);
25 | }
26 | default:
27 | res.setHeader('Allow', ['POST']);
28 | res.status(405).end(`There was some error!`);
29 | }
30 | } catch (error) {
31 | console.log(error);
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'order-status'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/product/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { productQueries } from '@lib/sql';
3 | import { Category } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { query, method } = req;
13 | const id = query.id as string;
14 | try {
15 | switch (method) {
16 | case this.GET: {
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 | const { rows } = await client.query(
20 | productQueries.getProductForAdmin(),
21 | [id]
22 | );
23 | return { product: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['GET']);
29 | res.status(405).end(`There was some error!`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'product'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/settings/index.ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { settingsQueries } from '@lib/sql';
3 | import { Settings } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { method } = req;
13 | try {
14 | switch (method) {
15 | case this.GET: {
16 | const results = await this.tx(async (client) => {
17 | await this.authorization(client, req, res);
18 | const { rows } = await client.query(
19 | settingsQueries.getSettings(),
20 | []
21 | );
22 | return { settings: rows[0] };
23 | });
24 | return res.status(200).json(results);
25 | }
26 | default:
27 | res.setHeader('Allow', ['GET']);
28 | res.status(405).end(`There was some error!`);
29 | }
30 | } catch (error) {
31 | return res.status(500).json({
32 | error: {
33 | type: this.ErrorNames.SERVER_ERROR,
34 | message: error?.message,
35 | from: 'Settings'
36 | }
37 | });
38 | }
39 | };
40 | }
41 |
42 | export default new Handler().execute;
43 |
--------------------------------------------------------------------------------
/pages/api/admin/staff/[id].ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { staffQueries } from '@lib/sql';
3 | import { StaffType } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { query, method } = req;
13 | const id = query.id as string;
14 | try {
15 | switch (method) {
16 | case this.GET: {
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res);
19 | const { rows } = await client.query(
20 | staffQueries.getStaff(),
21 | [id]
22 | );
23 | return { staff: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['GET']);
29 | res.status(405).end(`There was some error!`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'staff'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/api/admin/staff/delete.ts:
--------------------------------------------------------------------------------
1 | import PostgresClient from '@lib/database';
2 | import { staffQueries } from '@lib/sql';
3 | import { StaffType } from '@ts-types/generated';
4 | import type { NextApiRequest, NextApiResponse } from 'next';
5 |
6 | class Handler extends PostgresClient {
7 | constructor() {
8 | super();
9 | }
10 |
11 | execute = async (req: NextApiRequest, res: NextApiResponse) => {
12 | const { method, body } = req;
13 | try {
14 | switch (method) {
15 | case this.POST: {
16 | const { id } = body;
17 | const results = await this.tx(async (client) => {
18 | await this.authorization(client, req, res, true);
19 | const { rows } = await client.query(
20 | staffQueries.deleteStaff(),
21 | [id]
22 | );
23 | return { staff: rows[0] };
24 | });
25 | return res.status(200).json(results);
26 | }
27 | default:
28 | res.setHeader('Allow', ['POST']);
29 | res.status(405).end(`There was some error`);
30 | }
31 | } catch (error) {
32 | return res.status(500).json({
33 | error: {
34 | type: this.ErrorNames.SERVER_ERROR,
35 | message: error?.message,
36 | from: 'deleteStaff'
37 | }
38 | });
39 | }
40 | };
41 | }
42 |
43 | export default new Handler().execute;
44 |
--------------------------------------------------------------------------------
/pages/terms.tsx:
--------------------------------------------------------------------------------
1 | import Layout from '@store/containers/layout/layout';
2 | import TermsPageContent from '@store/containers/term/terms';
3 | import Head from 'next/head';
4 |
5 | export default function FAQ() {
6 | return (
7 |
8 |
9 |
13 |
14 | Terms & Condition
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/public/arrow-next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/arrow-previous.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/category-placeholder.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/category-placeholder.jpg
--------------------------------------------------------------------------------
/public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/android-chrome-256x256.png
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #2d89ef
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/public/icons/apple-icon-180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/icons/apple-icon-180.png
--------------------------------------------------------------------------------
/public/icons/manifest-icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/icons/manifest-icon-192.png
--------------------------------------------------------------------------------
/public/icons/manifest-icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/icons/manifest-icon-512.png
--------------------------------------------------------------------------------
/public/image/card-argon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/card-argon.png
--------------------------------------------------------------------------------
/public/image/card-helium.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/card-helium.png
--------------------------------------------------------------------------------
/public/image/card-krypton.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/card-krypton.png
--------------------------------------------------------------------------------
/public/image/card-neon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/card-neon.png
--------------------------------------------------------------------------------
/public/image/card-xenon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/card-xenon.png
--------------------------------------------------------------------------------
/public/image/layout-classic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/layout-classic.png
--------------------------------------------------------------------------------
/public/image/layout-modern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/layout-modern.png
--------------------------------------------------------------------------------
/public/image/layout-standard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/image/layout-standard.png
--------------------------------------------------------------------------------
/public/locales/ar/banner.json:
--------------------------------------------------------------------------------
1 | {
2 | "heading": "توصيل البقالة في 90 دقيقة",
3 | "subheading": "احصل على الأطعمة والوجبات الخفيفة الصحية على عتبة داركم طوال اليوم كل يوم"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/ar/widgets.json:
--------------------------------------------------------------------------------
1 | {
2 | "sticker-card-title-rev": "إجمالي الإيرادات",
3 | "sticker-card-subtitle-rev": "(آخر 30 يومًا)",
4 | "sticker-card-title-order": "من أجل الكاملة",
5 | "sticker-card-subtitle-order": "(آخر 30 يومًا)",
6 | "sticker-card-title-customer": "عميل جديد",
7 | "sticker-card-subtitle-customer": "(آخر 30 يومًا)",
8 | "sticker-card-title-today-rev": "إيرادات اليوم"
9 | }
10 |
--------------------------------------------------------------------------------
/public/locales/en/banner.json:
--------------------------------------------------------------------------------
1 | {
2 | "heading": "Groceries Delivered in 90 Minute",
3 | "subheading": "Get your healthy foods & snacks delivered at your doorsteps all day everyday"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/en/error.json:
--------------------------------------------------------------------------------
1 | {
2 | "USER-EXIT": "User already exist",
3 | "EMAIL-EXIT": "Email already exist.",
4 | "SERVER-ERROR": "Server error.",
5 | "PERMISSION-DENIED": "Permission denied.",
6 | "SOMETHING-HAPPENED": "Something happened.",
7 | "UNKNOWN-ERROR": "Unknown error",
8 | "USER-NOT-ACTIVE": "User is not active",
9 | "USER-NOT-FOUND": "User does not exist",
10 | "INCORRECT-PASSWORD": "Incorrect Password",
11 | "INVALID-CSRF-TOKEN": "Invalid Token! Please refresh the page."
12 | }
13 |
--------------------------------------------------------------------------------
/public/locales/en/widgets.json:
--------------------------------------------------------------------------------
1 | {
2 | "sticker-card-title-rev": "Total Revenue",
3 | "sticker-card-subtitle-rev": "(Last 30 Days)",
4 | "sticker-card-title-order": "Total Order",
5 | "sticker-card-subtitle-order": "(Last 30 Days)",
6 | "sticker-card-title-customer": "New Customer",
7 | "sticker-card-subtitle-customer": "(Last 30 Days)",
8 | "sticker-card-title-today-rev": "Todays Revenue",
9 | "sticker-card-title-total-shops": "Total Shops"
10 | }
11 |
--------------------------------------------------------------------------------
/public/locales/fr/banner.json:
--------------------------------------------------------------------------------
1 | {
2 | "heading": "在90分鐘內交付雜貨",
3 | "subheading": "每天一整天都在您家門口提供健康食品和小吃"
4 | }
5 |
--------------------------------------------------------------------------------
/public/locales/fr/widgets.json:
--------------------------------------------------------------------------------
1 | {
2 | "sticker-card-title-rev": "總收入",
3 | "sticker-card-subtitle-rev": "(過去30天)",
4 | "sticker-card-title-order": "總訂單",
5 | "sticker-card-subtitle-order": "(過去30天)",
6 | "sticker-card-title-customer": "新客戶",
7 | "sticker-card-subtitle-customer": "(過去30天)",
8 | "sticker-card-title-today-rev": "今日收入"
9 | }
10 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "DropGala",
3 | "short_name": "DropGala",
4 | "lang": "en-US",
5 | "description": "Great place to buy items with a fair price.",
6 | "start_url": "/",
7 | "scope": "https://business.dropgala.com/",
8 | "display": "standalone",
9 | "theme_color": "#ffffff",
10 | "background_color": "#ffffff",
11 | "categories": ["shopping"],
12 | "icons": [
13 | {
14 | "src": "/favicons//android-chrome-192x192.png",
15 | "sizes": "192x192",
16 | "type": "image/png"
17 | },
18 | {
19 | "src": "/favicons//android-chrome-256x256.png",
20 | "sizes": "256x256",
21 | "type": "image/png"
22 | }
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/public/placeholders/avatar.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/placeholders/avatar.jpg
--------------------------------------------------------------------------------
/public/placeholders/avatar__placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/placeholders/avatar__placeholder.png
--------------------------------------------------------------------------------
/public/placeholders/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/placeholders/image.jpg
--------------------------------------------------------------------------------
/public/placeholders/image__placeholder.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/placeholders/image__placeholder.png
--------------------------------------------------------------------------------
/public/placeholders/no-image.svg:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /
--------------------------------------------------------------------------------
/public/shop.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/public/shop.jpg
--------------------------------------------------------------------------------
/redux/index.ts:
--------------------------------------------------------------------------------
1 | import { Action, configureStore, ThunkAction } from '@reduxjs/toolkit';
2 | import { PRODUCTION_ENV } from '@utils/utils';
3 |
4 | import cartReducer from './card/index';
5 |
6 | export function makeStore() {
7 | return configureStore({
8 | reducer: {
9 | cart: cartReducer
10 | },
11 | devTools: !PRODUCTION_ENV
12 | });
13 | }
14 |
15 | const store = makeStore();
16 |
17 | export type AppState = ReturnType;
18 |
19 | export type AppDispatch = typeof store.dispatch;
20 |
21 | export type AppThunk = ThunkAction<
22 | ReturnType,
23 | AppState,
24 | unknown,
25 | Action
26 | >;
27 |
28 | export default store;
29 |
--------------------------------------------------------------------------------
/settings/seo.settings.ts:
--------------------------------------------------------------------------------
1 | import { siteSettings } from '@settings/site.settings';
2 | export const SEO = {
3 | openGraph: {
4 | type: 'website',
5 | locale: 'en_IE',
6 | site_name: siteSettings.name
7 | },
8 | twitter: {
9 | handle: '@handle',
10 | site: '@site',
11 | cardType: 'summary_large_image'
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/store/assets/icons/arrow-left.tsx:
--------------------------------------------------------------------------------
1 | const ArrowLeft = ({
2 | color = 'currentColor',
3 | width = '19px',
4 | height = '12px',
5 | ...props
6 | }) => {
7 | return (
8 |
22 | );
23 | };
24 |
25 | export default ArrowLeft;
26 |
--------------------------------------------------------------------------------
/store/assets/icons/arrow-right.tsx:
--------------------------------------------------------------------------------
1 | const ArrowRight = ({
2 | color = 'currentColor',
3 | width = '16px',
4 | height = '13px'
5 | }) => {
6 | return (
7 |
36 | );
37 | };
38 |
39 | export default ArrowRight;
40 |
--------------------------------------------------------------------------------
/store/assets/icons/cart-icon.tsx:
--------------------------------------------------------------------------------
1 | const CartIcon = ({
2 | color = 'currentColor',
3 | width = '18px',
4 | height = '18px'
5 | }) => {
6 | return (
7 |
20 | );
21 | };
22 |
23 | export default CartIcon;
24 |
--------------------------------------------------------------------------------
/store/assets/icons/categories.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const CategoriesSvg = (props) => (
4 |
19 | );
20 |
21 | export default CategoriesSvg;
22 |
--------------------------------------------------------------------------------
/store/assets/icons/chevron-down.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const ChevronDown = (props) => (
4 |
16 | );
17 |
18 | export default ChevronDown;
19 |
--------------------------------------------------------------------------------
/store/assets/icons/chevron-left.tsx:
--------------------------------------------------------------------------------
1 | const ChevronLeft = ({
2 | color = 'currentColor',
3 | width = '8.5px',
4 | height = '14px'
5 | }) => {
6 | return (
7 |
19 | );
20 | };
21 |
22 | export default ChevronLeft;
23 |
--------------------------------------------------------------------------------
/store/assets/icons/chevron-right.tsx:
--------------------------------------------------------------------------------
1 | const ChevronRight = ({
2 | color = 'currentColor',
3 | width = '8.5px',
4 | height = '14px'
5 | }) => {
6 | return (
7 |
19 | );
20 | };
21 |
22 | export default ChevronRight;
23 |
--------------------------------------------------------------------------------
/store/assets/icons/close.tsx:
--------------------------------------------------------------------------------
1 | const CloseIcon = ({
2 | color = 'currentColor',
3 | width = '18px',
4 | height = '18px'
5 | }) => {
6 | return (
7 |
18 | );
19 | };
20 |
21 | export default CloseIcon;
22 |
--------------------------------------------------------------------------------
/store/assets/icons/home.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const HomeSvg = (props) => (
4 |
17 | );
18 |
19 | export default HomeSvg;
20 |
--------------------------------------------------------------------------------
/store/assets/icons/minus-icon.tsx:
--------------------------------------------------------------------------------
1 | const MinusIcon = ({
2 | color = 'currentColor',
3 | width = '12px',
4 | height = '2px'
5 | }) => {
6 | return (
7 |
20 | );
21 | };
22 |
23 | export default MinusIcon;
24 |
--------------------------------------------------------------------------------
/store/assets/icons/plus-icon.tsx:
--------------------------------------------------------------------------------
1 | const PlusIcon = ({
2 | color = 'currentColor',
3 | width = '12px',
4 | height = '12px'
5 | }) => {
6 | return (
7 |
22 | );
23 | };
24 |
25 | export default PlusIcon;
26 |
--------------------------------------------------------------------------------
/store/assets/icons/search-icon.tsx:
--------------------------------------------------------------------------------
1 | const SearchIcon = ({
2 | color = 'currentColor',
3 | width = '14px',
4 | height = '14px'
5 | }) => {
6 | return (
7 |
19 | );
20 | };
21 |
22 | export default SearchIcon;
23 |
--------------------------------------------------------------------------------
/store/assets/icons/success-tick.tsx:
--------------------------------------------------------------------------------
1 | const SuccessIcon = (props) => {
2 | return (
3 |
40 | );
41 | };
42 |
43 | export default SuccessIcon;
44 |
--------------------------------------------------------------------------------
/store/assets/image/fb.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/store/assets/image/git.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/store/assets/image/hero-banner-img.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/larbisahli/full-stack-ecommerce-store/eda6ac463626faf5dce0e1235419c5276a40f95f/store/assets/image/hero-banner-img.jpg
--------------------------------------------------------------------------------
/store/assets/image/inst.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/store/assets/image/link.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/store/assets/image/twt.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/store/assets/image/ytb.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/store/components/active-link.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { useRouter } from 'next/router';
3 | import React, { Children } from 'react';
4 |
5 | const ActiveLink = ({ children, activeClassName, href, ...props }: any) => {
6 | const { pathname } = useRouter();
7 | const child = Children.only(children);
8 | const childClassName = child.props.className || '';
9 |
10 | const className =
11 | pathname === href
12 | ? `${childClassName} ${activeClassName}`.trim()
13 | : childClassName;
14 |
15 | return (
16 |
17 | {React.cloneElement(child, {
18 | className: className || null
19 | })}
20 |
21 | );
22 | };
23 |
24 | export default ActiveLink;
25 |
--------------------------------------------------------------------------------
/store/components/banner.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { BannerBase, BannerContent } from './utils/theme';
3 |
4 | type BannerProps = {
5 | background: string;
6 | children: React.ReactNode;
7 | };
8 |
9 | const Banner: React.FC = ({ background, children }) => {
10 | return (
11 |
17 | );
18 | };
19 |
20 | export default Banner;
21 |
--------------------------------------------------------------------------------
/store/components/carousel/slider.tsx:
--------------------------------------------------------------------------------
1 | import 'swiper/css';
2 | import 'swiper/css/autoplay';
3 | import 'swiper/css/thumbs';
4 | export type { SwiperOptions, Swiper as SwiperType } from 'swiper';
5 | export { Autoplay, Grid, Navigation, Pagination, Thumbs } from 'swiper';
6 | export { Swiper, SwiperSlide } from 'swiper/react';
7 |
--------------------------------------------------------------------------------
/store/components/feature-block.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import {
4 | FeatureBase,
5 | FeatureContent,
6 | FeatureCounter,
7 | FeatureDetails,
8 | FeatureTitle
9 | } from './utils/theme';
10 |
11 | type FeatureBLockProps = {
12 | title: string;
13 | description: string;
14 | className?: string;
15 | counterBg?: string;
16 | counter: number;
17 | };
18 |
19 | const FeatureBLock: React.FC = ({
20 | title,
21 | description,
22 | className,
23 | counterBg,
24 | counter
25 | }) => {
26 | const classNames = FeatureBase + ' ' + className;
27 | return (
28 |
29 |
30 | {counter}
31 |
32 |
33 |
{title}
34 |
35 |
{description}
36 |
37 |
38 | );
39 | };
40 |
41 | export default FeatureBLock;
42 |
--------------------------------------------------------------------------------
/store/components/icon-button.tsx:
--------------------------------------------------------------------------------
1 | import React, { MouseEvent } from 'react';
2 |
3 | import { IconBtnBase } from './utils/theme';
4 |
5 | type Props = {
6 | className?: string;
7 | children: React.ReactNode | undefined;
8 | disabled?: boolean;
9 | onClick?: React.MouseEventHandler;
10 | };
11 |
12 | const defaultProps = {
13 | className: '',
14 | disabled: false
15 | };
16 |
17 | type NativeAttrs = Omit, keyof Props>;
18 |
19 | export type IconButtonProps = Props & NativeAttrs;
20 |
21 | const IconButton: React.FC = ({
22 | className,
23 | children,
24 | disabled,
25 | onClick
26 | }) => {
27 | const classNames = IconBtnBase + ' ' + className;
28 |
29 | const onClickHandler = (event: MouseEvent) => {
30 | if (disabled) return;
31 | onClick && onClick(event);
32 | };
33 |
34 | return (
35 |
43 | );
44 | };
45 |
46 | IconButton.defaultProps = defaultProps;
47 |
48 | export default IconButton;
49 |
--------------------------------------------------------------------------------
/store/components/scrollbar.tsx:
--------------------------------------------------------------------------------
1 | import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
2 |
3 | type ScrollbarProps = {
4 | className?: string;
5 | children: React.ReactNode;
6 | options?: any;
7 | };
8 |
9 | export const Scrollbar: React.FC = ({
10 | children,
11 | className,
12 | options,
13 | ...props
14 | }) => {
15 | return (
16 |
27 | {children}
28 |
29 | );
30 | };
31 |
--------------------------------------------------------------------------------
/store/components/section-title.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | SectionTitleBar,
3 | SectionTitleBarBase,
4 | SectionTitleBarHeading,
5 | SectionTitleBarSubTitle,
6 | SectionTitleBase,
7 | SectionTitleHeading,
8 | SectionTitleSubTitle
9 | } from './utils/theme';
10 |
11 | type SectionTitleProps = {
12 | title: string;
13 | subtitle?: string;
14 | className?: string;
15 | };
16 |
17 | export const SectionTitle: React.FC = ({
18 | title,
19 | subtitle,
20 | className
21 | }) => {
22 | const classNames = SectionTitleBase + ' ' + className;
23 | return (
24 |
25 |
{title}
26 |
27 |
{subtitle}
28 |
29 | );
30 | };
31 |
32 | export const SectionTitleWithBar: React.FC = ({
33 | title,
34 | subtitle,
35 | className
36 | }) => {
37 | const classNames = SectionTitleBarBase + ' ' + className;
38 | return (
39 |
40 |
41 |
{title}
42 |
43 |
{subtitle}
44 |
45 | );
46 | };
47 |
--------------------------------------------------------------------------------
/store/components/utils/prop-types.ts:
--------------------------------------------------------------------------------
1 | export const tuple = (...args: T) => args;
2 |
3 | const buttonVariants = tuple('primary', 'secondary', 'elevation');
4 | const buttonSizes = tuple('big', 'normal', 'small');
5 | const CounterSizes = tuple('big', 'normal');
6 |
7 | export type ButtonVariants = typeof buttonVariants[number];
8 | export type ButtonSizes = typeof buttonSizes[number];
9 |
10 | export type CounterSizes = typeof CounterSizes[number];
11 |
--------------------------------------------------------------------------------
/store/containers/banner/hero-block.tsx:
--------------------------------------------------------------------------------
1 | import Carousel from '@store/components/carousel/carousel';
2 | import { SwiperSlide } from '@store/components/carousel/slider';
3 | import { HeroBannerType } from '@ts-types/generated';
4 | import React, { memo } from 'react';
5 |
6 | import HeroBannerCard from './hero-banner-card';
7 |
8 | interface Props {
9 | heroBanners?: HeroBannerType[];
10 | className?: string;
11 | contentClassName?: string;
12 | }
13 |
14 | const HeroSliderBlock: React.FC = ({
15 | heroBanners,
16 | contentClassName = 'py-20'
17 | }) => {
18 | return (
19 |
24 |
35 | {heroBanners?.map((banner: any) => (
36 |
37 |
42 |
43 | ))}
44 |
45 |
46 | );
47 | };
48 |
49 | export default memo(HeroSliderBlock);
50 |
--------------------------------------------------------------------------------
/store/containers/layout/layout.tsx:
--------------------------------------------------------------------------------
1 | import { useAppDispatch, useAppSelector } from '@hooks/use-store';
2 | import { useStorage } from '@hooks/useStorage';
3 | import { rehydrate } from '@redux/card';
4 | import { Product } from '@ts-types/generated';
5 |
6 | import { CartDrawer, Drawer } from '../drawer/drawer';
7 | import Footer from './footer';
8 | import Header from './header';
9 |
10 | const Layout = (props) => {
11 | const items = useAppSelector((state) => state.cart.items);
12 | const dispatch = useAppDispatch();
13 |
14 | const rehydrateLocalState = (items: Product[]) => {
15 | dispatch(rehydrate(items));
16 | };
17 |
18 | // This component is global on all pages we are using it to get the items in local storage
19 | useStorage(items, rehydrateLocalState);
20 | return (
21 |
29 |
30 |
31 |
32 |
{props.children}
33 |
34 |
35 |
36 |
37 | );
38 | };
39 | export default Layout;
40 |
--------------------------------------------------------------------------------
/store/contexts/search/use-search.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState } from 'react';
2 |
3 | const searchContext = createContext({} as any);
4 |
5 | export const SearchProvider = ({ children }) => {
6 | const [searchTerm, setSearchTerm] = useState('');
7 | return (
8 |
9 | {children}
10 |
11 | );
12 | };
13 |
14 | export const useSearch = () => useContext(searchContext);
15 |
--------------------------------------------------------------------------------
/store/contexts/sticky/sticky.provider.tsx:
--------------------------------------------------------------------------------
1 | import React, { createContext, useReducer } from 'react';
2 | export const StickyContext = createContext<{
3 | state?: any;
4 | dispatch?: React.Dispatch;
5 | }>({});
6 |
7 | const INITIAL_STATE = {
8 | isSticky: false
9 | };
10 |
11 | function reducer(state, action) {
12 | switch (action.type) {
13 | case 'SET_STICKY':
14 | return {
15 | ...state,
16 | isSticky: true
17 | };
18 | case 'REMOVE_STICKY':
19 | return {
20 | ...state,
21 | isSticky: false
22 | };
23 | default: {
24 | throw new Error(`Unsupported action: ${action.type}`);
25 | }
26 | }
27 | }
28 |
29 | export const StickyProvider = ({ children }) => {
30 | const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
31 | return (
32 |
33 | {children}
34 |
35 | );
36 | };
37 |
--------------------------------------------------------------------------------
/store/helpers/constants.tsx:
--------------------------------------------------------------------------------
1 | // CONSTANTS
2 | export const CURRENCY = '$';
3 |
--------------------------------------------------------------------------------
/store/helpers/get-initials-or-option.tsx:
--------------------------------------------------------------------------------
1 | export function getInitialsOrOption(string) {
2 | string = string.replace(/[^\w\s]/gi, '');
3 | let letters = string.split(' ');
4 | if (letters.length > 1) {
5 | return (letters[0][0] + letters[1][0]).toUpperCase();
6 | } else {
7 | return (string[0] + string[1]).toUpperCase();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/store/helpers/use-media.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | const isClient = typeof window === 'object';
3 |
4 | export const useMedia = (query: string, defaultState: boolean = false) => {
5 | const [state, setState] = useState(
6 | isClient ? () => window.matchMedia(query).matches : defaultState
7 | );
8 |
9 | useEffect(() => {
10 | let mounted = true;
11 | const mql = window.matchMedia(query);
12 | const onChange = () => {
13 | if (!mounted) {
14 | return;
15 | }
16 | setState(!!mql.matches);
17 | };
18 |
19 | mql.addListener(onChange);
20 | setState(mql.matches);
21 |
22 | return () => {
23 | mounted = false;
24 | mql.removeListener(onChange);
25 | };
26 | }, [query]);
27 |
28 | return state;
29 | };
30 |
--------------------------------------------------------------------------------
/store/helpers/use-searchable.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | export const useSearchable = (data = [], searchText, searchProps) => {
3 | return [];
4 | // return useMemo(() => {
5 | // const regex = new RegExp(searchText, 'i');
6 | // return data.filter((item) =>
7 | // searchProps(item).some((sp) => regex.test(sp))
8 | // );
9 | // }, [data, searchText, searchProps]);
10 | };
11 | // const useSearchable = (data: T[], searchText: string, searchProps: (item: T) => string[]) => {
12 | // return useMemo(() => {
13 | // const regex = new RegExp(searchText, "i");
14 | // return data.filter((item) =>
15 | // searchProps(item).some((sp) => regex.test(sp))
16 | // );
17 | // }, [data, searchText, searchProps]);
18 | // };
19 | // export default useSearchable;
20 |
--------------------------------------------------------------------------------
/store/helpers/useOnClickOutside.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 |
3 | export const useOnClickOutside = (ref, handler) => {
4 | useEffect(
5 | () => {
6 | const listener = (event) => {
7 | // Do nothing if clicking ref's element or descendent elements
8 | if (!ref.current || ref.current.contains(event.target)) {
9 | return;
10 | }
11 |
12 | handler(event);
13 | };
14 |
15 | document.addEventListener('mousedown', listener);
16 | document.addEventListener('touchstart', listener);
17 |
18 | return () => {
19 | document.removeEventListener('mousedown', listener);
20 | document.removeEventListener('touchstart', listener);
21 | };
22 | },
23 | // Add ref and handler to effect dependencies
24 | // It's worth noting that because passed in handler is a new ...
25 | // ... function on every render that will cause this effect ...
26 | // ... callback/cleanup to run every render. It's not a big deal ...
27 | // ... but to optimize you can wrap handler in useCallback before ...
28 | // ... passing it into this hook.
29 | [ref, handler]
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/styles/color-picker.module.css:
--------------------------------------------------------------------------------
1 | .color_picker {
2 | @apply p-0 h-12 w-12 flex items-center bg-transparent rounded-full border border-border-200 outline-none appearance-none focus:outline-none focus:ring-0;
3 | }
4 |
5 | .color_picker::-webkit-color-swatch-wrapper {
6 | padding: 0;
7 | }
8 | .color_picker::-webkit-color-swatch {
9 | border-radius: 100%;
10 | box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px -1px,
11 | rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@pages/*": ["pages/*"],
19 | "@store/*": ["store/*"],
20 | "@components/*": ["components/*"],
21 | "@contexts/*": ["contexts/*"],
22 | "@utils/*": ["utils/*"],
23 | "@hooks/*": ["hooks/*"],
24 | "@redux/*": ["redux/*"],
25 | "@lib/*": ["lib/*"],
26 | "@assets/*": ["assets/*"],
27 | "@containers/*": ["containers/*"],
28 | "@styles/*": ["styles/*"],
29 | "@middleware/*": ["middleware/*"],
30 | "@data/*": ["data/*"],
31 | "@graphql/*": ["graphql/*"],
32 | "@settings/*": ["settings/*"],
33 | "@ts-types/*": ["ts-types/*"],
34 | "@test-utils": ["__tests__/test-utils"]
35 | },
36 | "incremental": true
37 | },
38 | "include": [
39 | "next-env.d.ts",
40 | "additional.d.ts",
41 | "**/*.ts",
42 | "**/*.tsx",
43 | "pages/_error.js"
44 | ],
45 | "exclude": ["node_modules"]
46 | }
47 |
--------------------------------------------------------------------------------
/utils/api/http.ts:
--------------------------------------------------------------------------------
1 | import { getAuthCredentials } from '@utils/auth-utils';
2 | import { ROUTES } from '@utils/routes';
3 | import axios from 'axios';
4 | import Cookies from 'js-cookie';
5 | import Router from 'next/router';
6 |
7 | const http = axios.create({
8 | baseURL: process.env.NEXT_PUBLIC_REST_API_ENDPOINT, // TODO: take this api URL from env
9 | timeout: 30000,
10 | headers: {
11 | Accept: 'application/json',
12 | 'Content-Type': 'application/json'
13 | }
14 | });
15 |
16 | // Change request data/error here
17 | http.interceptors.request.use(
18 | (config) => {
19 | const { token } = getAuthCredentials();
20 | config.headers = {
21 | ...config.headers,
22 | Authorization: `Bearer ${token}`
23 | };
24 | return config;
25 | },
26 | (error) => {
27 | return Promise.reject(error);
28 | }
29 | );
30 |
31 | // Change response data/error here
32 | http.interceptors.response.use(
33 | (response) => {
34 | return response;
35 | },
36 | (error) => {
37 | if (
38 | (error.response && error.response.status === 401) ||
39 | (error.response && error.response.status === 403) ||
40 | (error.response &&
41 | error.response.data.message === 'PICKBAZAR_ERROR.NOT_AUTHORIZED')
42 | ) {
43 | Cookies.remove('AUTH_CRED');
44 | Router.push(ROUTES.LOGIN);
45 | }
46 | return Promise.reject(error);
47 | }
48 | );
49 |
50 | export default http;
51 |
--------------------------------------------------------------------------------
/utils/auth-utils.ts:
--------------------------------------------------------------------------------
1 | // import { CREATE_PRIVILEGE, READ_PRIVILEGE, UPDATE_PRIVILEGE, DELETE_PRIVILEGE, SUPER_ADMIN_PRIVILEGE } from './constants';
2 |
3 | export function hasAccess(
4 | _allowedRoles: string[],
5 | _userPermissions: string[] | undefined | null
6 | ) {
7 | if (_userPermissions) {
8 | return Boolean(
9 | _allowedRoles?.find((role) => _userPermissions.includes(role))
10 | );
11 | }
12 | return false;
13 | }
14 |
--------------------------------------------------------------------------------
/utils/cartesian.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Cartesian is a result of multiple arrays
3 | * @param a array - check the sample input below
4 | * @returns array of arrays - check the sample out put below
5 | */
6 | export const cartesian = (...a: T) => {
7 | return a.reduce((a: T, b: T) =>
8 | a.flatMap((d: T) => b.map((e: T) => [d, e].flat()))
9 | );
10 | };
11 |
12 | // Cartesian Example
13 | // --------------------------------------------
14 | // INPUT:
15 | // Sample for calling the function:
16 | // cartesian([1,2],[10,20],[100,200,300]) [LG,S],[RED, BLACK],[1KG,5KG]
17 | // --------------------------------------------
18 | // OUTPUT:
19 | // [ [ 1, 10, 100 ],
20 | // [ 1, 10, 200 ],
21 | // [ 1, 10, 300 ],
22 | // [ 1, 20, 100 ],
23 | // [ 1, 20, 200 ],
24 | // [ 1, 20, 300 ],
25 | // [ 2, 10, 100 ],
26 | // [ 2, 10, 200 ],
27 | // [ 2, 10, 300 ],
28 | // [ 2, 20, 100 ],
29 | // [ 2, 20, 200 ],
30 | // [ 2, 20, 300 ] ]
31 |
--------------------------------------------------------------------------------
/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const LIMIT = 10;
2 |
3 | export const READ_PRIVILEGE = 'read_privilege';
4 | export type READ_TYPE = typeof READ_PRIVILEGE;
5 | export const CREATE_PRIVILEGE = 'create_privilege';
6 | export type CREATE_TYPE = typeof CREATE_PRIVILEGE;
7 | export const UPDATE_PRIVILEGE = 'update_privilege';
8 | export type UPDATE_TYPE = typeof UPDATE_PRIVILEGE;
9 | export const DELETE_PRIVILEGE = 'delete_privilege';
10 | export type DELETE_TYPE = typeof DELETE_PRIVILEGE;
11 | export const SUPER_ADMIN_PRIVILEGE = 'super_admin_privilege';
12 | export type ADMIN_TYPE = typeof SUPER_ADMIN_PRIVILEGE;
13 |
--------------------------------------------------------------------------------
/utils/cookies.ts:
--------------------------------------------------------------------------------
1 | import { CookieSerializeOptions, serialize } from 'cookie';
2 | import { NextApiResponse } from 'next';
3 |
4 | /**
5 | * This sets `cookie` using the `res` object
6 | */
7 |
8 | export const setCookie = (
9 | res: NextApiResponse,
10 | name: string,
11 | value: unknown,
12 | options: CookieSerializeOptions = {}
13 | ) => {
14 | const stringValue =
15 | typeof value === 'object' ? 'j:' + JSON.stringify(value) : String(value);
16 |
17 | if (typeof options.maxAge === 'number') {
18 | options.expires = new Date(Date.now() + options.maxAge * 1000);
19 | }
20 |
21 | res.setHeader('Set-Cookie', serialize(name, stringValue, options));
22 | };
23 |
--------------------------------------------------------------------------------
/utils/data-mappers.ts:
--------------------------------------------------------------------------------
1 | import { PaginatorInfo } from '@ts-types/generated';
2 | import camelcaseKeys from 'camelcase-keys';
3 | import pickBy from 'lodash/pickBy';
4 |
5 | interface PaginatorInfoType {
6 | [key: string]: unknown;
7 | }
8 | type PaginatorOutputType = {
9 | hasMorePages: boolean;
10 | nextPageUrl: string;
11 | [key: string]: unknown;
12 | };
13 | export const mapPaginatorData = (obj: PaginatorInfoType): PaginatorInfo => {
14 | const formattedValues = camelcaseKeys(obj);
15 | return {
16 | ...(formattedValues as PaginatorInfo),
17 | hasMorePages: formattedValues.lastPage !== formattedValues.currentPage
18 | };
19 | };
20 |
21 | export const stringifySearchQuery = (values: any) => {
22 | const parsedValues = pickBy(values);
23 | return Object.keys(parsedValues)
24 | .map((k) => {
25 | if (k === 'type') {
26 | return `${k}.slug:${parsedValues[k]};`;
27 | }
28 | if (k === 'category') {
29 | return `categories.slug:${parsedValues[k]};`;
30 | }
31 | return `${k}:${parsedValues[k]};`;
32 | })
33 | .join('')
34 | .slice(0, -1);
35 | };
36 |
--------------------------------------------------------------------------------
/utils/form-error.tsx:
--------------------------------------------------------------------------------
1 | import Router from 'next/router';
2 | import Cookies from 'js-cookie';
3 |
4 | export function getErrorMessage(error: any) {
5 | let processedError = {
6 | message: '',
7 | validation: []
8 | };
9 |
10 | if (error.graphQLErrors) {
11 | for (const graphQLError of error.graphQLErrors) {
12 | if (
13 | graphQLError.extensions &&
14 | graphQLError.extensions.category === 'validation'
15 | ) {
16 | processedError['message'] = graphQLError.message;
17 | processedError['validation'] = graphQLError.extensions.validation;
18 | return processedError;
19 | } else if (
20 | graphQLError.extensions &&
21 | graphQLError.extensions.category === 'authorization'
22 | ) {
23 | Cookies.remove('auth_token');
24 | Cookies.remove('auth_permissions');
25 | Router.push('/');
26 | }
27 | }
28 | }
29 | processedError['message'] = error.message;
30 | return processedError;
31 | }
32 |
--------------------------------------------------------------------------------
/utils/format-address.tsx:
--------------------------------------------------------------------------------
1 | function removeFalsy(obj: any) {
2 | return Object.fromEntries(Object.entries(obj).filter(([_, v]) => Boolean(v)));
3 | }
4 |
5 | export function formatAddress(address: any) {
6 | if (!address) return;
7 | const temp = ['street_address', 'city', 'state', 'zip', 'country'].reduce(
8 | (acc, k) => ({ ...acc, [k]: (address as any)[k] }),
9 | {}
10 | );
11 | const formattedAddress = removeFalsy(temp);
12 | return Object.values(formattedAddress).join(', ');
13 | }
14 |
--------------------------------------------------------------------------------
/utils/is-loggedin.ts:
--------------------------------------------------------------------------------
1 | import Cookies from 'js-cookie';
2 | import { SUPER_ADMIN } from './constants';
3 |
4 | export function loggedIn() {
5 | const token = Cookies.get('auth_token');
6 | if (!token) return false;
7 | if (token) {
8 | const permissions = Cookies.get('auth_permissions');
9 | if (!permissions?.includes(SUPER_ADMIN)) {
10 | return false;
11 | }
12 | }
13 | return true;
14 | }
15 |
--------------------------------------------------------------------------------
/utils/locals.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | const localeRTLList = ['ar', 'he'];
3 | export function useIsRTL() {
4 | const { locale } = useRouter();
5 | if (locale && localeRTLList.includes(locale)) {
6 | return { isRTL: true, alignLeft: 'right', alignRight: 'left' };
7 | }
8 | return { isRTL: false, alignLeft: 'left', alignRight: 'right' };
9 | }
10 |
--------------------------------------------------------------------------------
/utils/motion/fade-in-left.ts:
--------------------------------------------------------------------------------
1 | export function fadeInLeft(duration: number = 0.3) {
2 | return {
3 | from: {
4 | left: '-100%',
5 | transition: {
6 | type: 'easeInOut',
7 | duration: duration
8 | }
9 | },
10 | to: {
11 | left: 0,
12 | transition: {
13 | type: 'easeInOut',
14 | duration: duration
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/motion/fade-in-out.ts:
--------------------------------------------------------------------------------
1 | export function fadeInOut(duration: number = 0.2) {
2 | return {
3 | from: {
4 | opacity: 0,
5 | transition: {
6 | type: 'easeInOut',
7 | duration: duration
8 | }
9 | },
10 | to: {
11 | opacity: 1,
12 | transition: {
13 | type: 'easeInOut',
14 | duration: duration
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/motion/fade-in-right.ts:
--------------------------------------------------------------------------------
1 | export function fadeInRight(duration: number = 0.3) {
2 | return {
3 | from: {
4 | right: '-100%',
5 | transition: {
6 | type: 'easeInOut',
7 | duration: duration
8 | }
9 | },
10 | to: {
11 | right: 0,
12 | transition: {
13 | type: 'easeInOut',
14 | duration: duration
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/motion/height-collapse.ts:
--------------------------------------------------------------------------------
1 | export function heightCollapse() {
2 | return {
3 | from: {
4 | opacity: 0,
5 | height: 0,
6 | transition: {
7 | ease: [0.04, 0.62, 0.23, 0.98]
8 | }
9 | },
10 | to: {
11 | opacity: 1,
12 | height: 'auto',
13 | transition: {
14 | ease: [0.04, 0.62, 0.23, 0.98]
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/motion/zoom-in-bottom.ts:
--------------------------------------------------------------------------------
1 | export function zoomInBottom(duration: number = 0.2) {
2 | return {
3 | from: {
4 | y: 8,
5 | opacity: 0,
6 | scale: 0.99,
7 | transition: {
8 | type: 'easeOut',
9 | duration: duration
10 | }
11 | },
12 | to: {
13 | y: 0,
14 | opacity: 1,
15 | scale: 1,
16 | transition: {
17 | type: 'easeOut',
18 | duration: duration
19 | }
20 | }
21 | };
22 | }
23 |
--------------------------------------------------------------------------------
/utils/motion/zoom-in-out.ts:
--------------------------------------------------------------------------------
1 | export function zoomInOut(duration: number = 0.2) {
2 | return {
3 | from: {
4 | scale: 0.9,
5 | transition: {
6 | type: 'easeOut',
7 | duration: duration
8 | }
9 | },
10 | to: {
11 | scale: 1,
12 | transition: {
13 | type: 'easeOut',
14 | duration: duration
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/motion/zoom-out-in.ts:
--------------------------------------------------------------------------------
1 | export function zoomOutIn(duration: number = 0.2) {
2 | return {
3 | from: {
4 | scale: 1.1,
5 | transition: {
6 | type: 'easeOut',
7 | duration: duration
8 | }
9 | },
10 | to: {
11 | scale: 1,
12 | transition: {
13 | type: 'easeOut',
14 | duration: duration
15 | }
16 | }
17 | };
18 | }
19 |
--------------------------------------------------------------------------------
/utils/parse-cookie.ts:
--------------------------------------------------------------------------------
1 | function tryDecode(str: string, decode: any) {
2 | try {
3 | return decode(str);
4 | } catch (e) {
5 | return str;
6 | }
7 | }
8 |
9 | export function parseContextCookie(str: string, options?: any) {
10 | var pairSplitRegExp = /; */;
11 | var decode = decodeURIComponent;
12 | var obj: any = {};
13 | var opt = options || {};
14 | var pairs = str?.split(pairSplitRegExp) ?? [];
15 | var dec = opt.decode || decode;
16 |
17 | for (var i = 0; i < pairs.length; i++) {
18 | var pair = pairs[i];
19 | var eq_idx = pair.indexOf('=');
20 |
21 | // skip things that don't look like key=value
22 | if (eq_idx < 0) {
23 | continue;
24 | }
25 |
26 | var key = pair.substr(0, eq_idx).trim();
27 | var val = pair.substr(++eq_idx, pair.length).trim();
28 |
29 | // quoted values
30 | if ('"' == val[0]) {
31 | val = val.slice(1, -1);
32 | }
33 |
34 | // only assign once
35 | if (undefined == obj[key]) {
36 | obj[key] = tryDecode(val, dec);
37 | }
38 | }
39 |
40 | return obj;
41 | }
42 |
--------------------------------------------------------------------------------
/utils/private-route.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useRouter } from 'next/router';
3 | import { getAuthCredentials, hasAccess } from './auth-utils';
4 | import Loader from '@components/ui/loader/loader';
5 | import AccessDeniedPage from '@components/common/access-denied';
6 | import { ROUTES } from './routes';
7 |
8 | const PrivateRoute: React.FC<{ authProps: any }> = ({
9 | children,
10 | authProps
11 | }) => {
12 | const router = useRouter();
13 | const { token, permissions } = getAuthCredentials();
14 | const isUser = !!token;
15 | const hasPermission =
16 | Array.isArray(permissions) &&
17 | !!permissions.length &&
18 | hasAccess(authProps.permissions, permissions);
19 | React.useEffect(() => {
20 | if (!isUser) router.replace(ROUTES.LOGIN); // If not authenticated, force log in
21 | }, [isUser]);
22 |
23 | if (isUser && hasPermission) {
24 | return <>{children}>;
25 | }
26 | if (isUser && !hasPermission) {
27 | return ;
28 | }
29 | // Session is being fetched, or no user.
30 | // If no user, useEffect() will redirect.
31 | return ;
32 | };
33 |
34 | export default PrivateRoute;
35 |
--------------------------------------------------------------------------------
/utils/routes.ts:
--------------------------------------------------------------------------------
1 | export const ROUTES = {
2 | DASHBOARD: '/admin/dashboard',
3 | LOGIN: '/admin/login',
4 | ORDER_STATUS: '/admin/order-status',
5 | ORDERS: '/admin/orders',
6 | PRODUCTS: '/admin/products',
7 | COUPONS: '/admin/coupons',
8 | CUSTOMERS: '/admin/customers',
9 | SHIPPING_ZONES: '/admin/shipping-zones',
10 | SETTINGS: '/admin/settings',
11 | CATEGORIES: '/admin/categories',
12 | ATTRIBUTES: '/admin/attributes',
13 | TAGS: '/admin/tags',
14 | SUPPLIERS: '/admin/suppliers',
15 | LOGOUT: '/admin/logout',
16 | STAFFS: '/admin/staffs',
17 | ABOUT_PAGE: '/about-page',
18 | HERO_CAROUSEL: '/admin/hero-carousel'
19 | };
20 |
--------------------------------------------------------------------------------
/utils/use-breadcrumb.ts:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { useEffect, useState } from 'react';
3 |
4 | export function convertBreadcrumbTitle(string: string) {
5 | return string
6 | .replace(/-/g, ' ')
7 | .replace(/oe/g, 'ö')
8 | .replace(/ae/g, 'ä')
9 | .replace(/ue/g, 'ü')
10 | .toLowerCase();
11 | }
12 |
13 | export default function useBreadcrumb() {
14 | const router = useRouter();
15 | const [breadcrumbs, setBreadcrumbs] = useState(null);
16 | useEffect(() => {
17 | if (router) {
18 | const linkPath =
19 | router.asPath.indexOf('?') > 0
20 | ? router.pathname.split('/')
21 | : router.asPath.split('/');
22 | linkPath.shift();
23 |
24 | const pathArray = linkPath.map((path, i) => {
25 | return {
26 | breadcrumb: path,
27 | href: '/' + linkPath.slice(0, i + 1).join('/')
28 | };
29 | });
30 |
31 | setBreadcrumbs(pathArray);
32 | }
33 | }, [router]);
34 |
35 | return breadcrumbs;
36 | }
37 |
--------------------------------------------------------------------------------
/utils/use-click-outside.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react';
2 |
3 | type Event = MouseEvent | TouchEvent;
4 |
5 | export default function useOnClickOutside(
6 | ref: RefObject,
7 | handler: (event: Event) => void
8 | ) {
9 | useEffect(() => {
10 | const listener = (event: Event) => {
11 | const el = ref?.current;
12 | if (!el || el.contains((event?.target as Node) || null)) {
13 | return;
14 | }
15 | handler(event);
16 | };
17 | document.addEventListener('mousedown', listener);
18 | document.addEventListener('touchstart', listener);
19 | return () => {
20 | document.removeEventListener('mousedown', listener);
21 | document.removeEventListener('touchstart', listener);
22 | };
23 | }, [ref, handler]);
24 | }
25 |
--------------------------------------------------------------------------------