summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-02-17 17:07:50 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-02-17 17:07:50 +0700
commitf99e0aba70efad0deb907d8e27f09fc9f527c8a4 (patch)
treef0ac96e4e736a1d385e32553f0e641ee27e11fd3
parent90e1edab9b6a8ccc09a49fed3addbec2cbc4e4c3 (diff)
Refactor
-rw-r--r--package.json5
-rw-r--r--src/core/api/odooApi.js47
-rw-r--r--src/core/api/searchSuggestApi.js12
-rw-r--r--src/core/components/Seo.jsx11
-rw-r--r--src/core/components/elements/Appbar/Appbar.jsx33
-rw-r--r--src/core/components/elements/Badge/Badge.jsx33
-rw-r--r--src/core/components/elements/Divider/Divider.jsx11
-rw-r--r--src/core/components/elements/Image/Image.jsx15
-rw-r--r--src/core/components/elements/Link/Link.jsx17
-rw-r--r--src/core/components/elements/NavBar/NavBar.jsx31
-rw-r--r--src/core/components/elements/NavBar/Search.jsx89
-rw-r--r--src/core/components/elements/Pagination/Pagination.js64
-rw-r--r--src/core/components/elements/Popup/BottomPopup.jsx21
-rw-r--r--src/core/components/elements/Skeleton/BrandSkeleton.jsx8
-rw-r--r--src/core/components/elements/Skeleton/ImageSkeleton.jsx10
-rw-r--r--src/core/components/elements/Skeleton/ProductCardSkeleton.jsx15
-rw-r--r--src/core/components/layouts/AnimationLayout.jsx22
-rw-r--r--src/core/components/layouts/AppLayout.jsx15
-rw-r--r--src/core/components/layouts/BasicLayout.jsx15
-rw-r--r--src/core/hooks/useActive.js19
-rw-r--r--src/core/hooks/useAuth.js14
-rw-r--r--src/core/utils/auth.js37
-rw-r--r--src/core/utils/cart.js39
-rw-r--r--src/core/utils/currencyFormat.js10
-rw-r--r--src/core/utils/slug.js24
-rw-r--r--src/core/utils/toTitleCase.js6
-rw-r--r--src/lib/brand/api/BrandApi.js8
-rw-r--r--src/lib/brand/components/Brand.jsx70
-rw-r--r--src/lib/brand/components/BrandCard.jsx20
-rw-r--r--src/lib/brand/hooks/useBrand.js13
-rw-r--r--src/lib/cart/api/CartApi.js11
-rw-r--r--src/lib/cart/components/Cart.jsx30
-rw-r--r--src/lib/cart/hooks/useCart.js17
-rw-r--r--src/lib/home/api/categoryHomeApi.js8
-rw-r--r--src/lib/home/api/categoryHomeIdApi.js8
-rw-r--r--src/lib/home/api/heroBannerApi.js8
-rw-r--r--src/lib/home/api/popularProductApi.js8
-rw-r--r--src/lib/home/api/preferredBrandApi.js8
-rw-r--r--src/lib/home/components/CategoryHome.jsx28
-rw-r--r--src/lib/home/components/CategoryHomeId.jsx19
-rw-r--r--src/lib/home/components/HeroBanner.jsx50
-rw-r--r--src/lib/home/components/PopularProduct.jsx24
-rw-r--r--src/lib/home/components/PreferredBrand.jsx30
-rw-r--r--src/lib/home/components/Skeleton/PopularProductSkeleton.jsx10
-rw-r--r--src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx12
-rw-r--r--src/lib/home/hooks/useCategoryHome.js13
-rw-r--r--src/lib/home/hooks/useCategoryHomeId.js13
-rw-r--r--src/lib/home/hooks/useHeroBanner.js13
-rw-r--r--src/lib/home/hooks/usePopularProduct.js13
-rw-r--r--src/lib/home/hooks/usePreferredBrand.js13
-rw-r--r--src/lib/product/api/productApi.js8
-rw-r--r--src/lib/product/api/productSearchApi.js9
-rw-r--r--src/lib/product/api/productSimilarApi.js8
-rw-r--r--src/lib/product/components/Product.jsx276
-rw-r--r--src/lib/product/components/ProductCard.jsx68
-rw-r--r--src/lib/product/components/ProductFilter.jsx131
-rw-r--r--src/lib/product/components/ProductSearch.jsx95
-rw-r--r--src/lib/product/components/ProductSimilar.jsx15
-rw-r--r--src/lib/product/components/ProductSlider.jsx51
-rw-r--r--src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx14
-rw-r--r--src/lib/product/hooks/useProductSearch.js15
-rw-r--r--src/lib/product/hooks/useProductSimilar.js13
-rw-r--r--src/pages/_app.jsx36
-rw-r--r--src/pages/api/shop/search.js80
-rw-r--r--src/pages/api/shop/suggest.js9
-rw-r--r--src/pages/index.jsx20
-rw-r--r--src/pages/shop/brands/[slug].jsx23
-rw-r--r--src/pages/shop/cart.jsx10
-rw-r--r--src/pages/shop/product/[slug].jsx29
-rw-r--r--src/pages/shop/search.jsx19
-rw-r--r--src/styles/globals.css30
-rw-r--r--src2/components/auth/WithAuth.js (renamed from src/components/auth/WithAuth.js)0
-rw-r--r--src2/components/elements/Alert.js (renamed from src/components/elements/Alert.js)0
-rw-r--r--src2/components/elements/BottomPopup.js (renamed from src/components/elements/BottomPopup.js)0
-rw-r--r--src2/components/elements/ConfirmAlert.js (renamed from src/components/elements/ConfirmAlert.js)0
-rw-r--r--src2/components/elements/DescriptionRow.js (renamed from src/components/elements/DescriptionRow.js)0
-rw-r--r--src2/components/elements/Disclosure.js (renamed from src/components/elements/Disclosure.js)0
-rw-r--r--src2/components/elements/Fields.js (renamed from src/components/elements/Fields.js)0
-rw-r--r--src2/components/elements/Filter.js (renamed from src/components/elements/Filter.js)0
-rw-r--r--src2/components/elements/Image.js (renamed from src/components/elements/Image.js)0
-rw-r--r--src2/components/elements/LineDivider.js (renamed from src/components/elements/LineDivider.js)0
-rw-r--r--src2/components/elements/Link.js (renamed from src/components/elements/Link.js)0
-rw-r--r--src2/components/elements/Pagination.js (renamed from src/components/elements/Pagination.js)0
-rw-r--r--src2/components/elements/ProgressBar.js (renamed from src/components/elements/ProgressBar.js)0
-rw-r--r--src2/components/elements/Skeleton.js (renamed from src/components/elements/Skeleton.js)0
-rw-r--r--src2/components/elements/Spinner.js (renamed from src/components/elements/Spinner.js)0
-rw-r--r--src2/components/layouts/AppBar.js (renamed from src/components/layouts/AppBar.js)0
-rw-r--r--src2/components/layouts/Footer.js (renamed from src/components/layouts/Footer.js)0
-rw-r--r--src2/components/layouts/Header.js (renamed from src/components/layouts/Header.js)0
-rw-r--r--src2/components/layouts/Layout.js (renamed from src/components/layouts/Layout.js)0
-rw-r--r--src2/components/manufactures/ManufactureCard.js (renamed from src/components/manufactures/ManufactureCard.js)0
-rw-r--r--src2/components/products/ProductCard.js (renamed from src/components/products/ProductCard.js)0
-rw-r--r--src2/components/products/ProductCategories.js (renamed from src/components/products/ProductCategories.js)0
-rw-r--r--src2/components/products/ProductSimilar.js (renamed from src/components/products/ProductSimilar.js)0
-rw-r--r--src2/components/products/ProductSlider.js (renamed from src/components/products/ProductSlider.js)0
-rw-r--r--src2/components/transactions/TransactionDetail.js (renamed from src/components/transactions/TransactionDetail.js)0
-rw-r--r--src2/components/transactions/TransactionStatusBadge.js (renamed from src/components/transactions/TransactionStatusBadge.js)0
-rw-r--r--src2/components/variants/VariantCard.js (renamed from src/components/variants/VariantCard.js)0
-rw-r--r--src2/components/variants/VariantGroupCard.js (renamed from src/components/variants/VariantGroupCard.js)0
-rw-r--r--src2/core/utils/address.js (renamed from src/core/utils/address.js)0
-rw-r--r--src2/core/utils/apiOdoo.js (renamed from src/core/utils/apiOdoo.js)0
-rw-r--r--src2/core/utils/auth.js38
-rw-r--r--src2/core/utils/cart.js36
-rw-r--r--src2/core/utils/convertToOption.js (renamed from src/core/utils/convertToOption.js)0
-rw-r--r--src2/core/utils/currencyFormat.js8
-rw-r--r--src2/core/utils/formValidation.js (renamed from src/core/utils/formValidation.js)0
-rw-r--r--src2/core/utils/getFileBase64.js (renamed from src/core/utils/getFileBase64.js)0
-rw-r--r--src2/core/utils/greeting.js (renamed from src/core/utils/greeting.js)0
-rw-r--r--src2/core/utils/mailer.js (renamed from src/core/utils/mailer.js)0
-rw-r--r--src2/core/utils/slug.js25
-rw-r--r--src2/core/utils/toTitleCase.js8
-rw-r--r--src2/icons/chevron-left.svg (renamed from src/icons/chevron-left.svg)0
-rw-r--r--src2/icons/chevron-right.svg (renamed from src/icons/chevron-right.svg)0
-rw-r--r--src2/icons/close.svg (renamed from src/icons/close.svg)0
-rw-r--r--src2/icons/filter.svg (renamed from src/icons/filter.svg)0
-rw-r--r--src2/icons/image-placeholder.svg (renamed from src/icons/image-placeholder.svg)0
-rw-r--r--src2/icons/instagram.svg (renamed from src/icons/instagram.svg)0
-rw-r--r--src2/icons/linkedin.svg (renamed from src/icons/linkedin.svg)0
-rw-r--r--src2/icons/menu.svg (renamed from src/icons/menu.svg)0
-rw-r--r--src2/icons/minus.svg (renamed from src/icons/minus.svg)0
-rw-r--r--src2/icons/plus.svg (renamed from src/icons/plus.svg)0
-rw-r--r--src2/icons/search.svg (renamed from src/icons/search.svg)0
-rw-r--r--src2/icons/shopping-cart.svg (renamed from src/icons/shopping-cart.svg)0
-rw-r--r--src2/icons/trash.svg (renamed from src/icons/trash.svg)0
-rw-r--r--src2/images/logo.pngbin0 -> 49879 bytes
-rw-r--r--src2/images/page-not-found.png (renamed from src/images/page-not-found.png)bin42280 -> 42280 bytes
-rw-r--r--src2/lib/elements/hooks/useBottomPopup.js (renamed from src/lib/elements/hooks/useBottomPopup.js)0
-rw-r--r--src2/lib/elements/hooks/useConfirmAlert.js (renamed from src/lib/elements/hooks/useConfirmAlert.js)0
-rw-r--r--src2/pages/404.js (renamed from src/pages/404.js)0
-rw-r--r--src2/pages/_app.js (renamed from src/pages/_app.js)0
-rw-r--r--src2/pages/_error.js (renamed from src/pages/_error.js)0
-rw-r--r--src2/pages/activate.js (renamed from src/pages/activate.js)0
-rw-r--r--src2/pages/api/activation-request.js (renamed from src/pages/api/activation-request.js)0
-rw-r--r--src2/pages/api/activation.js (renamed from src/pages/api/activation.js)0
-rw-r--r--src2/pages/api/login.js (renamed from src/pages/api/login.js)0
-rw-r--r--src2/pages/api/register.js (renamed from src/pages/api/register.js)0
-rw-r--r--src2/pages/api/shop/search.js96
-rw-r--r--src2/pages/api/shop/suggest.js12
-rw-r--r--src2/pages/api/token.js (renamed from src/pages/api/token.js)0
-rw-r--r--src2/pages/faqs.js (renamed from src/pages/faqs.js)0
-rw-r--r--src2/pages/index.js (renamed from src/pages/index.js)0
-rw-r--r--src2/pages/login.js (renamed from src/pages/login.js)0
-rw-r--r--src2/pages/logout.js (renamed from src/pages/logout.js)0
-rw-r--r--src2/pages/my/address/[id]/edit.js (renamed from src/pages/my/address/[id]/edit.js)0
-rw-r--r--src2/pages/my/address/create.js (renamed from src/pages/my/address/create.js)0
-rw-r--r--src2/pages/my/address/index.js (renamed from src/pages/my/address/index.js)0
-rw-r--r--src2/pages/my/invoice/[id].js (renamed from src/pages/my/invoice/[id].js)0
-rw-r--r--src2/pages/my/invoices.js (renamed from src/pages/my/invoices.js)0
-rw-r--r--src2/pages/my/menu.js (renamed from src/pages/my/menu.js)0
-rw-r--r--src2/pages/my/profile.js (renamed from src/pages/my/profile.js)0
-rw-r--r--src2/pages/my/transaction/[id].js (renamed from src/pages/my/transaction/[id].js)0
-rw-r--r--src2/pages/my/transactions.js (renamed from src/pages/my/transactions.js)0
-rw-r--r--src2/pages/my/wishlist.js (renamed from src/pages/my/wishlist.js)0
-rw-r--r--src2/pages/register.js (renamed from src/pages/register.js)0
-rw-r--r--src2/pages/shop/brands/[slug].js (renamed from src/pages/shop/brands/[slug].js)0
-rw-r--r--src2/pages/shop/brands/index.js (renamed from src/pages/shop/brands/index.js)0
-rw-r--r--src2/pages/shop/cart.js (renamed from src/pages/shop/cart.js)0
-rw-r--r--src2/pages/shop/checkout/finish.js (renamed from src/pages/shop/checkout/finish.js)0
-rw-r--r--src2/pages/shop/checkout/index.js (renamed from src/pages/shop/checkout/index.js)0
-rw-r--r--src2/pages/shop/product/[slug].js (renamed from src/pages/shop/product/[slug].js)0
-rw-r--r--src2/pages/shop/quotation/finish.js (renamed from src/pages/shop/quotation/finish.js)0
-rw-r--r--src2/pages/shop/quotation/index.js (renamed from src/pages/shop/quotation/index.js)0
-rw-r--r--src2/pages/shop/search.js (renamed from src/pages/shop/search.js)0
-rw-r--r--src2/styles/globals.css468
-rw-r--r--tailwind.config.js8
165 files changed, 2597 insertions, 131 deletions
diff --git a/package.json b/package.json
index da31e920..e3036b5c 100644
--- a/package.json
+++ b/package.json
@@ -11,10 +11,12 @@
"dependencies": {
"@heroicons/react": "^2.0.13",
"@hookform/resolvers": "^2.9.10",
+ "@tailwindcss/line-clamp": "^0.4.2",
"axios": "^1.1.3",
+ "camelcase-object-deep": "^1.1.7",
"cookies-next": "^2.1.1",
"framer-motion": "^7.6.7",
- "lodash": "^4.17.21",
+ "lodash-contrib": "^4.1200.1",
"next": "13.0.0",
"next-progress": "^2.2.0",
"nodemailer": "^6.8.0",
@@ -25,6 +27,7 @@
"react-infinite-scroll-component": "^6.1.0",
"react-lazy-load": "^4.0.1",
"react-lazy-load-image-component": "^1.5.5",
+ "react-query": "^3.39.3",
"react-select": "^5.7.0",
"swiper": "^8.4.4",
"yup": "^0.32.11"
diff --git a/src/core/api/odooApi.js b/src/core/api/odooApi.js
new file mode 100644
index 00000000..59d88faa
--- /dev/null
+++ b/src/core/api/odooApi.js
@@ -0,0 +1,47 @@
+import axios from 'axios'
+import camelcaseObjectDeep from 'camelcase-object-deep'
+import { getCookie, setCookie } from 'cookies-next'
+import { getAuth } from '../utils/auth'
+
+const renewToken = async () => {
+ let token = await axios.get(process.env.ODOO_HOST + '/api/token')
+ setCookie('token', token.data.result)
+ return token.data.result
+}
+
+const getToken = async () => {
+ let token = getCookie('token')
+ if (token == undefined) token = await renewToken()
+ return token
+}
+
+const maxConnectionAttempt = 15
+let connectionAttempt = 0
+
+const odooApi = async (method, url, data = {}, headers = {}) => {
+ connectionAttempt++
+ try {
+ let token = await getToken()
+ const auth = getAuth()
+
+ let axiosParameter = {
+ method,
+ url: process.env.ODOO_HOST + url,
+ headers: {'Authorization': token, ...headers}
+ }
+ if (auth) axiosParameter.headers['Token'] = auth.token
+ if (method.toUpperCase() == 'POST') axiosParameter.headers['Content-Type'] = 'application/x-www-form-urlencoded'
+ if (Object.keys(data).length > 0) axiosParameter.data = new URLSearchParams(Object.entries(data)).toString()
+
+ let res = await axios(axiosParameter)
+ if (res.data.status.code == 401 && connectionAttempt < maxConnectionAttempt) {
+ await renewToken()
+ return odooApi(method, url, data, headers)
+ }
+ return camelcaseObjectDeep(res.data.result) || []
+ } catch (error) {
+ console.log(error)
+ }
+}
+
+export default odooApi; \ No newline at end of file
diff --git a/src/core/api/searchSuggestApi.js b/src/core/api/searchSuggestApi.js
new file mode 100644
index 00000000..b5edebda
--- /dev/null
+++ b/src/core/api/searchSuggestApi.js
@@ -0,0 +1,12 @@
+import axios from "axios"
+
+const searchSuggestApi = async ({ query }) => {
+ const dataSearchSuggest = await axios(`${process.env.SELF_HOST}/api/shop/suggest?q=${query.trim()}`)
+ return dataSearchSuggest
+}
+
+searchSuggestApi.defaultProps = {
+ query: ''
+}
+
+export default searchSuggestApi \ No newline at end of file
diff --git a/src/core/components/Seo.jsx b/src/core/components/Seo.jsx
new file mode 100644
index 00000000..bcfaa6ef
--- /dev/null
+++ b/src/core/components/Seo.jsx
@@ -0,0 +1,11 @@
+import Head from "next/head"
+
+const Seo = ({ title }) => {
+ return (
+ <Head>
+ <title>{ title }</title>
+ </Head>
+ )
+}
+
+export default Seo \ No newline at end of file
diff --git a/src/core/components/elements/Appbar/Appbar.jsx b/src/core/components/elements/Appbar/Appbar.jsx
new file mode 100644
index 00000000..0fe087d3
--- /dev/null
+++ b/src/core/components/elements/Appbar/Appbar.jsx
@@ -0,0 +1,33 @@
+import { useRouter } from "next/router"
+import Link from "../Link/Link"
+import { HomeIcon, Bars3Icon, ShoppingCartIcon, ChevronLeftIcon } from "@heroicons/react/24/outline"
+
+const AppBar = ({ title }) => {
+ const router = useRouter()
+
+ return (
+ <nav className="sticky top-0 z-50 bg-white shadow flex justify-between">
+ <div className="flex items-center">
+ <button type="button" className="p-4" onClick={() => router.back()}>
+ <ChevronLeftIcon className="w-6 stroke-2" />
+ </button>
+ <div className="font-semibold text-h-md">
+ { title }
+ </div>
+ </div>
+ <div className="flex items-center px-2">
+ <Link href="/shop/cart" className="py-4 px-2">
+ <ShoppingCartIcon className="w-6 text-gray_r-12" />
+ </Link>
+ <Link href="/shop/cart" className="py-4 px-2">
+ <HomeIcon className="w-6 text-gray_r-12" />
+ </Link>
+ <Link href="/shop/cart" className="py-4 px-2">
+ <Bars3Icon className="w-6 text-gray_r-12" />
+ </Link>
+ </div>
+ </nav>
+ )
+}
+
+export default AppBar \ No newline at end of file
diff --git a/src/core/components/elements/Badge/Badge.jsx b/src/core/components/elements/Badge/Badge.jsx
new file mode 100644
index 00000000..5d8ebd1c
--- /dev/null
+++ b/src/core/components/elements/Badge/Badge.jsx
@@ -0,0 +1,33 @@
+const Badge = ({
+ children,
+ type,
+ ...props
+}) => {
+ return (
+ <div
+ { ...props }
+ className={`${badgeStyle(type)} ${props?.className}`}
+ >
+ { children }
+ </div>
+ )
+}
+
+Badge.defaultProps = {
+ className: ''
+}
+
+const badgeStyle = (type) => {
+ let className = ['rounded px-1 text-[11px]']
+ switch (type) {
+ case 'solid-red':
+ className.push('bg-red_r-11 text-white')
+ break
+ case 'light':
+ className.push('bg-gray_r-4 text-gray_r-11')
+ break
+ }
+ return className.join(' ')
+}
+
+export default Badge \ No newline at end of file
diff --git a/src/core/components/elements/Divider/Divider.jsx b/src/core/components/elements/Divider/Divider.jsx
new file mode 100644
index 00000000..355cd509
--- /dev/null
+++ b/src/core/components/elements/Divider/Divider.jsx
@@ -0,0 +1,11 @@
+const Divider = (props) => {
+ return (
+ <div className={`h-1 bg-gray_r-4 ${props.className}`} />
+ )
+}
+
+Divider.defaultProps = {
+ className: ''
+}
+
+export default Divider \ No newline at end of file
diff --git a/src/core/components/elements/Image/Image.jsx b/src/core/components/elements/Image/Image.jsx
new file mode 100644
index 00000000..be2866e7
--- /dev/null
+++ b/src/core/components/elements/Image/Image.jsx
@@ -0,0 +1,15 @@
+import { LazyLoadImage } from "react-lazy-load-image-component"
+import "react-lazy-load-image-component/src/effects/opacity.css"
+
+const Image = ({ ...props }) => (
+ <LazyLoadImage
+ { ...props }
+ effect="opacity"
+ src={props.src || '/images/noimage.jpeg'}
+ alt={props.src ? props.alt : 'Image Not Found - Indoteknik'}
+ />
+)
+
+Image.defaultProps = LazyLoadImage.defaultProps
+
+export default Image \ No newline at end of file
diff --git a/src/core/components/elements/Link/Link.jsx b/src/core/components/elements/Link/Link.jsx
new file mode 100644
index 00000000..a619164d
--- /dev/null
+++ b/src/core/components/elements/Link/Link.jsx
@@ -0,0 +1,17 @@
+import NextLink from "next/link"
+
+const Link = ({ children, ...props }) => {
+ return (
+ <NextLink
+ {...props}
+ scroll={false}
+ className={`block font-medium text-red_r-11 ${props?.className}`}
+ >
+ {children}
+ </NextLink>
+ )
+}
+
+Link.defaultProps = NextLink.defaultProps
+
+export default Link \ No newline at end of file
diff --git a/src/core/components/elements/NavBar/NavBar.jsx b/src/core/components/elements/NavBar/NavBar.jsx
new file mode 100644
index 00000000..212fd341
--- /dev/null
+++ b/src/core/components/elements/NavBar/NavBar.jsx
@@ -0,0 +1,31 @@
+import Image from "next/image"
+import IndoteknikLogo from "@/images/logo.png"
+import { Bars3Icon, HeartIcon, ShoppingCartIcon } from "@heroicons/react/24/outline"
+import Link from "../Link/Link"
+import Search from "./Search"
+
+const NavBar = () => {
+ return (
+ <nav className="px-4 py-2 pb-3 sticky top-0 z-50 bg-white shadow">
+ <div className="flex justify-between items-center mb-2">
+ <Link href="/">
+ <Image src={IndoteknikLogo} alt="Indoteknik Logo" width={120} height={40} />
+ </Link>
+ <div className="flex gap-x-3">
+ <button type="button">
+ <HeartIcon className="w-6 text-gray_r-12" />
+ </button>
+ <Link href="/shop/cart">
+ <ShoppingCartIcon className="w-6 text-gray_r-12" />
+ </Link>
+ <button type="button">
+ <Bars3Icon className="w-6 text-gray_r-12" />
+ </button>
+ </div>
+ </div>
+ <Search />
+ </nav>
+ )
+}
+
+export default NavBar \ No newline at end of file
diff --git a/src/core/components/elements/NavBar/Search.jsx b/src/core/components/elements/NavBar/Search.jsx
new file mode 100644
index 00000000..cca1a97c
--- /dev/null
+++ b/src/core/components/elements/NavBar/Search.jsx
@@ -0,0 +1,89 @@
+import searchSuggestApi from "@/core/api/searchSuggestApi"
+import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"
+import { useCallback, useEffect, useRef, useState } from "react"
+import Link from "../Link/Link"
+import { useRouter } from "next/router"
+
+const Search = () => {
+ const router = useRouter()
+ const queryRef = useRef()
+ const [ query, setQuery ] = useState('')
+ const [ suggestions, setSuggestions ] = useState([])
+
+ useEffect(() => {
+ setQuery(router.query.q)
+ }, [router.query])
+
+ const loadSuggestion = useCallback(() => {
+ if (query && document.activeElement == queryRef.current) {
+ (async () => {
+ const dataSuggestion = await searchSuggestApi({ query })
+ setSuggestions(dataSuggestion.data.suggestions)
+ })()
+ return
+ } else {
+ setSuggestions([])
+ }
+ }, [ query ])
+
+ useEffect(() => {
+ if (query && document.activeElement == queryRef.current) {
+ loadSuggestion()
+ } else {
+ setSuggestions([])
+ }
+ }, [ loadSuggestion, query ])
+
+ const handleSubmit = (e) => {
+ e.preventDefault()
+ if (query) {
+ router.push(`/shop/search?q=${query}`)
+ } else {
+ queryRef.current.focus()
+ }
+ }
+
+ const onInputBlur = () => {
+ setTimeout(() => {
+ setSuggestions([])
+ }, 100)
+ }
+
+ return (
+ <form
+ onSubmit={handleSubmit}
+ className="flex relative"
+ >
+ <input
+ type="text"
+ ref={queryRef}
+ className="form-input p-3 rounded-r-none border-r-0 focus:border-gray_r-6"
+ placeholder="Ketik nama, part number, merk"
+ value={query}
+ onChange={(e) => setQuery(e.target.value)}
+ onBlur={onInputBlur}
+ onFocus={loadSuggestion}
+ />
+ <button
+ type="submit"
+ className="rounded-r border border-l-0 border-gray_r-6 px-2"
+ >
+ <MagnifyingGlassIcon className="w-6" />
+ </button>
+
+ { suggestions.length > 1 && (
+ <>
+ <div className="absolute w-full top-[50px] rounded-b bg-gray_r-1 border border-gray_r-6 divide-y divide-gray_r-6">
+ {suggestions.map((suggestion, index) => (
+ <Link href={`/shop/search?q=${suggestion.term}`} key={index} className="px-3 py-3 !text-gray_r-12 font-normal">
+ {suggestion.term}
+ </Link>
+ ))}
+ </div>
+ </>
+ ) }
+ </form>
+ )
+}
+
+export default Search \ No newline at end of file
diff --git a/src/core/components/elements/Pagination/Pagination.js b/src/core/components/elements/Pagination/Pagination.js
new file mode 100644
index 00000000..485295fe
--- /dev/null
+++ b/src/core/components/elements/Pagination/Pagination.js
@@ -0,0 +1,64 @@
+import Link from "../Link/Link"
+
+const Pagination = ({ pageCount, currentPage, url, className }) => {
+ let firstPage = false
+ let lastPage = false
+ let dotsPrevPage = false
+ let dotsNextPage = false
+ let urlParameterPrefix = url.includes('?') ? '&' : '?'
+
+ return pageCount > 1 && (
+ <div className={`pagination ${className}`}>
+ { Array.from(Array(pageCount)).map((v, i) => {
+ let page = i + 1
+ let rangePrevPage = currentPage - 2
+ let rangeNextPage = currentPage + 2
+ let PageComponent = <Link key={i} href={`${url + urlParameterPrefix}page=${page}`} className={"pagination-item" + (page == currentPage ? " pagination-item--active " : "")}>{page}</Link>
+ let DotsComponent = <div key={i} className="pagination-dots">...</div>
+
+ if (pageCount == 7) {
+ return PageComponent
+ }
+
+ if (currentPage == 1) rangeNextPage += 3
+ if (currentPage == 2) rangeNextPage += 2
+ if (currentPage == 3) rangeNextPage += 1
+ if (currentPage == 4) rangePrevPage -= 1
+ if (currentPage == pageCount) rangePrevPage -= 3
+ if (currentPage == pageCount - 1) rangePrevPage -= 2
+ if (currentPage == pageCount - 2) rangePrevPage -= 1
+ if (currentPage == pageCount - 3) rangeNextPage += 1
+
+ if (page > rangePrevPage && page < rangeNextPage) {
+ return PageComponent
+ }
+
+ if (page == 1 && rangePrevPage >= 1 && !firstPage) {
+ firstPage = true
+ return PageComponent
+ }
+
+ if (page == pageCount && rangeNextPage <= pageCount && !lastPage) {
+ lastPage = true
+ return PageComponent
+ }
+
+ if (page > currentPage && (pageCount - currentPage) > 1 && !dotsNextPage) {
+ dotsNextPage = true
+ return DotsComponent
+ }
+
+ if (page < currentPage && (currentPage - 1) > 1 && !dotsPrevPage) {
+ dotsPrevPage = true
+ return DotsComponent
+ }
+ }) }
+ </div>
+ )
+}
+
+Pagination.defaultProps = {
+ className: ''
+}
+
+export default Pagination \ No newline at end of file
diff --git a/src/core/components/elements/Popup/BottomPopup.jsx b/src/core/components/elements/Popup/BottomPopup.jsx
new file mode 100644
index 00000000..e687cf20
--- /dev/null
+++ b/src/core/components/elements/Popup/BottomPopup.jsx
@@ -0,0 +1,21 @@
+import { XMarkIcon } from "@heroicons/react/24/outline"
+
+const BottomPopup = ({ children, active, title, close }) => (
+ <>
+ <div
+ onClick={close}
+ className={`overlay ${active ? 'block' : 'hidden'}`}
+ />
+ <div className={`fixed bottom-0 left-0 w-full border-t border-gray_r-6 rounded-t-xl z-[60] p-4 pt-0 bg-white ${active ? 'block' : 'hidden'}`}>
+ <div className="flex justify-between py-4">
+ <div className="font-semibold text-h-sm">{ title }</div>
+ <button type="button" onClick={close}>
+ <XMarkIcon className="w-5 stroke-2" />
+ </button>
+ </div>
+ { children }
+ </div>
+ </>
+)
+
+export default BottomPopup \ No newline at end of file
diff --git a/src/core/components/elements/Skeleton/BrandSkeleton.jsx b/src/core/components/elements/Skeleton/BrandSkeleton.jsx
new file mode 100644
index 00000000..ce5a994d
--- /dev/null
+++ b/src/core/components/elements/Skeleton/BrandSkeleton.jsx
@@ -0,0 +1,8 @@
+const BrandSkeleton = () => (
+ <div role="status" className="animate-pulse">
+ <div className="h-12 bg-gray-200 rounded"></div>
+ <span className="sr-only">Loading...</span>
+ </div>
+)
+
+export default BrandSkeleton \ No newline at end of file
diff --git a/src/core/components/elements/Skeleton/ImageSkeleton.jsx b/src/core/components/elements/Skeleton/ImageSkeleton.jsx
new file mode 100644
index 00000000..2cda9536
--- /dev/null
+++ b/src/core/components/elements/Skeleton/ImageSkeleton.jsx
@@ -0,0 +1,10 @@
+const ImageSkeleton = () => (
+ <div role="status" className="animate-pulse">
+ <div className="flex items-center justify-center h-56 mb-4 bg-gray-300 rounded" aria-busy>
+ <svg className="w-12 h-12 text-gray-200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg>
+ </div>
+ <span className="sr-only">Loading...</span>
+ </div>
+)
+
+export default ImageSkeleton \ No newline at end of file
diff --git a/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx
new file mode 100644
index 00000000..66b48f79
--- /dev/null
+++ b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx
@@ -0,0 +1,15 @@
+const ProductCardSkeleton = () => (
+ <div role="status" className="p-4 max-w-sm rounded border border-gray-300 shadow animate-pulse md:p-6">
+ <div className="flex items-center justify-center h-36 mb-4 bg-gray-300 rounded" aria-busy>
+ <svg className="w-12 h-12 text-gray-200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" fill="currentColor" viewBox="0 0 640 512"><path d="M480 80C480 35.82 515.8 0 560 0C604.2 0 640 35.82 640 80C640 124.2 604.2 160 560 160C515.8 160 480 124.2 480 80zM0 456.1C0 445.6 2.964 435.3 8.551 426.4L225.3 81.01C231.9 70.42 243.5 64 256 64C268.5 64 280.1 70.42 286.8 81.01L412.7 281.7L460.9 202.7C464.1 196.1 472.2 192 480 192C487.8 192 495 196.1 499.1 202.7L631.1 419.1C636.9 428.6 640 439.7 640 450.9C640 484.6 612.6 512 578.9 512H55.91C25.03 512 .0006 486.1 .0006 456.1L0 456.1z"/></svg>
+ </div>
+ <div className="h-2 bg-gray-200 rounded-full w-10 mb-1"></div>
+ <div className="h-2.5 bg-gray-200 rounded-full w-full mb-4"></div>
+ <div className="h-2 bg-gray-200 rounded-full mb-2.5"></div>
+ <div className="h-2 bg-gray-200 rounded-full mb-2.5"></div>
+ <div className="h-2 bg-gray-200 rounded-full"></div>
+ <span className="sr-only">Loading...</span>
+ </div>
+)
+
+export default ProductCardSkeleton \ No newline at end of file
diff --git a/src/core/components/layouts/AnimationLayout.jsx b/src/core/components/layouts/AnimationLayout.jsx
new file mode 100644
index 00000000..cdd2d059
--- /dev/null
+++ b/src/core/components/layouts/AnimationLayout.jsx
@@ -0,0 +1,22 @@
+import { motion } from 'framer-motion'
+
+const AnimationLayout = ({ children, ...props }) => {
+ const transition = {
+ ease: 'easeOut',
+ duration: 0.3
+ }
+
+ return children && (
+ <motion.main
+ initial={{ opacity: 0, x: 0, y: 0 }}
+ animate={{ opacity: 1, x: 0, y: 0 }}
+ exit={{ opacity: 0, x: 15, y: 0 }}
+ transition={transition}
+ {...props}
+ >
+ { children }
+ </motion.main>
+ )
+}
+
+export default AnimationLayout \ No newline at end of file
diff --git a/src/core/components/layouts/AppLayout.jsx b/src/core/components/layouts/AppLayout.jsx
new file mode 100644
index 00000000..7aaa52ca
--- /dev/null
+++ b/src/core/components/layouts/AppLayout.jsx
@@ -0,0 +1,15 @@
+import AppBar from "../elements/Appbar/Appbar"
+import AnimationLayout from "./AnimationLayout"
+
+const AppLayout = ({ children, title }) => {
+ return (
+ <>
+ <AppBar title={title}/>
+ <AnimationLayout>
+ { children }
+ </AnimationLayout>
+ </>
+ )
+}
+
+export default AppLayout \ No newline at end of file
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
new file mode 100644
index 00000000..32c785e5
--- /dev/null
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -0,0 +1,15 @@
+import NavBar from "../elements/NavBar/NavBar"
+import AnimationLayout from "./AnimationLayout"
+
+const BasicLayout = ({ children }) => {
+ return (
+ <>
+ <NavBar />
+ <AnimationLayout>
+ { children }
+ </AnimationLayout>
+ </>
+ )
+}
+
+export default BasicLayout \ No newline at end of file
diff --git a/src/core/hooks/useActive.js b/src/core/hooks/useActive.js
new file mode 100644
index 00000000..e3a371cb
--- /dev/null
+++ b/src/core/hooks/useActive.js
@@ -0,0 +1,19 @@
+import { useState } from "react"
+
+const useActive = () => {
+ const [ active, setActive ] = useState(false)
+
+ const activate = () => {
+ setActive(true)
+ }
+
+ const deactivate = () => {
+ setActive(false)
+ }
+
+ return {
+ activate, deactivate, active
+ }
+}
+
+export default useActive \ No newline at end of file
diff --git a/src/core/hooks/useAuth.js b/src/core/hooks/useAuth.js
new file mode 100644
index 00000000..488562f6
--- /dev/null
+++ b/src/core/hooks/useAuth.js
@@ -0,0 +1,14 @@
+import { getAuth } from "../utils/auth"
+
+const useAuth = () => {
+ const [auth, setAuth] = useState(null)
+
+ useEffect(() => {
+ const handleIsAuthenticated = () => setAuth(getAuth())
+ handleIsAuthenticated()
+ }, [])
+
+ return [auth, setAuth]
+}
+
+export default useAuth \ No newline at end of file
diff --git a/src/core/utils/auth.js b/src/core/utils/auth.js
index 62eba2c0..6aeba02b 100644
--- a/src/core/utils/auth.js
+++ b/src/core/utils/auth.js
@@ -1,38 +1,29 @@
-import { deleteCookie, getCookie, setCookie } from 'cookies-next';
-import { useEffect, useState } from 'react';
+import {
+ deleteCookie,
+ getCookie,
+ setCookie
+} from 'cookies-next'
const getAuth = () => {
- let auth = getCookie('auth');
+ let auth = getCookie('auth')
if (auth) {
- return JSON.parse(auth);
+ return JSON.parse(auth)
}
- return false;
+ return false
}
const setAuth = (user) => {
- setCookie('auth', JSON.stringify(user));
- return true;
+ setCookie('auth', JSON.stringify(user))
+ return true
}
const deleteAuth = () => {
- deleteCookie('auth');
- return true;
-}
-
-const useAuth = () => {
- const [auth, setAuth] = useState(null);
-
- useEffect(() => {
- const handleIsAuthenticated = () => setAuth(getAuth());
- handleIsAuthenticated();
- }, []);
-
- return [auth, setAuth];
+ deleteCookie('auth')
+ return true
}
export {
getAuth,
setAuth,
- deleteAuth,
- useAuth
-}; \ No newline at end of file
+ deleteAuth
+} \ No newline at end of file
diff --git a/src/core/utils/cart.js b/src/core/utils/cart.js
index 66efcbf2..291d511b 100644
--- a/src/core/utils/cart.js
+++ b/src/core/utils/cart.js
@@ -1,36 +1,37 @@
const getCart = () => {
- const cart = localStorage.getItem('cart');
- if (cart) return JSON.parse(cart);
- return {};
+ const cart = localStorage.getItem('cart')
+ if (cart) return JSON.parse(cart)
+ return {}
}
const setCart = (cart) => {
- localStorage.setItem('cart', JSON.stringify(cart));
- return true;
+ localStorage.setItem('cart', JSON.stringify(cart))
+ return true
}
-const getItemCart = (product_id) => {
- let cart = getCart();
- return cart[product_id];
+const getItemCart = ({ productId }) => {
+ let cart = getCart()
+ return cart[productId]
}
-const createOrUpdateItemCart = (product_id, quantity, selected = false) => {
- let cart = getCart();
- cart[product_id] = { product_id, quantity, selected };
- setCart(cart);
- return true;
+const addItemCart = ({ productId, quantity, selected = false }) => {
+ let cart = getCart()
+ quantity = parseInt(quantity)
+ cart[productId] = { productId, quantity, selected }
+ setCart(cart)
+ return true
}
-const deleteItemCart = (product_id) => {
- let cart = getCart();
- delete cart[product_id];
- setCart(cart);
- return true;
+const deleteItemCart = ({ productId }) => {
+ let cart = getCart()
+ delete cart[productId]
+ setCart(cart)
+ return true
}
export {
getCart,
getItemCart,
- createOrUpdateItemCart,
+ addItemCart,
deleteItemCart
} \ No newline at end of file
diff --git a/src/core/utils/currencyFormat.js b/src/core/utils/currencyFormat.js
index dadeaec6..31f4a8dc 100644
--- a/src/core/utils/currencyFormat.js
+++ b/src/core/utils/currencyFormat.js
@@ -1,8 +1,10 @@
-export default function currencyFormat(value) {
+const currencyFormat = (value) => {
const currency = new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0
- });
- return currency.format(value);
-} \ No newline at end of file
+ })
+ return currency.format(value)
+}
+
+export default currencyFormat \ No newline at end of file
diff --git a/src/core/utils/slug.js b/src/core/utils/slug.js
index 0a7d30fc..fab37330 100644
--- a/src/core/utils/slug.js
+++ b/src/core/utils/slug.js
@@ -1,25 +1,25 @@
-import toTitleCase from './toTitleCase';
+import toTitleCase from './toTitleCase'
-const createSlug = (name, id) => {
- let slug = name?.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id;
- let splitSlug = slug.split('-');
- let filterSlugFromEmptyChar = splitSlug.filter(x => x != '');
- return filterSlugFromEmptyChar.join('-');
+const createSlug = (prefix, name, id) => {
+ let slug = name?.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id
+ let splitSlug = slug.split('-')
+ let filterSlugFromEmptyChar = splitSlug.filter(x => x != '')
+ return prefix + filterSlugFromEmptyChar.join('-')
}
const getIdFromSlug = (slug) => {
- let id = slug.split('-');
- return id[id.length-1];
+ let id = slug.split('-')
+ return id[id.length-1]
}
const getNameFromSlug = (slug) => {
- let name = slug.split('-');
- name.pop();
- return toTitleCase(name.join(' '));
+ let name = slug.split('-')
+ name.pop()
+ return toTitleCase(name.join(' '))
}
export {
createSlug,
getIdFromSlug,
getNameFromSlug
-}; \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/core/utils/toTitleCase.js b/src/core/utils/toTitleCase.js
index 5cfd70d0..b2751f0b 100644
--- a/src/core/utils/toTitleCase.js
+++ b/src/core/utils/toTitleCase.js
@@ -1,8 +1,10 @@
-export default function toTitleCase(str) {
+const toTitleCase = (str) => {
return str.replace(
/\w\S*/g,
function(txt) {
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
}
);
-} \ No newline at end of file
+}
+
+export default toTitleCase \ No newline at end of file
diff --git a/src/lib/brand/api/BrandApi.js b/src/lib/brand/api/BrandApi.js
new file mode 100644
index 00000000..15634cc4
--- /dev/null
+++ b/src/lib/brand/api/BrandApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const BrandApi = async ({ id }) => {
+ const dataBrand = await odooApi('GET', `/api/v1/manufacture/${id}`)
+ return dataBrand
+}
+
+export default BrandApi \ No newline at end of file
diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx
new file mode 100644
index 00000000..a42f4c81
--- /dev/null
+++ b/src/lib/brand/components/Brand.jsx
@@ -0,0 +1,70 @@
+import useBrand from "../hooks/useBrand"
+import Image from "@/core/components/elements/Image/Image"
+
+import { Swiper, SwiperSlide } from "swiper/react"
+import { Pagination, Autoplay } from "swiper"
+import "swiper/css"
+import "swiper/css/pagination"
+import "swiper/css/autoplay"
+import Divider from "@/core/components/elements/Divider/Divider"
+import ImageSkeleton from "@/core/components/elements/Skeleton/ImageSkeleton"
+
+const swiperBanner = {
+ pagination: { dynamicBullets: true },
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ modules: [Pagination, Autoplay]
+}
+
+const Brand = ({ id }) => {
+ const { brand } = useBrand({ id })
+
+ return (
+ <>
+ <div className="min-h-[200px]">
+ { brand.isLoading && <ImageSkeleton /> }
+ { brand.data && (
+ <>
+ <Swiper
+ slidesPerView={1}
+ pagination={swiperBanner.pagination}
+ modules={swiperBanner.modules}
+ autoplay={swiperBanner.autoplay}
+ className="border-b border-gray_r-6"
+ >
+ { brand.data?.banners?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner}
+ alt={`Brand ${brand.data?.name} - Indoteknik`}
+ className="w-full h-auto"
+ />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ <div className="p-4 pt-2">
+ <div className="text-caption-1 text-gray_r-11 mb-2">Produk dari brand:</div>
+ { brand?.data?.logo && (
+ <Image
+ src={brand?.data?.logo}
+ alt={brand?.data?.name}
+ className="w-32 p-2 border borde-gray_r-6 rounded"
+ />
+ ) }
+ { !brand?.data?.logo && (
+ <div className="bg-red_r-10 text-white text-center text-caption-1 py-2 px-4 rounded w-fit">
+ { brand?.data?.name }
+ </div>
+ ) }
+ </div>
+ </>
+ ) }
+ </div>
+ <Divider />
+ </>
+ )
+}
+
+export default Brand \ No newline at end of file
diff --git a/src/lib/brand/components/BrandCard.jsx b/src/lib/brand/components/BrandCard.jsx
new file mode 100644
index 00000000..8783010e
--- /dev/null
+++ b/src/lib/brand/components/BrandCard.jsx
@@ -0,0 +1,20 @@
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import { createSlug } from "@/core/utils/slug"
+
+const BrandCard = ({ brand }) => {
+ return (
+ <Link
+ href={createSlug('/shop/brands/', brand.name, brand.id)}
+ className="py-1 px-2 rounded border border-gray_r-6"
+ >
+ <Image
+ src={brand.logo}
+ alt={brand.name}
+ className="h-10 object-contain object-center"
+ />
+ </Link>
+ )
+}
+
+export default BrandCard \ No newline at end of file
diff --git a/src/lib/brand/hooks/useBrand.js b/src/lib/brand/hooks/useBrand.js
new file mode 100644
index 00000000..be42a44c
--- /dev/null
+++ b/src/lib/brand/hooks/useBrand.js
@@ -0,0 +1,13 @@
+import { useQuery } from "react-query"
+import BrandApi from "../api/BrandApi"
+
+const useBrand = ({ id }) => {
+ const fetchBrand = async () => await BrandApi({ id })
+ const { data, isLoading } = useQuery(`brand-${id}`, fetchBrand)
+
+ return {
+ brand: { data, isLoading }
+ }
+}
+
+export default useBrand \ No newline at end of file
diff --git a/src/lib/cart/api/CartApi.js b/src/lib/cart/api/CartApi.js
new file mode 100644
index 00000000..9a5b5053
--- /dev/null
+++ b/src/lib/cart/api/CartApi.js
@@ -0,0 +1,11 @@
+import odooApi from "@/core/api/odooApi"
+
+const CartApi = async ({ variantIds }) => {
+ if (variantIds) {
+ const dataCart = await odooApi('GET', `/api/v1/product_variant/${variantIds}`)
+ return dataCart
+ }
+ return null
+}
+
+export default CartApi \ No newline at end of file
diff --git a/src/lib/cart/components/Cart.jsx b/src/lib/cart/components/Cart.jsx
new file mode 100644
index 00000000..5f9ae1c0
--- /dev/null
+++ b/src/lib/cart/components/Cart.jsx
@@ -0,0 +1,30 @@
+import Link from "@/core/components/elements/Link/Link"
+import useCart from "../hooks/useCart"
+import Image from "@/core/components/elements/Image/Image"
+
+const Cart = () => {
+ const { cart } = useCart()
+
+ return (
+ <div className="p-4">
+ <div className="flex justify-between mb-4">
+ <h1 className="font-semibold">Daftar Produk Belanja</h1>
+ <Link href="/">Cari Produk Lain</Link>
+ </div>
+ <div className="flex flex-col gap-y-4">
+ { cart.data?.map((product) => (
+ <div key={product.id} className="flex">
+ <div className="w-4/12">
+ <Image src={product?.parent?.image} alt={product?.name} className="object-contain object-center border border-gray_r-6 h-32 w-full rounded-md" />
+ </div>
+ <div className="flex-1 px-2">
+ <div>{ product?.parent?.name }</div>
+ </div>
+ </div>
+ )) }
+ </div>
+ </div>
+ )
+}
+
+export default Cart \ No newline at end of file
diff --git a/src/lib/cart/hooks/useCart.js b/src/lib/cart/hooks/useCart.js
new file mode 100644
index 00000000..44931b8a
--- /dev/null
+++ b/src/lib/cart/hooks/useCart.js
@@ -0,0 +1,17 @@
+import { getCart } from "@/core/utils/cart"
+import { useQuery } from "react-query"
+import _ from "lodash"
+import CartApi from "../api/CartApi"
+
+const useCart = () => {
+ const cart = getCart()
+ const variantIds = _.keys(cart).join(',')
+ const fetchCart = async () => CartApi({ variantIds })
+ const { data, isLoading } = useQuery('cart', fetchCart)
+
+ return {
+ cart: { data, isLoading }
+ }
+}
+
+export default useCart \ No newline at end of file
diff --git a/src/lib/home/api/categoryHomeApi.js b/src/lib/home/api/categoryHomeApi.js
new file mode 100644
index 00000000..efb31240
--- /dev/null
+++ b/src/lib/home/api/categoryHomeApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const categoryHomeIdApi = async ({ id }) => {
+ const dataCategoryHomeId = await odooApi('GET', `/api/v1/categories_homepage?id=${id}`)
+ return dataCategoryHomeId
+}
+
+export default categoryHomeIdApi \ No newline at end of file
diff --git a/src/lib/home/api/categoryHomeIdApi.js b/src/lib/home/api/categoryHomeIdApi.js
new file mode 100644
index 00000000..d5612195
--- /dev/null
+++ b/src/lib/home/api/categoryHomeIdApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const categoryHomeIdApi = async () => {
+ const dataCategoryHomeIds = await odooApi('GET', '/api/v1/categories_homepage/ids')
+ return dataCategoryHomeIds
+}
+
+export default categoryHomeIdApi \ No newline at end of file
diff --git a/src/lib/home/api/heroBannerApi.js b/src/lib/home/api/heroBannerApi.js
new file mode 100644
index 00000000..7ba84bc6
--- /dev/null
+++ b/src/lib/home/api/heroBannerApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const heroBannerApi = async () => {
+ const dataHeroBanners = await odooApi('GET', '/api/v1/banner?type=index-a-1')
+ return dataHeroBanners
+}
+
+export default heroBannerApi \ No newline at end of file
diff --git a/src/lib/home/api/popularProductApi.js b/src/lib/home/api/popularProductApi.js
new file mode 100644
index 00000000..d7adca83
--- /dev/null
+++ b/src/lib/home/api/popularProductApi.js
@@ -0,0 +1,8 @@
+import axios from "axios"
+
+const popularProductApi = async () => {
+ const dataPopularProducts = await axios(`${process.env.SELF_HOST}/api/shop/search?q=*&page=1&orderBy=popular`)
+ return dataPopularProducts.data.response
+}
+
+export default popularProductApi \ No newline at end of file
diff --git a/src/lib/home/api/preferredBrandApi.js b/src/lib/home/api/preferredBrandApi.js
new file mode 100644
index 00000000..f289f387
--- /dev/null
+++ b/src/lib/home/api/preferredBrandApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const preferredBrandApi = async () => {
+ const dataPreferredBrands = await odooApi('GET', '/api/v1/manufacture?level=prioritas')
+ return dataPreferredBrands
+}
+
+export default preferredBrandApi \ No newline at end of file
diff --git a/src/lib/home/components/CategoryHome.jsx b/src/lib/home/components/CategoryHome.jsx
new file mode 100644
index 00000000..0bca9846
--- /dev/null
+++ b/src/lib/home/components/CategoryHome.jsx
@@ -0,0 +1,28 @@
+import ProductSlider from "@/lib/product/components/ProductSlider"
+import useCategoryHome from "../hooks/useCategoryHome"
+import PopularProductSkeleton from "./Skeleton/PopularProductSkeleton"
+
+const CategoryHome = ({ id }) => {
+ const { categoryHome } = useCategoryHome({ id })
+
+ return (
+ <div className="p-4 relative bg-yellow_r-2">
+ { categoryHome.data ? (
+ <ProductSlider
+ products={{
+ products: categoryHome.data?.[0].products,
+ banner: {
+ image: categoryHome.data?.[0].image,
+ name: categoryHome.data?.[0].name,
+ url: `/shop/search?category=${categoryHome.data?.[0].name}`
+ }
+ }}
+ simpleTitle
+ bannerMode
+ />
+ ) : <PopularProductSkeleton /> }
+ </div>
+ )
+}
+
+export default CategoryHome \ No newline at end of file
diff --git a/src/lib/home/components/CategoryHomeId.jsx b/src/lib/home/components/CategoryHomeId.jsx
new file mode 100644
index 00000000..4cbbd1fc
--- /dev/null
+++ b/src/lib/home/components/CategoryHomeId.jsx
@@ -0,0 +1,19 @@
+import { LazyLoadComponent } from "react-lazy-load-image-component"
+import useCategoryHomeId from "../hooks/useCategoryHomeId"
+import CategoryHome from "./CategoryHome"
+
+const CategoryHomeId = () => {
+ const { categoryHomeIds } = useCategoryHomeId()
+
+ return (
+ <div className="flex flex-col gap-y-6">
+ { categoryHomeIds.data?.map((id) => (
+ <LazyLoadComponent key={id}>
+ <CategoryHome id={id} />
+ </LazyLoadComponent>
+ )) }
+ </div>
+ )
+}
+
+export default CategoryHomeId \ No newline at end of file
diff --git a/src/lib/home/components/HeroBanner.jsx b/src/lib/home/components/HeroBanner.jsx
new file mode 100644
index 00000000..604ca8ac
--- /dev/null
+++ b/src/lib/home/components/HeroBanner.jsx
@@ -0,0 +1,50 @@
+import ImageSkeleton from "@/core/components/elements/Skeleton/ImageSkeleton"
+import useHeroBanner from "../hooks/useHeroBanner"
+import Image from "@/core/components/elements/Image/Image"
+
+// Swiper
+import { Swiper, SwiperSlide } from "swiper/react"
+import { Pagination, Autoplay } from "swiper"
+import "swiper/css"
+import "swiper/css/pagination"
+import "swiper/css/autoplay"
+
+const swiperBanner = {
+ pagination: { dynamicBullets: true },
+ autoplay: {
+ delay: 6000,
+ disableOnInteraction: false
+ },
+ modules: [Pagination, Autoplay]
+}
+
+const HeroBanner = () => {
+ const { heroBanners } = useHeroBanner()
+
+ return (
+ <div className="min-h-[200px]">
+ { heroBanners.isLoading && <ImageSkeleton /> }
+ { !heroBanners.isLoading && (
+ <Swiper
+ slidesPerView={1}
+ pagination={swiperBanner.pagination}
+ modules={swiperBanner.modules}
+ autoplay={swiperBanner.autoplay}
+ className="border-b border-gray_r-6"
+ >
+ { heroBanners.data?.map((banner, index) => (
+ <SwiperSlide key={index}>
+ <Image
+ src={banner.image}
+ alt={banner.name}
+ className="w-full h-auto"
+ />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ ) }
+ </div>
+ )
+}
+
+export default HeroBanner \ No newline at end of file
diff --git a/src/lib/home/components/PopularProduct.jsx b/src/lib/home/components/PopularProduct.jsx
new file mode 100644
index 00000000..87e47218
--- /dev/null
+++ b/src/lib/home/components/PopularProduct.jsx
@@ -0,0 +1,24 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import usePopularProduct from "../hooks/usePopularProduct"
+import ProductCard from "@/lib/product/components/ProductCard"
+import PopularProductSkeleton from "./Skeleton/PopularProductSkeleton"
+import ProductSlider from "@/lib/product/components/ProductSlider"
+
+const PopularProduct = () => {
+ const { popularProducts } = usePopularProduct()
+
+ return (
+ <div className="px-4">
+ <div className="font-medium mb-4">Produk Populer</div>
+ { popularProducts.isLoading && <PopularProductSkeleton /> }
+ { !popularProducts.isLoading && (
+ <ProductSlider
+ products={popularProducts.data}
+ simpleTitle
+ />
+ ) }
+ </div>
+ )
+}
+
+export default PopularProduct \ No newline at end of file
diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx
new file mode 100644
index 00000000..3d3b1b69
--- /dev/null
+++ b/src/lib/home/components/PreferredBrand.jsx
@@ -0,0 +1,30 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import usePreferredBrand from "../hooks/usePreferredBrand"
+import PreferredBrandSkeleton from "./Skeleton/PreferredBrandSkeleton"
+import BrandCard from "@/lib/brand/components/BrandCard"
+
+const PreferredBrand = () => {
+ const { preferredBrands } = usePreferredBrand()
+
+ return (
+ <div className="px-4">
+ <div className="font-medium mb-4">Brand Pilihan</div>
+ { preferredBrands.isLoading && <PreferredBrandSkeleton /> }
+ { !preferredBrands.isLoading && (
+ <Swiper
+ slidesPerView={3.5}
+ spaceBetween={8}
+ freeMode
+ >
+ { preferredBrands.data?.manufactures.map((brand) => (
+ <SwiperSlide key={brand.id}>
+ <BrandCard brand={brand} />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ ) }
+ </div>
+ )
+}
+
+export default PreferredBrand \ No newline at end of file
diff --git a/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
new file mode 100644
index 00000000..c5b0fcaa
--- /dev/null
+++ b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
@@ -0,0 +1,10 @@
+import ProductCardSkeleton from "@/core/components/elements/Skeleton/ProductCardSkeleton"
+
+const PopularProductSkeleton = () => (
+ <div className="grid grid-cols-2 gap-x-3">
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+)
+
+export default PopularProductSkeleton \ No newline at end of file
diff --git a/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx b/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx
new file mode 100644
index 00000000..6bdd3c82
--- /dev/null
+++ b/src/lib/home/components/Skeleton/PreferredBrandSkeleton.jsx
@@ -0,0 +1,12 @@
+import BrandSkeleton from "@/core/components/elements/Skeleton/BrandSkeleton"
+
+const PreferredBrandSkeleton = () => (
+ <div className="grid grid-cols-4 gap-x-3">
+ <BrandSkeleton />
+ <BrandSkeleton />
+ <BrandSkeleton />
+ <BrandSkeleton />
+ </div>
+)
+
+export default PreferredBrandSkeleton \ No newline at end of file
diff --git a/src/lib/home/hooks/useCategoryHome.js b/src/lib/home/hooks/useCategoryHome.js
new file mode 100644
index 00000000..14ef2a0f
--- /dev/null
+++ b/src/lib/home/hooks/useCategoryHome.js
@@ -0,0 +1,13 @@
+import categoryHomeApi from "../api/categoryHomeApi"
+import { useQuery } from "react-query"
+
+const useCategoryHome = ({ id }) => {
+ const fetchCategoryHome = async () => await categoryHomeApi({ id })
+ const { isLoading, data } = useQuery(`categoryHome-${id}`, fetchCategoryHome)
+
+ return {
+ categoryHome: { data, isLoading }
+ }
+}
+
+export default useCategoryHome \ No newline at end of file
diff --git a/src/lib/home/hooks/useCategoryHomeId.js b/src/lib/home/hooks/useCategoryHomeId.js
new file mode 100644
index 00000000..bb61b655
--- /dev/null
+++ b/src/lib/home/hooks/useCategoryHomeId.js
@@ -0,0 +1,13 @@
+import categoryHomeIdApi from "../api/categoryHomeIdApi"
+import { useQuery } from "react-query"
+
+const useCategoryHomeId = () => {
+ const fetchCategoryHomeId = async () => await categoryHomeIdApi()
+ const { isLoading, data } = useQuery("categoryHomeId", fetchCategoryHomeId)
+
+ return {
+ categoryHomeIds: { data, isLoading }
+ }
+}
+
+export default useCategoryHomeId \ No newline at end of file
diff --git a/src/lib/home/hooks/useHeroBanner.js b/src/lib/home/hooks/useHeroBanner.js
new file mode 100644
index 00000000..a15dda60
--- /dev/null
+++ b/src/lib/home/hooks/useHeroBanner.js
@@ -0,0 +1,13 @@
+import heroBannerApi from "../api/heroBannerApi"
+import { useQuery } from "react-query"
+
+const useHeroBanner = () => {
+ const fetchHeroBanner = async () => await heroBannerApi()
+ const { isLoading, data } = useQuery("heroBanner", fetchHeroBanner)
+
+ return {
+ heroBanners: { data, isLoading }
+ }
+}
+
+export default useHeroBanner \ No newline at end of file
diff --git a/src/lib/home/hooks/usePopularProduct.js b/src/lib/home/hooks/usePopularProduct.js
new file mode 100644
index 00000000..f69c2f71
--- /dev/null
+++ b/src/lib/home/hooks/usePopularProduct.js
@@ -0,0 +1,13 @@
+import popularProductApi from "../api/popularProductApi"
+import { useQuery } from "react-query"
+
+const usePopularProduct = () => {
+ const fetchPopularProduct = async () => await popularProductApi()
+ const { data, isLoading } = useQuery('popularProduct', fetchPopularProduct)
+
+ return {
+ popularProducts: { data, isLoading }
+ }
+}
+
+export default usePopularProduct \ No newline at end of file
diff --git a/src/lib/home/hooks/usePreferredBrand.js b/src/lib/home/hooks/usePreferredBrand.js
new file mode 100644
index 00000000..4be9793e
--- /dev/null
+++ b/src/lib/home/hooks/usePreferredBrand.js
@@ -0,0 +1,13 @@
+import preferredBrandApi from "../api/preferredBrandApi"
+import { useQuery } from "react-query"
+
+const usePreferredBrand = () => {
+ const fetchPreferredBrand = async () => await preferredBrandApi()
+ const { data, isLoading } = useQuery('preferredBrand', fetchPreferredBrand)
+
+ return {
+ preferredBrands: { data, isLoading }
+ }
+}
+
+export default usePreferredBrand \ No newline at end of file
diff --git a/src/lib/product/api/productApi.js b/src/lib/product/api/productApi.js
new file mode 100644
index 00000000..a543f086
--- /dev/null
+++ b/src/lib/product/api/productApi.js
@@ -0,0 +1,8 @@
+import odooApi from "@/core/api/odooApi"
+
+const productApi = async ({ id }) => {
+ const dataProduct = await odooApi('GET', `/api/v1/product/${id}`)
+ return dataProduct
+}
+
+export default productApi \ No newline at end of file
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js
new file mode 100644
index 00000000..86b2914f
--- /dev/null
+++ b/src/lib/product/api/productSearchApi.js
@@ -0,0 +1,9 @@
+import _ from "lodash-contrib"
+import axios from "axios"
+
+const productSearchApi = async ({ query }) => {
+ const dataProductSearch = await axios(`${process.env.SELF_HOST}/api/shop/search?${query}`)
+ return dataProductSearch.data
+}
+
+export default productSearchApi \ No newline at end of file
diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js
new file mode 100644
index 00000000..1449d9ca
--- /dev/null
+++ b/src/lib/product/api/productSimilarApi.js
@@ -0,0 +1,8 @@
+import axios from "axios"
+
+const productSimilarApi = async ({ query }) => {
+ const dataProductSimilar = await axios(`${process.env.SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular`)
+ return dataProductSimilar.data.response
+}
+
+export default productSimilarApi \ No newline at end of file
diff --git a/src/lib/product/components/Product.jsx b/src/lib/product/components/Product.jsx
new file mode 100644
index 00000000..2a3624e7
--- /dev/null
+++ b/src/lib/product/components/Product.jsx
@@ -0,0 +1,276 @@
+import Badge from "@/core/components/elements/Badge/Badge"
+import Divider from "@/core/components/elements/Divider/Divider"
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import currencyFormat from "@/core/utils/currencyFormat"
+import { useEffect, useState } from "react"
+import Select from "react-select"
+import ProductSimilar from "./ProductSimilar"
+import LazyLoad from "react-lazy-load"
+import { toast } from "react-hot-toast"
+import { addItemCart } from "@/core/utils/cart"
+
+const informationTabOptions = [
+ { value: 'specification', label: 'Spesifikasi' },
+ { value: 'description', label: 'Deskripsi' },
+ { value: 'important', label: 'Info Penting' },
+]
+
+const Product = ({ product }) => {
+ const [ quantity, setQuantity ] = useState('1')
+ const [ selectedVariant, setSelectedVariant ] = useState(null)
+ const [ informationTab, setInformationTab ] = useState(null)
+
+ const [ activeVariant, setActiveVariant ] = useState({
+ id: product.id,
+ code: product.code,
+ name: product.name,
+ price: product.lowestPrice,
+ stock: product.stockTotal,
+ weight: product.weight,
+ })
+
+ const variantOptions = product.variants?.map((variant) => ({
+ value: variant.id,
+ label:
+ (variant.code ? `[${variant.code}] ` : '')
+ +
+ (variant.attributes.length > 0 ? variant.attributes.join(', ') : product.name)
+ }))
+
+ useEffect(() => {
+ if (!selectedVariant && variantOptions.length == 1) {
+ setSelectedVariant(variantOptions[0])
+ }
+ }, [selectedVariant, variantOptions])
+
+ useEffect(() => {
+ if (selectedVariant) {
+ const variant = product.variants.find(variant => variant.id == selectedVariant.value)
+ const variantAttributes = variant.attributes.length > 0 ? ' - ' + variant.attributes.join(', ') : ''
+ console.log(variant);
+ setActiveVariant({
+ id: variant.id,
+ code: variant.code,
+ name: variant.parent.name + variantAttributes,
+ price: variant.price,
+ stock: variant.stock,
+ weight: variant.weight
+ })
+ }
+ }, [selectedVariant, product])
+
+ useEffect(() => {
+ if (!informationTab) {
+ setInformationTab(informationTabOptions[0].value)
+ }
+ }, [informationTab])
+
+ const handleClickCart = () => {
+ if (!selectedVariant) {
+ toast.error('Pilih varian terlebih dahulu')
+ return
+ }
+ if (!quantity || quantity < 1 || isNaN(parseInt(quantity))) {
+ toast.error('Jumlah barang minimal 1')
+ return
+ }
+ addItemCart({
+ productId: activeVariant.id,
+ quantity
+ })
+ toast.success('Berhasil menambahkan ke keranjang')
+ }
+
+ return (
+ <>
+ <Image
+ src={product.image}
+ alt={product.name}
+ className="h-72 object-contain object-center w-full border-b border-gray_r-4 bg-white"
+ />
+
+ <div className="p-4">
+ <Link href="/" className="mb-2">{ product.manufacture?.name }</Link>
+ <h1 className="leading-6 font-medium">
+ {activeVariant?.name}
+ </h1>
+ { activeVariant?.price?.discountPercentage > 0 && (
+ <div className="flex gap-x-1 items-center mt-2">
+ <div className="text-gray_r-11 line-through text-caption-1">
+ {currencyFormat(activeVariant?.price?.priceDiscount)}
+ </div>
+ <Badge type="solid-red">
+ {activeVariant?.price?.discountPercentage}%
+ </Badge>
+ </div>
+ ) }
+ <h3 className="text-red_r-11 font-semibold mt-1">
+ { activeVariant?.price?.price > 0 ? currencyFormat(activeVariant?.price?.price) : (
+ <span className="text-gray_r-11 leading-6 font-normal">
+ Hubungi kami untuk dapatkan harga terbaik,&nbsp;
+ <a href="https://wa.me/" className="text-red_r-11 underline">klik disini</a>
+ </span>
+ ) }
+ </h3>
+ </div>
+
+ <Divider />
+
+ <div className="p-4">
+ <div>
+ <label className="flex justify-between">
+ Pilih Varian:
+ <span className="text-gray_r-11">
+ { product?.variantTotal } Varian
+ </span>
+ </label>
+ <Select
+ name="variant"
+ classNamePrefix="form-select"
+ options={variantOptions}
+ className="mt-2"
+ value={selectedVariant}
+ onChange={(option) => setSelectedVariant(option)}
+ isSearchable={product.variantTotal > 10}
+ />
+ </div>
+ <div className="mt-4 mb-2">Jumlah</div>
+ <div className="flex gap-x-3">
+ <div className="w-2/12">
+ <input
+ name="quantity"
+ type="number"
+ className="form-input"
+ value={quantity}
+ onChange={(e) => setQuantity(e.target.value)}
+ />
+ </div>
+ <button
+ type="button"
+ className="btn-yellow flex-1"
+ onClick={handleClickCart}
+ >
+ Keranjang
+ </button>
+ <button
+ type="button"
+ className="btn-solid-red flex-1"
+ >
+ Beli
+ </button>
+ </div>
+ </div>
+
+ <Divider />
+
+ <div className="p-4">
+ <h2 className="font-semibold">Informasi Produk</h2>
+ <div className="flex gap-x-4 mt-4 mb-3">
+ { informationTabOptions.map((option) => (
+ <TabButton
+ value={option.value}
+ key={option.value}
+ active={informationTab == option.value}
+ onClick={() => setInformationTab(option.value)}
+ >
+ {option.label}
+ </TabButton>
+ )) }
+ </div>
+
+ <TabContent
+ active={informationTab == 'specification'}
+ className="rounded border border-gray_r-6 divide-y divide-gray_r-6"
+ >
+ <SpecificationContent label="Jumlah Varian">
+ <span>{product?.variantTotal} Varian</span>
+ </SpecificationContent>
+ <SpecificationContent label="Nomor SKU">
+ <span>SKU-{product?.id}</span>
+ </SpecificationContent>
+ <SpecificationContent label="Part Number">
+ <span>{activeVariant?.code || '-'}</span>
+ </SpecificationContent>
+ <SpecificationContent label="Stok">
+ { activeVariant?.stock > 0 && (
+ <span className="flex gap-x-1.5">
+ <div className="badge-solid-red">Ready Stock</div>
+ <div className="badge-gray">
+ { activeVariant?.stock > 5 ? '> 5' : '< 5' }
+ </div>
+ </span>
+ ) }
+ { activeVariant?.stock == 0 && (
+ <a
+ href="https://wa.me"
+ className="text-red_r-11 font-medium"
+ >
+ Tanya Stok
+ </a>
+ ) }
+ </SpecificationContent>
+ <SpecificationContent label="Berat Barang">
+ { activeVariant?.weight > 0 && (
+ <span>{ activeVariant?.weight } KG</span>
+ ) }
+ { activeVariant?.weight == 0 && (
+ <a
+ href="https://wa.me"
+ className="text-red_r-11 font-medium"
+ >
+ Tanya Berat
+ </a>
+ ) }
+ </SpecificationContent>
+ </TabContent>
+
+ <TabContent
+ active={informationTab == 'description'}
+ className="leading-6 text-gray_r-11"
+ dangerouslySetInnerHTML={{__html: (product.description != '' ? product.description : 'Belum ada deskripsi produk.')}}
+ />
+ </div>
+
+ <Divider />
+
+ <div className="p-4">
+ <h2 className="font-semibold mb-4">Kamu Mungkin Juga Suka</h2>
+ <LazyLoad>
+ <ProductSimilar query={product?.name.split(' ').slice(1, 3).join(' ')} />
+ </LazyLoad>
+ </div>
+ </>
+ )
+}
+
+const TabButton = ({ children, active, ...props }) => {
+ const activeClassName = active ? 'text-red_r-11 border-b border-red_r-11' : 'text-gray_r-11'
+ return (
+ <button
+ {...props}
+ type="button"
+ className={`font-medium pb-1 ${activeClassName}`}
+ >
+ { children }
+ </button>
+ )
+}
+
+const TabContent = ({ children, active, className, ...props }) => (
+ <div
+ {...props}
+ className={`${active ? 'block' : 'hidden'} ${className}`}
+ >
+ { children }
+ </div>
+)
+
+const SpecificationContent = ({ children, label }) => (
+ <div className="flex justify-between p-3">
+ <span className="text-gray_r-11">{ label }</span>
+ { children }
+ </div>
+)
+
+export default Product \ No newline at end of file
diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx
new file mode 100644
index 00000000..86ac3a64
--- /dev/null
+++ b/src/lib/product/components/ProductCard.jsx
@@ -0,0 +1,68 @@
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import currencyFormat from "@/core/utils/currencyFormat"
+import { createSlug } from "@/core/utils/slug"
+
+const ProductCard = ({ product, simpleTitle }) => {
+ return (
+ <>
+ <div className="rounded shadow-sm border border-gray_r-4">
+ <Link
+ href={createSlug('/shop/product/', product?.name, product?.id)}
+ className="border-b border-gray_r-4 relative"
+ >
+ <Image
+ src={product?.image}
+ alt={product?.name}
+ className="w-full object-contain object-center h-36 bg-white"
+ />
+ { product.variantTotal > 1 && (
+ <div className="absolute badge-gray bottom-1.5 left-1.5">{ product.variantTotal } Varian</div>
+ ) }
+ </Link>
+ <div className="p-2 pb-3 text-caption-2 leading-5 bg-white">
+ <Link
+ href={createSlug('/shop/brands/', product?.manufacture?.name, product?.manufacture.id)}
+ className="mb-1"
+ >
+ {product?.manufacture?.name}
+ </Link>
+ <Link
+ href={createSlug('/shop/product/', product?.name, product?.id)}
+ className={`font-medium mb-2 !text-gray_r-12 ${simpleTitle ? 'line-clamp-2' : 'line-clamp-3'}`}
+ >
+ {product?.name}
+ </Link>
+ { product?.lowestPrice?.discountPercentage > 0 && (
+ <div className="flex gap-x-1 mb-1 items-center">
+ <div className="text-gray_r-11 line-through text-[11px]">
+ {currencyFormat(product?.lowestPrice?.price)}
+ </div>
+ <div className="badge-solid-red">
+ {product?.lowestPrice?.discountPercentage}%
+ </div>
+ </div>
+ ) }
+
+ <div className="text-red_r-11 font-semibold mb-2">
+ { product?.lowestPrice?.priceDiscount > 0 ? currencyFormat(product?.lowestPrice?.priceDiscount) : (
+ <a href="https://wa.me/">Call for price</a>
+ ) }
+ </div>
+ { product?.stockTotal > 0 && (
+ <div className="flex gap-x-1">
+ <div className="badge-solid-red">
+ Ready Stock
+ </div>
+ <div className="badge-gray">
+ { product?.stockTotal > 5 ? '> 5' : '< 5' }
+ </div>
+ </div>
+ ) }
+ </div>
+ </div>
+ </>
+ )
+}
+
+export default ProductCard \ No newline at end of file
diff --git a/src/lib/product/components/ProductFilter.jsx b/src/lib/product/components/ProductFilter.jsx
new file mode 100644
index 00000000..023b6a8b
--- /dev/null
+++ b/src/lib/product/components/ProductFilter.jsx
@@ -0,0 +1,131 @@
+import BottomPopup from "@/core/components/elements/Popup/BottomPopup"
+import { useRouter } from "next/router"
+import { useState } from "react"
+import _ from "lodash"
+import { toQuery } from "lodash-contrib"
+
+const orderOptions = [
+ { value: 'price-asc', label: 'Harga Terendah' },
+ { value: 'price-desc', label: 'Harga Tertinggi' },
+ { value: 'popular', label: 'Populer' },
+ { value: 'stock', label: 'Ready Stock' },
+]
+
+const ProductFilter = ({
+ active,
+ close,
+ brands,
+ categories,
+ prefixUrl,
+ defaultBrand = null
+}) => {
+ const router = useRouter()
+ const { query } = router
+ const [ order, setOrder ] = useState(query?.orderBy)
+ const [ brand, setBrand ] = useState(query?.brand)
+ const [ category, setCategory ] = useState(query?.category)
+ const [ priceFrom, setPriceFrom ] = useState(query?.priceFrom)
+ const [ priceTo, setPriceTo ] = useState(query?.priceTo)
+
+ const handleSubmit = () => {
+ let params = {
+ q: router.query.q,
+ orderBy: order,
+ brand,
+ category,
+ priceFrom,
+ priceTo
+ }
+ params = _.pickBy(params, _.identity)
+ params = toQuery(params)
+ router.push(`${prefixUrl}?${params}`)
+ }
+
+ return (
+ <BottomPopup
+ active={active}
+ close={close}
+ title="Filter Produk"
+ >
+ <div className="flex flex-col gap-y-4">
+ { !defaultBrand && (
+ <div>
+ <label>Brand</label>
+ <select
+ name="brand"
+ className="form-input mt-2"
+ value={brand}
+ onChange={(e) => setBrand(e.target.value)}
+ >
+ <option value="">Pilih Brand...</option>
+ { brands.map((brand, index) => (
+ <option value={brand} key={index}>
+ {brand}
+ </option>
+ )) }
+ </select>
+ </div>
+ ) }
+ <div>
+ <label>Kategori</label>
+ <select
+ name="category"
+ className="form-input mt-2"
+ value={category}
+ onChange={(e) => setCategory(e.target.value)}
+ >
+ <option value="">Pilih Kategori...</option>
+ { categories.map((category, index) => (
+ <option value={category} key={index}>
+ {category}
+ </option>
+ )) }
+ </select>
+ </div>
+ <div>
+ <label>Urutkan</label>
+ <div className="flex mt-2 gap-x-2 overflow-x-auto">
+ { orderOptions.map((orderOption) => (
+ <button
+ key={orderOption.value}
+ className={`btn-light px-3 font-normal flex-shrink-0 ${order == orderOption.value ? 'bg-yellow_r-10' : 'bg-transparent'}`}
+ onClick={() => setOrder(orderOption.value)}
+ >
+ { orderOption.label }
+ </button>
+ )) }
+ </div>
+ </div>
+ <div>
+ <label>Harga</label>
+ <div className="flex mt-2 gap-x-4 items-center">
+ <input
+ type="number"
+ className="form-input"
+ placeholder="Dari"
+ value={priceFrom}
+ onChange={(e) => setPriceFrom(e.target.value)}
+ />
+ <span>—</span>
+ <input
+ type="number"
+ className="form-input"
+ placeholder="Sampai"
+ value={priceTo}
+ onChange={(e) => setPriceTo(e.target.value)}
+ />
+ </div>
+ </div>
+ <button
+ type="button"
+ className="btn-solid-red w-full mt-2"
+ onClick={handleSubmit}
+ >
+ Terapkan Filter
+ </button>
+ </div>
+ </BottomPopup>
+ )
+}
+
+export default ProductFilter \ No newline at end of file
diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx
new file mode 100644
index 00000000..14df9864
--- /dev/null
+++ b/src/lib/product/components/ProductSearch.jsx
@@ -0,0 +1,95 @@
+import { useEffect, useState } from "react"
+import useProductSearch from "../hooks/useProductSearch"
+import ProductCard from "./ProductCard"
+import Pagination from "@/core/components/elements/Pagination/Pagination"
+import { toQuery } from "lodash-contrib"
+import _ from "lodash"
+import ProductSearchSkeleton from "./Skeleton/ProductSearchSkeleton"
+import ProductFilter from "./ProductFilter"
+import useActive from "@/core/hooks/useActive"
+
+const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => {
+ const { page = 1 } = query
+ if (defaultBrand) query.brand = defaultBrand.toLowerCase()
+ const { productSearch } = useProductSearch({ query })
+ const [ products, setProducts ] = useState(null)
+ const popup = useActive()
+
+ const pageCount = Math.ceil(productSearch.data?.response.numFound / productSearch.data?.responseHeader.params.rows)
+ const productStart = productSearch.data?.responseHeader.params.start
+ const productRows = productSearch.data?.responseHeader.params.rows
+ const productFound = productSearch.data?.response.numFound
+
+ const brands = productSearch.data?.facetCounts?.facetFields?.brandStr?.filter((value, index) => {
+ if (index % 2 === 0) {
+ return true
+ }
+ })
+ const categories = productSearch.data?.facetCounts?.facetFields?.categoryNameStr?.filter((value, index) => {
+ if (index % 2 === 0) {
+ return true
+ }
+ })
+
+ useEffect(() => {
+ if (!products) {
+ setProducts(productSearch.data?.response?.products)
+ }
+ }, [query, products, productSearch])
+
+ if (productSearch.isLoading) {
+ return <ProductSearchSkeleton />
+ }
+
+ return (
+ <div className="p-4">
+ <h1 className="mb-2 font-semibold text-h-sm">Produk</h1>
+
+ <div className="mb-2 leading-6 text-gray_r-11">
+ { productFound > 0 ? (
+ <>
+ Menampilkan&nbsp;
+ {pageCount > 1 ? (
+ <>
+ {productStart + 1}-{
+ (productStart + productRows) > productFound ? productFound : productStart + productRows
+ }
+ &nbsp;dari&nbsp;
+ </>
+ ) : ''}
+ { productFound }
+ &nbsp;produk { query.q && (<>untuk pencarian <span className="font-semibold">{ query.q }</span></>) }
+ </>
+ ) : 'Mungkin yang anda cari'}
+ </div>
+
+ <button className="btn-light mb-6 py-2 px-5" onClick={popup.activate}>
+ Filter
+ </button>
+
+ <div className="grid grid-cols-2 gap-3">
+ { products && products.map((product) => (
+ <ProductCard product={product} key={product.id} />
+ )) }
+ </div>
+
+ <Pagination
+ pageCount={pageCount}
+ currentPage={parseInt(page)}
+ url={`${prefixUrl}?${toQuery(_.omit(query, ['page']))}`}
+ className="mt-6 mb-2"
+ />
+
+ <ProductFilter
+ active={popup.active}
+ close={popup.deactivate}
+ brands={brands || []}
+ categories={categories || []}
+ prefixUrl={prefixUrl}
+ defaultBrand={defaultBrand}
+ />
+ </div>
+ )
+}
+
+export default ProductSearch \ No newline at end of file
diff --git a/src/lib/product/components/ProductSimilar.jsx b/src/lib/product/components/ProductSimilar.jsx
new file mode 100644
index 00000000..89cab536
--- /dev/null
+++ b/src/lib/product/components/ProductSimilar.jsx
@@ -0,0 +1,15 @@
+import PopularProductSkeleton from "@/lib/home/components/Skeleton/PopularProductSkeleton"
+import useProductSimilar from "../hooks/useProductSimilar"
+import ProductSlider from "./ProductSlider"
+
+const ProductSimilar = ({ query }) => {
+ const { productSimilar } = useProductSimilar({ query })
+
+ if (productSimilar.isLoading) {
+ return <PopularProductSkeleton />
+ }
+
+ return <ProductSlider products={productSimilar.data} />
+}
+
+export default ProductSimilar \ No newline at end of file
diff --git a/src/lib/product/components/ProductSlider.jsx b/src/lib/product/components/ProductSlider.jsx
new file mode 100644
index 00000000..8d677547
--- /dev/null
+++ b/src/lib/product/components/ProductSlider.jsx
@@ -0,0 +1,51 @@
+import { Swiper, SwiperSlide } from "swiper/react"
+import ProductCard from "./ProductCard"
+import "swiper/css"
+import Image from "@/core/components/elements/Image/Image"
+import Link from "@/core/components/elements/Link/Link"
+import { useState } from "react"
+
+const bannerClassName = 'absolute rounded-r top-0 left-0 h-full max-w-[52%] idt-transition border border-gray_r-6'
+
+const ProductSlider = ({
+ products,
+ simpleTitle = false,
+ bannerMode = false
+}) => {
+ const [ activeIndex, setActiveIndex ] = useState(0)
+ const swiperSliderFirstMove = (swiper) => {
+ setActiveIndex(swiper.activeIndex)
+ }
+
+ return (
+ <>
+ { bannerMode && (
+ <Image
+ src={products.banner.image}
+ alt={products.banner.name}
+ className={`${bannerClassName} ${activeIndex > 0 ? 'opacity-0' : 'opacity-100'}`}
+ />
+ ) }
+ <Swiper
+ freeMode={true}
+ slidesPerView={2.2}
+ spaceBetween={8}
+ onSlideChange={swiperSliderFirstMove}
+ prefix="product"
+ >
+ { bannerMode && (
+ <SwiperSlide>
+ <Link href={products.banner.url} className="w-full h-full block"></Link>
+ </SwiperSlide>
+ ) }
+ { products?.products?.map((product, index) => (
+ <SwiperSlide key={index}>
+ <ProductCard product={product} simpleTitle={simpleTitle} />
+ </SwiperSlide>
+ )) }
+ </Swiper>
+ </>
+ )
+}
+
+export default ProductSlider \ No newline at end of file
diff --git a/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx b/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx
new file mode 100644
index 00000000..e51a565c
--- /dev/null
+++ b/src/lib/product/components/Skeleton/ProductSearchSkeleton.jsx
@@ -0,0 +1,14 @@
+import ProductCardSkeleton from "@/core/components/elements/Skeleton/ProductCardSkeleton"
+
+const ProductSearchSkeleton = () => (
+ <div className="p-4 grid grid-cols-2 gap-4">
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+)
+
+export default ProductSearchSkeleton \ No newline at end of file
diff --git a/src/lib/product/hooks/useProductSearch.js b/src/lib/product/hooks/useProductSearch.js
new file mode 100644
index 00000000..d23a8098
--- /dev/null
+++ b/src/lib/product/hooks/useProductSearch.js
@@ -0,0 +1,15 @@
+import { useQuery } from "react-query"
+import productSearchApi from "../api/productSearchApi"
+import _ from "lodash-contrib"
+
+const useProductSearch = ({ query }) => {
+ const queryString = _.toQuery(query)
+ const fetchProductSearch = async () => await productSearchApi({ query: queryString })
+ const { data, isLoading } = useQuery(`productSearch-${queryString}`, fetchProductSearch)
+
+ return {
+ productSearch: { data, isLoading }
+ }
+}
+
+export default useProductSearch \ No newline at end of file
diff --git a/src/lib/product/hooks/useProductSimilar.js b/src/lib/product/hooks/useProductSimilar.js
new file mode 100644
index 00000000..444fec0b
--- /dev/null
+++ b/src/lib/product/hooks/useProductSimilar.js
@@ -0,0 +1,13 @@
+import productSimilarApi from "../api/productSimilarApi"
+import { useQuery } from "react-query"
+
+const useProductSimilar = ({ query }) => {
+ const fetchProductSimilar = async () => await productSimilarApi({ query })
+ const { data, isLoading } = useQuery(`productSimilar-${query}`, fetchProductSimilar)
+
+ return {
+ productSimilar: { data, isLoading }
+ }
+}
+
+export default useProductSimilar \ No newline at end of file
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
new file mode 100644
index 00000000..33573480
--- /dev/null
+++ b/src/pages/_app.jsx
@@ -0,0 +1,36 @@
+import '../styles/globals.css'
+import NextProgress from 'next-progress'
+import { useRouter } from 'next/router'
+import { AnimatePresence } from 'framer-motion'
+import { Toaster } from "react-hot-toast"
+import { QueryClient, QueryClientProvider } from 'react-query'
+
+const queryClient = new QueryClient()
+
+function MyApp({ Component, pageProps }) {
+ const router = useRouter()
+
+ return (
+ <>
+ <Toaster
+ position="top-center"
+ toastOptions={{
+ duration: 3000,
+ className: 'border border-gray_r-8'
+ }}
+ />
+ <NextProgress color="#F01C21" options={{ showSpinner: false }} />
+ <QueryClientProvider client={queryClient}>
+ <AnimatePresence
+ mode='wait'
+ initial={false}
+ onExitComplete={() => window.scrollTo(0, 0)}
+ >
+ <Component {...pageProps} key={router.asPath} />
+ </AnimatePresence>
+ </QueryClientProvider>
+ </>
+ )
+}
+
+export default MyApp
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index ad986c86..5e5f1b6a 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -1,4 +1,5 @@
-import axios from "axios";
+import axios from "axios"
+import camelcaseObjectDeep from "camelcase-object-deep"
const productResponseMap = (products) => {
return products.map((product) => {
@@ -7,23 +8,23 @@ const productResponseMap = (products) => {
image: product.image ? product.image[0] : '',
code: product.default_code ? product.default_code[0] : '',
name: product.product_name ? product.product_name[0] : '',
- lowest_price: {
+ lowestPrice: {
price: product.price ? product.price[0] : 0,
- price_discount: product.price_discount ? product.price_discount[0] : 0,
- discount_percentage: product.discount ? product.discount[0] : 0,
+ priceDiscount: product.price_discount ? product.price_discount[0] : 0,
+ discountPercentage: product.discount ? product.discount[0] : 0,
},
- variant_total: product.variant_total ? product.variant_total[0] : 0,
- stock_total: product.stock_total ? product.stock_total[0] : 0,
+ variantTotal: product.variant_total ? product.variant_total[0] : 0,
+ stockTotal: product.stock_total ? product.stock_total[0] : 0,
weight: product.weight ? product.weight[0] : 0,
manufacture: {},
categories: [],
- };
+ }
if (product.manufacture_id && product.brand) {
productMapped.manufacture = {
id: product.manufacture_id ? product.manufacture_id[0] : '',
name: product.brand ? product.brand[0] : '',
- };
+ }
}
productMapped.categories = [
@@ -31,41 +32,41 @@ const productResponseMap = (products) => {
id: product.category_id ? product.category_id[0] : '',
name: product.category_name ? product.category_name[0] : '',
}
- ];
+ ]
- return productMapped;
- });
+ return productMapped
+ })
}
export default async function handler(req, res) {
const {
- q,
+ q = '*',
page = 1,
brand = '',
category = '',
- price_from = 0,
- price_to = 0,
- order_by = ''
- } = req.query;
+ priceFrom = 0,
+ priceTo = 0,
+ orderBy = ''
+ } = req.query
- let paramOrderBy = '';
- switch (order_by) {
+ let paramOrderBy = ''
+ switch (orderBy) {
case 'price-asc':
- paramOrderBy = ', price_discount ASC';
- break;
+ paramOrderBy = ', price_discount ASC'
+ break
case 'price-desc':
- paramOrderBy = ', price_discount DESC';
- break;
+ paramOrderBy = ', price_discount DESC'
+ break
case 'popular':
- paramOrderBy = ', search_rank DESC';
- break;
+ paramOrderBy = ', search_rank DESC'
+ break
case 'stock':
- paramOrderBy = ', stock_total DESC';
- break;
+ paramOrderBy = ', stock_total DESC'
+ break
}
- let limit = 30;
- let offset = (page - 1) * limit;
+ let limit = 30
+ let offset = (page - 1) * limit
let parameter = [
`facet.query=${q}`,
'facet=true',
@@ -77,20 +78,21 @@ export default async function handler(req, res) {
`start=${offset}`,
`rows=${limit}`,
`sort=product_rating DESC ${paramOrderBy}`,
- `fq=price_discount:[${price_from == '' ? '*' : price_from} TO ${price_to == '' ? '*' : price_to}]`
- ];
+ `fq=price_discount:[${priceFrom == '' ? '*' : priceFrom} TO ${priceTo == '' ? '*' : priceTo}]`
+ ]
- if (brand) parameter.push(`fq=brand:${brand}`);
- if (category) parameter.push(`fq=category_name:${category}`);
+ if (brand) parameter.push(`fq=brand:${brand}`)
+ if (category) parameter.push(`fq=category_name:${category}`)
- let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'));
+ let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'))
try {
- result.data.response.products = productResponseMap(result.data.response.docs);
- result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start);
- result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows);
- delete result.data.response.docs;
- res.status(200).json(result.data);
+ result.data.response.products = productResponseMap(result.data.response.docs)
+ result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start)
+ result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows)
+ delete result.data.response.docs
+ result.data = camelcaseObjectDeep(result.data)
+ res.status(200).json(result.data)
} catch (error) {
- res.status(400).json({ error: error.message });
+ res.status(400).json({ error: error.message })
}
} \ No newline at end of file
diff --git a/src/pages/api/shop/suggest.js b/src/pages/api/shop/suggest.js
index 6db1a851..4e373a92 100644
--- a/src/pages/api/shop/suggest.js
+++ b/src/pages/api/shop/suggest.js
@@ -1,12 +1,15 @@
import axios from "axios";
export default async function handler(req, res) {
- const { q } = req.query;
+ const { q = '' } = req.query;
let result = await axios(process.env.SOLR_HOST + `/solr/products/suggest?suggest=true&suggest.dictionary=mySuggester&suggest.q=${q}`);
try {
- res.status(200).json(result.data);
+ res.status(200).json(result.data.suggest.mySuggester[q]);
} catch (error) {
- res.status(400).json({ error: error.message });
+ res.status(400).json({
+ numFound: 0,
+ suggestions: []
+ });
}
} \ No newline at end of file
diff --git a/src/pages/index.jsx b/src/pages/index.jsx
new file mode 100644
index 00000000..05c6520a
--- /dev/null
+++ b/src/pages/index.jsx
@@ -0,0 +1,20 @@
+import Seo from "@/core/components/Seo"
+import BasicLayout from "@/core/components/layouts/BasicLayout"
+import CategoryHomeId from "@/lib/home/components/CategoryHomeId"
+import HeroBanner from "@/lib/home/components/HeroBanner"
+import PopularProduct from "@/lib/home/components/PopularProduct"
+import PreferredBrand from "@/lib/home/components/PreferredBrand"
+
+export default function Home() {
+ return (
+ <BasicLayout>
+ <Seo title="Beranda - Indoteknik" />
+ <HeroBanner />
+ <div className="flex flex-col gap-y-6 my-6">
+ <PreferredBrand />
+ <PopularProduct />
+ <CategoryHomeId />
+ </div>
+ </BasicLayout>
+ )
+} \ No newline at end of file
diff --git a/src/pages/shop/brands/[slug].jsx b/src/pages/shop/brands/[slug].jsx
new file mode 100644
index 00000000..4965d4f7
--- /dev/null
+++ b/src/pages/shop/brands/[slug].jsx
@@ -0,0 +1,23 @@
+import BasicLayout from "@/core/components/layouts/BasicLayout"
+import { getIdFromSlug, getNameFromSlug } from "@/core/utils/slug"
+import ProductSearch from "@/lib/product/components/ProductSearch"
+import { useRouter } from "next/router"
+import _ from "lodash"
+import Brand from "@/lib/brand/components/Brand"
+
+export default function BrandDetail() {
+ const router = useRouter()
+ const { slug = '' } = router.query
+ return (
+ <BasicLayout>
+ <Brand id={getIdFromSlug(slug)} />
+ { !_.isEmpty(router.query) && (
+ <ProductSearch
+ query={_.omit(router.query, 'slug')}
+ prefixUrl={`/shop/brands/${slug}`}
+ defaultBrand={getNameFromSlug(slug)}
+ />
+ ) }
+ </BasicLayout>
+ )
+} \ No newline at end of file
diff --git a/src/pages/shop/cart.jsx b/src/pages/shop/cart.jsx
new file mode 100644
index 00000000..20279e0c
--- /dev/null
+++ b/src/pages/shop/cart.jsx
@@ -0,0 +1,10 @@
+import AppLayout from "@/core/components/layouts/AppLayout"
+import CartComponent from "@/lib/cart/components/Cart"
+
+export default function Cart() {
+ return (
+ <AppLayout title="Keranjang">
+ <CartComponent />
+ </AppLayout>
+ )
+} \ No newline at end of file
diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx
new file mode 100644
index 00000000..84a3c7d4
--- /dev/null
+++ b/src/pages/shop/product/[slug].jsx
@@ -0,0 +1,29 @@
+import Seo from "@/core/components/Seo"
+import BasicLayout from "@/core/components/layouts/BasicLayout"
+import { getIdFromSlug } from "@/core/utils/slug"
+import productApi from "@/lib/product/api/productApi"
+import Product from "@/lib/product/components/Product"
+
+export async function getServerSideProps(context) {
+ const { slug } = context.query
+ let product = await productApi({ id: getIdFromSlug(slug) })
+ if (product?.length == 1) {
+ product = product[0]
+ product.description = product.description.replaceAll('<p>', '||p||')
+ product.description = product.description.replaceAll('</p>', '||/p||')
+ product.description = product.description.replace(/(<([^>]+)>)/gi, ' ')
+ product.description = product.description.replaceAll('||p||', '<p>')
+ product.description = product.description.replaceAll('||/p||', '</p>')
+ product.description = product.description.trim()
+ }
+ return { props: { product } }
+}
+
+export default function ProductDetail({ product }) {
+ return (
+ <BasicLayout>
+ <Seo title={product?.name} />
+ <Product product={product} />
+ </BasicLayout>
+ )
+} \ No newline at end of file
diff --git a/src/pages/shop/search.jsx b/src/pages/shop/search.jsx
new file mode 100644
index 00000000..e86b1f4e
--- /dev/null
+++ b/src/pages/shop/search.jsx
@@ -0,0 +1,19 @@
+import BasicLayout from "@/core/components/layouts/BasicLayout"
+import ProductSearch from "@/lib/product/components/ProductSearch"
+import { useRouter } from "next/router"
+import _ from "lodash-contrib"
+
+export default function Search() {
+ const router = useRouter()
+
+ return (
+ <BasicLayout>
+ { !_.isEmpty(router.query) && (
+ <ProductSearch
+ query={router.query}
+ prefixUrl="/shop/search"
+ />
+ ) }
+ </BasicLayout>
+ )
+} \ No newline at end of file
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b871a325..c457859b 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -18,6 +18,10 @@ html, body {
;
}
+button {
+ @apply block;
+}
+
@layer base {
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
@@ -28,28 +32,6 @@ html, body {
input[type=number] {
-moz-appearance:textfield;
}
-
- h1, .h1 {
- @apply
- text-h-md
- font-semibold
- ;
- }
-
- h2, .h2 {
- @apply
- text-body-2
- font-semibold
- leading-6
- ;
- }
-
- a {
- @apply
- font-medium
- text-red_r-11
- ;
- }
}
@layer components {
@@ -61,7 +43,7 @@ html, body {
.badge-green,
.badge-solid-green {
@apply
- text-caption-2
+ text-[11px]
leading-none
font-medium
px-1
@@ -313,7 +295,7 @@ html, body {
@apply translate-x-0;
}
-.menu-overlay {
+.overlay {
@apply
fixed
top-0
diff --git a/src/components/auth/WithAuth.js b/src2/components/auth/WithAuth.js
index ef975873..ef975873 100644
--- a/src/components/auth/WithAuth.js
+++ b/src2/components/auth/WithAuth.js
diff --git a/src/components/elements/Alert.js b/src2/components/elements/Alert.js
index 914d1590..914d1590 100644
--- a/src/components/elements/Alert.js
+++ b/src2/components/elements/Alert.js
diff --git a/src/components/elements/BottomPopup.js b/src2/components/elements/BottomPopup.js
index c1a56e10..c1a56e10 100644
--- a/src/components/elements/BottomPopup.js
+++ b/src2/components/elements/BottomPopup.js
diff --git a/src/components/elements/ConfirmAlert.js b/src2/components/elements/ConfirmAlert.js
index d33abb89..d33abb89 100644
--- a/src/components/elements/ConfirmAlert.js
+++ b/src2/components/elements/ConfirmAlert.js
diff --git a/src/components/elements/DescriptionRow.js b/src2/components/elements/DescriptionRow.js
index 7fe9e3a1..7fe9e3a1 100644
--- a/src/components/elements/DescriptionRow.js
+++ b/src2/components/elements/DescriptionRow.js
diff --git a/src/components/elements/Disclosure.js b/src2/components/elements/Disclosure.js
index 1f334be3..1f334be3 100644
--- a/src/components/elements/Disclosure.js
+++ b/src2/components/elements/Disclosure.js
diff --git a/src/components/elements/Fields.js b/src2/components/elements/Fields.js
index 586a6a22..586a6a22 100644
--- a/src/components/elements/Fields.js
+++ b/src2/components/elements/Fields.js
diff --git a/src/components/elements/Filter.js b/src2/components/elements/Filter.js
index f2051ba8..f2051ba8 100644
--- a/src/components/elements/Filter.js
+++ b/src2/components/elements/Filter.js
diff --git a/src/components/elements/Image.js b/src2/components/elements/Image.js
index 60e249b9..60e249b9 100644
--- a/src/components/elements/Image.js
+++ b/src2/components/elements/Image.js
diff --git a/src/components/elements/LineDivider.js b/src2/components/elements/LineDivider.js
index 4e8c7b52..4e8c7b52 100644
--- a/src/components/elements/LineDivider.js
+++ b/src2/components/elements/LineDivider.js
diff --git a/src/components/elements/Link.js b/src2/components/elements/Link.js
index 065b5c9e..065b5c9e 100644
--- a/src/components/elements/Link.js
+++ b/src2/components/elements/Link.js
diff --git a/src/components/elements/Pagination.js b/src2/components/elements/Pagination.js
index ff2a8462..ff2a8462 100644
--- a/src/components/elements/Pagination.js
+++ b/src2/components/elements/Pagination.js
diff --git a/src/components/elements/ProgressBar.js b/src2/components/elements/ProgressBar.js
index 0adedcdf..0adedcdf 100644
--- a/src/components/elements/ProgressBar.js
+++ b/src2/components/elements/ProgressBar.js
diff --git a/src/components/elements/Skeleton.js b/src2/components/elements/Skeleton.js
index fbdbc245..fbdbc245 100644
--- a/src/components/elements/Skeleton.js
+++ b/src2/components/elements/Skeleton.js
diff --git a/src/components/elements/Spinner.js b/src2/components/elements/Spinner.js
index 21006ecd..21006ecd 100644
--- a/src/components/elements/Spinner.js
+++ b/src2/components/elements/Spinner.js
diff --git a/src/components/layouts/AppBar.js b/src2/components/layouts/AppBar.js
index fe74c940..fe74c940 100644
--- a/src/components/layouts/AppBar.js
+++ b/src2/components/layouts/AppBar.js
diff --git a/src/components/layouts/Footer.js b/src2/components/layouts/Footer.js
index d173a525..d173a525 100644
--- a/src/components/layouts/Footer.js
+++ b/src2/components/layouts/Footer.js
diff --git a/src/components/layouts/Header.js b/src2/components/layouts/Header.js
index 23fda642..23fda642 100644
--- a/src/components/layouts/Header.js
+++ b/src2/components/layouts/Header.js
diff --git a/src/components/layouts/Layout.js b/src2/components/layouts/Layout.js
index fd507963..fd507963 100644
--- a/src/components/layouts/Layout.js
+++ b/src2/components/layouts/Layout.js
diff --git a/src/components/manufactures/ManufactureCard.js b/src2/components/manufactures/ManufactureCard.js
index 73a96902..73a96902 100644
--- a/src/components/manufactures/ManufactureCard.js
+++ b/src2/components/manufactures/ManufactureCard.js
diff --git a/src/components/products/ProductCard.js b/src2/components/products/ProductCard.js
index c79a4900..c79a4900 100644
--- a/src/components/products/ProductCard.js
+++ b/src2/components/products/ProductCard.js
diff --git a/src/components/products/ProductCategories.js b/src2/components/products/ProductCategories.js
index 3b671f29..3b671f29 100644
--- a/src/components/products/ProductCategories.js
+++ b/src2/components/products/ProductCategories.js
diff --git a/src/components/products/ProductSimilar.js b/src2/components/products/ProductSimilar.js
index 9e2292cb..9e2292cb 100644
--- a/src/components/products/ProductSimilar.js
+++ b/src2/components/products/ProductSimilar.js
diff --git a/src/components/products/ProductSlider.js b/src2/components/products/ProductSlider.js
index 662a6511..662a6511 100644
--- a/src/components/products/ProductSlider.js
+++ b/src2/components/products/ProductSlider.js
diff --git a/src/components/transactions/TransactionDetail.js b/src2/components/transactions/TransactionDetail.js
index 295a4f9f..295a4f9f 100644
--- a/src/components/transactions/TransactionDetail.js
+++ b/src2/components/transactions/TransactionDetail.js
diff --git a/src/components/transactions/TransactionStatusBadge.js b/src2/components/transactions/TransactionStatusBadge.js
index f94fd3fd..f94fd3fd 100644
--- a/src/components/transactions/TransactionStatusBadge.js
+++ b/src2/components/transactions/TransactionStatusBadge.js
diff --git a/src/components/variants/VariantCard.js b/src2/components/variants/VariantCard.js
index a821480c..a821480c 100644
--- a/src/components/variants/VariantCard.js
+++ b/src2/components/variants/VariantCard.js
diff --git a/src/components/variants/VariantGroupCard.js b/src2/components/variants/VariantGroupCard.js
index 462c63cf..462c63cf 100644
--- a/src/components/variants/VariantGroupCard.js
+++ b/src2/components/variants/VariantGroupCard.js
diff --git a/src/core/utils/address.js b/src2/core/utils/address.js
index c4a19af5..c4a19af5 100644
--- a/src/core/utils/address.js
+++ b/src2/core/utils/address.js
diff --git a/src/core/utils/apiOdoo.js b/src2/core/utils/apiOdoo.js
index 4d0adae3..4d0adae3 100644
--- a/src/core/utils/apiOdoo.js
+++ b/src2/core/utils/apiOdoo.js
diff --git a/src2/core/utils/auth.js b/src2/core/utils/auth.js
new file mode 100644
index 00000000..62eba2c0
--- /dev/null
+++ b/src2/core/utils/auth.js
@@ -0,0 +1,38 @@
+import { deleteCookie, getCookie, setCookie } from 'cookies-next';
+import { useEffect, useState } from 'react';
+
+const getAuth = () => {
+ let auth = getCookie('auth');
+ if (auth) {
+ return JSON.parse(auth);
+ }
+ return false;
+}
+
+const setAuth = (user) => {
+ setCookie('auth', JSON.stringify(user));
+ return true;
+}
+
+const deleteAuth = () => {
+ deleteCookie('auth');
+ return true;
+}
+
+const useAuth = () => {
+ const [auth, setAuth] = useState(null);
+
+ useEffect(() => {
+ const handleIsAuthenticated = () => setAuth(getAuth());
+ handleIsAuthenticated();
+ }, []);
+
+ return [auth, setAuth];
+}
+
+export {
+ getAuth,
+ setAuth,
+ deleteAuth,
+ useAuth
+}; \ No newline at end of file
diff --git a/src2/core/utils/cart.js b/src2/core/utils/cart.js
new file mode 100644
index 00000000..66efcbf2
--- /dev/null
+++ b/src2/core/utils/cart.js
@@ -0,0 +1,36 @@
+const getCart = () => {
+ const cart = localStorage.getItem('cart');
+ if (cart) return JSON.parse(cart);
+ return {};
+}
+
+const setCart = (cart) => {
+ localStorage.setItem('cart', JSON.stringify(cart));
+ return true;
+}
+
+const getItemCart = (product_id) => {
+ let cart = getCart();
+ return cart[product_id];
+}
+
+const createOrUpdateItemCart = (product_id, quantity, selected = false) => {
+ let cart = getCart();
+ cart[product_id] = { product_id, quantity, selected };
+ setCart(cart);
+ return true;
+}
+
+const deleteItemCart = (product_id) => {
+ let cart = getCart();
+ delete cart[product_id];
+ setCart(cart);
+ return true;
+}
+
+export {
+ getCart,
+ getItemCart,
+ createOrUpdateItemCart,
+ deleteItemCart
+} \ No newline at end of file
diff --git a/src/core/utils/convertToOption.js b/src2/core/utils/convertToOption.js
index 08fec08f..08fec08f 100644
--- a/src/core/utils/convertToOption.js
+++ b/src2/core/utils/convertToOption.js
diff --git a/src2/core/utils/currencyFormat.js b/src2/core/utils/currencyFormat.js
new file mode 100644
index 00000000..dadeaec6
--- /dev/null
+++ b/src2/core/utils/currencyFormat.js
@@ -0,0 +1,8 @@
+export default function currencyFormat(value) {
+ const currency = new Intl.NumberFormat('id-ID', {
+ style: 'currency',
+ currency: 'IDR',
+ maximumFractionDigits: 0
+ });
+ return currency.format(value);
+} \ No newline at end of file
diff --git a/src/core/utils/formValidation.js b/src2/core/utils/formValidation.js
index 0e83f4cc..0e83f4cc 100644
--- a/src/core/utils/formValidation.js
+++ b/src2/core/utils/formValidation.js
diff --git a/src/core/utils/getFileBase64.js b/src2/core/utils/getFileBase64.js
index 78013e43..78013e43 100644
--- a/src/core/utils/getFileBase64.js
+++ b/src2/core/utils/getFileBase64.js
diff --git a/src/core/utils/greeting.js b/src2/core/utils/greeting.js
index 7dc19f8f..7dc19f8f 100644
--- a/src/core/utils/greeting.js
+++ b/src2/core/utils/greeting.js
diff --git a/src/core/utils/mailer.js b/src2/core/utils/mailer.js
index 4e7ff7cc..4e7ff7cc 100644
--- a/src/core/utils/mailer.js
+++ b/src2/core/utils/mailer.js
diff --git a/src2/core/utils/slug.js b/src2/core/utils/slug.js
new file mode 100644
index 00000000..0a7d30fc
--- /dev/null
+++ b/src2/core/utils/slug.js
@@ -0,0 +1,25 @@
+import toTitleCase from './toTitleCase';
+
+const createSlug = (name, id) => {
+ let slug = name?.trim().replace(new RegExp(/[^A-Za-z0-9]/, 'g'), '-').toLowerCase() + '-' + id;
+ let splitSlug = slug.split('-');
+ let filterSlugFromEmptyChar = splitSlug.filter(x => x != '');
+ return filterSlugFromEmptyChar.join('-');
+}
+
+const getIdFromSlug = (slug) => {
+ let id = slug.split('-');
+ return id[id.length-1];
+}
+
+const getNameFromSlug = (slug) => {
+ let name = slug.split('-');
+ name.pop();
+ return toTitleCase(name.join(' '));
+}
+
+export {
+ createSlug,
+ getIdFromSlug,
+ getNameFromSlug
+}; \ No newline at end of file
diff --git a/src2/core/utils/toTitleCase.js b/src2/core/utils/toTitleCase.js
new file mode 100644
index 00000000..5cfd70d0
--- /dev/null
+++ b/src2/core/utils/toTitleCase.js
@@ -0,0 +1,8 @@
+export default function toTitleCase(str) {
+ return str.replace(
+ /\w\S*/g,
+ function(txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ }
+ );
+} \ No newline at end of file
diff --git a/src/icons/chevron-left.svg b/src2/icons/chevron-left.svg
index a22ce386..a22ce386 100644
--- a/src/icons/chevron-left.svg
+++ b/src2/icons/chevron-left.svg
diff --git a/src/icons/chevron-right.svg b/src2/icons/chevron-right.svg
index eb58f2f2..eb58f2f2 100644
--- a/src/icons/chevron-right.svg
+++ b/src2/icons/chevron-right.svg
diff --git a/src/icons/close.svg b/src2/icons/close.svg
index 50e0589d..50e0589d 100644
--- a/src/icons/close.svg
+++ b/src2/icons/close.svg
diff --git a/src/icons/filter.svg b/src2/icons/filter.svg
index c15ce7b9..c15ce7b9 100644
--- a/src/icons/filter.svg
+++ b/src2/icons/filter.svg
diff --git a/src/icons/image-placeholder.svg b/src2/icons/image-placeholder.svg
index 935e1097..935e1097 100644
--- a/src/icons/image-placeholder.svg
+++ b/src2/icons/image-placeholder.svg
diff --git a/src/icons/instagram.svg b/src2/icons/instagram.svg
index d90842c6..d90842c6 100644
--- a/src/icons/instagram.svg
+++ b/src2/icons/instagram.svg
diff --git a/src/icons/linkedin.svg b/src2/icons/linkedin.svg
index a68aec96..a68aec96 100644
--- a/src/icons/linkedin.svg
+++ b/src2/icons/linkedin.svg
diff --git a/src/icons/menu.svg b/src2/icons/menu.svg
index 5d067e8e..5d067e8e 100644
--- a/src/icons/menu.svg
+++ b/src2/icons/menu.svg
diff --git a/src/icons/minus.svg b/src2/icons/minus.svg
index 12a10199..12a10199 100644
--- a/src/icons/minus.svg
+++ b/src2/icons/minus.svg
diff --git a/src/icons/plus.svg b/src2/icons/plus.svg
index 2923c684..2923c684 100644
--- a/src/icons/plus.svg
+++ b/src2/icons/plus.svg
diff --git a/src/icons/search.svg b/src2/icons/search.svg
index 6de1cdfa..6de1cdfa 100644
--- a/src/icons/search.svg
+++ b/src2/icons/search.svg
diff --git a/src/icons/shopping-cart.svg b/src2/icons/shopping-cart.svg
index 09f14ca6..09f14ca6 100644
--- a/src/icons/shopping-cart.svg
+++ b/src2/icons/shopping-cart.svg
diff --git a/src/icons/trash.svg b/src2/icons/trash.svg
index e23673ee..e23673ee 100644
--- a/src/icons/trash.svg
+++ b/src2/icons/trash.svg
diff --git a/src2/images/logo.png b/src2/images/logo.png
new file mode 100644
index 00000000..87c696aa
--- /dev/null
+++ b/src2/images/logo.png
Binary files differ
diff --git a/src/images/page-not-found.png b/src2/images/page-not-found.png
index 296c0443..296c0443 100644
--- a/src/images/page-not-found.png
+++ b/src2/images/page-not-found.png
Binary files differ
diff --git a/src/lib/elements/hooks/useBottomPopup.js b/src2/lib/elements/hooks/useBottomPopup.js
index 88b72316..88b72316 100644
--- a/src/lib/elements/hooks/useBottomPopup.js
+++ b/src2/lib/elements/hooks/useBottomPopup.js
diff --git a/src/lib/elements/hooks/useConfirmAlert.js b/src2/lib/elements/hooks/useConfirmAlert.js
index 4975c57d..4975c57d 100644
--- a/src/lib/elements/hooks/useConfirmAlert.js
+++ b/src2/lib/elements/hooks/useConfirmAlert.js
diff --git a/src/pages/404.js b/src2/pages/404.js
index 1e1850f2..1e1850f2 100644
--- a/src/pages/404.js
+++ b/src2/pages/404.js
diff --git a/src/pages/_app.js b/src2/pages/_app.js
index 6a40f4e6..6a40f4e6 100644
--- a/src/pages/_app.js
+++ b/src2/pages/_app.js
diff --git a/src/pages/_error.js b/src2/pages/_error.js
index 107ddf46..107ddf46 100644
--- a/src/pages/_error.js
+++ b/src2/pages/_error.js
diff --git a/src/pages/activate.js b/src2/pages/activate.js
index d9b41bf4..d9b41bf4 100644
--- a/src/pages/activate.js
+++ b/src2/pages/activate.js
diff --git a/src/pages/api/activation-request.js b/src2/pages/api/activation-request.js
index 3f33875c..3f33875c 100644
--- a/src/pages/api/activation-request.js
+++ b/src2/pages/api/activation-request.js
diff --git a/src/pages/api/activation.js b/src2/pages/api/activation.js
index 8b22af8d..8b22af8d 100644
--- a/src/pages/api/activation.js
+++ b/src2/pages/api/activation.js
diff --git a/src/pages/api/login.js b/src2/pages/api/login.js
index e02a73cb..e02a73cb 100644
--- a/src/pages/api/login.js
+++ b/src2/pages/api/login.js
diff --git a/src/pages/api/register.js b/src2/pages/api/register.js
index 7c8d8b39..7c8d8b39 100644
--- a/src/pages/api/register.js
+++ b/src2/pages/api/register.js
diff --git a/src2/pages/api/shop/search.js b/src2/pages/api/shop/search.js
new file mode 100644
index 00000000..ad986c86
--- /dev/null
+++ b/src2/pages/api/shop/search.js
@@ -0,0 +1,96 @@
+import axios from "axios";
+
+const productResponseMap = (products) => {
+ return products.map((product) => {
+ let productMapped = {
+ id: product.product_id ? product.product_id[0] : '',
+ image: product.image ? product.image[0] : '',
+ code: product.default_code ? product.default_code[0] : '',
+ name: product.product_name ? product.product_name[0] : '',
+ lowest_price: {
+ price: product.price ? product.price[0] : 0,
+ price_discount: product.price_discount ? product.price_discount[0] : 0,
+ discount_percentage: product.discount ? product.discount[0] : 0,
+ },
+ variant_total: product.variant_total ? product.variant_total[0] : 0,
+ stock_total: product.stock_total ? product.stock_total[0] : 0,
+ weight: product.weight ? product.weight[0] : 0,
+ manufacture: {},
+ categories: [],
+ };
+
+ if (product.manufacture_id && product.brand) {
+ productMapped.manufacture = {
+ id: product.manufacture_id ? product.manufacture_id[0] : '',
+ name: product.brand ? product.brand[0] : '',
+ };
+ }
+
+ productMapped.categories = [
+ {
+ id: product.category_id ? product.category_id[0] : '',
+ name: product.category_name ? product.category_name[0] : '',
+ }
+ ];
+
+ return productMapped;
+ });
+}
+
+export default async function handler(req, res) {
+ const {
+ q,
+ page = 1,
+ brand = '',
+ category = '',
+ price_from = 0,
+ price_to = 0,
+ order_by = ''
+ } = req.query;
+
+ let paramOrderBy = '';
+ switch (order_by) {
+ case 'price-asc':
+ paramOrderBy = ', price_discount ASC';
+ break;
+ case 'price-desc':
+ paramOrderBy = ', price_discount DESC';
+ break;
+ case 'popular':
+ paramOrderBy = ', search_rank DESC';
+ break;
+ case 'stock':
+ paramOrderBy = ', stock_total DESC';
+ break;
+ }
+
+ let limit = 30;
+ let offset = (page - 1) * limit;
+ let parameter = [
+ `facet.query=${q}`,
+ 'facet=true',
+ 'indent=true',
+ 'q.op=AND',
+ `q=${q}`,
+ 'facet.field=brand_str',
+ 'facet.field=category_name_str',
+ `start=${offset}`,
+ `rows=${limit}`,
+ `sort=product_rating DESC ${paramOrderBy}`,
+ `fq=price_discount:[${price_from == '' ? '*' : price_from} TO ${price_to == '' ? '*' : price_to}]`
+ ];
+
+ if (brand) parameter.push(`fq=brand:${brand}`);
+ if (category) parameter.push(`fq=category_name:${category}`);
+
+ let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'));
+ try {
+ result.data.response.products = productResponseMap(result.data.response.docs);
+ result.data.responseHeader.params.start = parseInt(result.data.responseHeader.params.start);
+ result.data.responseHeader.params.rows = parseInt(result.data.responseHeader.params.rows);
+ delete result.data.response.docs;
+ res.status(200).json(result.data);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src2/pages/api/shop/suggest.js b/src2/pages/api/shop/suggest.js
new file mode 100644
index 00000000..6db1a851
--- /dev/null
+++ b/src2/pages/api/shop/suggest.js
@@ -0,0 +1,12 @@
+import axios from "axios";
+
+export default async function handler(req, res) {
+ const { q } = req.query;
+
+ let result = await axios(process.env.SOLR_HOST + `/solr/products/suggest?suggest=true&suggest.dictionary=mySuggester&suggest.q=${q}`);
+ try {
+ res.status(200).json(result.data);
+ } catch (error) {
+ res.status(400).json({ error: error.message });
+ }
+} \ No newline at end of file
diff --git a/src/pages/api/token.js b/src2/pages/api/token.js
index ec048158..ec048158 100644
--- a/src/pages/api/token.js
+++ b/src2/pages/api/token.js
diff --git a/src/pages/faqs.js b/src2/pages/faqs.js
index cdb8ef52..cdb8ef52 100644
--- a/src/pages/faqs.js
+++ b/src2/pages/faqs.js
diff --git a/src/pages/index.js b/src2/pages/index.js
index 65999ff6..65999ff6 100644
--- a/src/pages/index.js
+++ b/src2/pages/index.js
diff --git a/src/pages/login.js b/src2/pages/login.js
index e80de44e..e80de44e 100644
--- a/src/pages/login.js
+++ b/src2/pages/login.js
diff --git a/src/pages/logout.js b/src2/pages/logout.js
index 8ea21fab..8ea21fab 100644
--- a/src/pages/logout.js
+++ b/src2/pages/logout.js
diff --git a/src/pages/my/address/[id]/edit.js b/src2/pages/my/address/[id]/edit.js
index 838d39e7..838d39e7 100644
--- a/src/pages/my/address/[id]/edit.js
+++ b/src2/pages/my/address/[id]/edit.js
diff --git a/src/pages/my/address/create.js b/src2/pages/my/address/create.js
index 42cd117c..42cd117c 100644
--- a/src/pages/my/address/create.js
+++ b/src2/pages/my/address/create.js
diff --git a/src/pages/my/address/index.js b/src2/pages/my/address/index.js
index 5cad4410..5cad4410 100644
--- a/src/pages/my/address/index.js
+++ b/src2/pages/my/address/index.js
diff --git a/src/pages/my/invoice/[id].js b/src2/pages/my/invoice/[id].js
index 820c9af8..820c9af8 100644
--- a/src/pages/my/invoice/[id].js
+++ b/src2/pages/my/invoice/[id].js
diff --git a/src/pages/my/invoices.js b/src2/pages/my/invoices.js
index 9b2e77dc..9b2e77dc 100644
--- a/src/pages/my/invoices.js
+++ b/src2/pages/my/invoices.js
diff --git a/src/pages/my/menu.js b/src2/pages/my/menu.js
index ae6c2af8..ae6c2af8 100644
--- a/src/pages/my/menu.js
+++ b/src2/pages/my/menu.js
diff --git a/src/pages/my/profile.js b/src2/pages/my/profile.js
index 97891259..97891259 100644
--- a/src/pages/my/profile.js
+++ b/src2/pages/my/profile.js
diff --git a/src/pages/my/transaction/[id].js b/src2/pages/my/transaction/[id].js
index fb806aa4..fb806aa4 100644
--- a/src/pages/my/transaction/[id].js
+++ b/src2/pages/my/transaction/[id].js
diff --git a/src/pages/my/transactions.js b/src2/pages/my/transactions.js
index 8be43af7..8be43af7 100644
--- a/src/pages/my/transactions.js
+++ b/src2/pages/my/transactions.js
diff --git a/src/pages/my/wishlist.js b/src2/pages/my/wishlist.js
index 3d479802..3d479802 100644
--- a/src/pages/my/wishlist.js
+++ b/src2/pages/my/wishlist.js
diff --git a/src/pages/register.js b/src2/pages/register.js
index 39bd137f..39bd137f 100644
--- a/src/pages/register.js
+++ b/src2/pages/register.js
diff --git a/src/pages/shop/brands/[slug].js b/src2/pages/shop/brands/[slug].js
index a387e55d..a387e55d 100644
--- a/src/pages/shop/brands/[slug].js
+++ b/src2/pages/shop/brands/[slug].js
diff --git a/src/pages/shop/brands/index.js b/src2/pages/shop/brands/index.js
index bfdcd403..bfdcd403 100644
--- a/src/pages/shop/brands/index.js
+++ b/src2/pages/shop/brands/index.js
diff --git a/src/pages/shop/cart.js b/src2/pages/shop/cart.js
index 1178781b..1178781b 100644
--- a/src/pages/shop/cart.js
+++ b/src2/pages/shop/cart.js
diff --git a/src/pages/shop/checkout/finish.js b/src2/pages/shop/checkout/finish.js
index df284f8a..df284f8a 100644
--- a/src/pages/shop/checkout/finish.js
+++ b/src2/pages/shop/checkout/finish.js
diff --git a/src/pages/shop/checkout/index.js b/src2/pages/shop/checkout/index.js
index 0a77ebed..0a77ebed 100644
--- a/src/pages/shop/checkout/index.js
+++ b/src2/pages/shop/checkout/index.js
diff --git a/src/pages/shop/product/[slug].js b/src2/pages/shop/product/[slug].js
index 61692c1c..61692c1c 100644
--- a/src/pages/shop/product/[slug].js
+++ b/src2/pages/shop/product/[slug].js
diff --git a/src/pages/shop/quotation/finish.js b/src2/pages/shop/quotation/finish.js
index f7983fef..f7983fef 100644
--- a/src/pages/shop/quotation/finish.js
+++ b/src2/pages/shop/quotation/finish.js
diff --git a/src/pages/shop/quotation/index.js b/src2/pages/shop/quotation/index.js
index e1c196db..e1c196db 100644
--- a/src/pages/shop/quotation/index.js
+++ b/src2/pages/shop/quotation/index.js
diff --git a/src/pages/shop/search.js b/src2/pages/shop/search.js
index 4152bd43..4152bd43 100644
--- a/src/pages/shop/search.js
+++ b/src2/pages/shop/search.js
diff --git a/src2/styles/globals.css b/src2/styles/globals.css
new file mode 100644
index 00000000..b871a325
--- /dev/null
+++ b/src2/styles/globals.css
@@ -0,0 +1,468 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap');
+
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+* {
+ -webkit-tap-highlight-color: transparent;
+}
+
+html, body {
+ @apply
+ w-screen
+ text-body-2
+ text-gray_r-12
+ bg-gray_r-1
+ overflow-x-clip
+ ;
+}
+
+@layer base {
+ input[type="number"]::-webkit-inner-spin-button,
+ input[type="number"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ input[type=number] {
+ -moz-appearance:textfield;
+ }
+
+ h1, .h1 {
+ @apply
+ text-h-md
+ font-semibold
+ ;
+ }
+
+ h2, .h2 {
+ @apply
+ text-body-2
+ font-semibold
+ leading-6
+ ;
+ }
+
+ a {
+ @apply
+ font-medium
+ text-red_r-11
+ ;
+ }
+}
+
+@layer components {
+ .badge-red,
+ .badge-solid-red,
+ .badge-gray,
+ .badge-yellow,
+ .badge-blue,
+ .badge-green,
+ .badge-solid-green {
+ @apply
+ text-caption-2
+ leading-none
+ font-medium
+ px-1
+ py-1
+ rounded
+ w-fit
+ ;
+ }
+
+ .badge-red {
+ @apply
+ bg-red_r-5
+ text-red_r-10
+ ;
+ }
+
+ .badge-solid-red {
+ @apply
+ bg-red_r-10
+ text-white
+ ;
+ }
+
+ .badge-gray {
+ @apply
+ bg-gray_r-5
+ text-gray_r-10
+ ;
+ }
+
+ .badge-yellow {
+ @apply
+ bg-yellow_r-3
+ text-yellow_r-11
+ ;
+ }
+
+ .badge-blue {
+ @apply
+ bg-blue-200
+ text-blue-600
+ ;
+ }
+
+ .badge-green {
+ @apply
+ bg-green_r-5
+ text-green_r-10
+ ;
+ }
+
+ .badge-solid-green {
+ @apply
+ bg-green_r-10
+ text-white
+ ;
+ }
+
+ .form-label {
+ @apply
+ font-medium
+ block
+ ;
+ }
+
+ .form-input {
+ @apply
+ p-3
+ rounded
+ border
+ text-gray_r-12
+ border-gray_r-7
+ bg-transparent
+ w-full
+ leading-none
+ focus:outline-none
+ focus:border-yellow_r-9
+ disabled:bg-gray_r-5
+ ;
+ }
+
+ .form-input[aria-invalid] {
+ @apply
+ border-red_r-10
+ focus:border-red_r-10
+ ;
+ }
+
+ .btn-yellow,
+ .btn-light,
+ .btn-red,
+ .btn-solid-red,
+ .btn-green {
+ @apply
+ block
+ w-fit
+ py-3
+ px-6
+ rounded
+ border
+ text-center
+ font-medium
+ ease-linear
+ duration-150
+ ;
+ }
+
+ .btn-yellow {
+ @apply
+ bg-yellow_r-9
+ border-yellow_r-9
+ disabled:text-gray_r-10
+ disabled:bg-yellow_r-7
+ disabled:border-yellow_r-7
+ ;
+ }
+
+ .btn-red {
+ @apply
+ bg-red_r-3
+ border-red_r-6
+ text-red_r-11
+ disabled:text-red_r-10
+ disabled:bg-red_r-6
+ ;
+ }
+
+ .btn-solid-red {
+ @apply
+ bg-red_r-11
+ border-red_r-11
+ text-gray_r-1
+ disabled:text-gray_r-1
+ disabled:bg-red_r-8
+ disabled:border-red_r-8
+ ;
+ }
+
+ .btn-green {
+ @apply
+ bg-green_r-3
+ border-green_r-6
+ text-green_r-11
+ disabled:text-green_r-10
+ disabled:bg-green_r-6
+ ;
+ }
+
+ .btn-light {
+ @apply
+ bg-gray_r-3
+ border-gray_r-6
+ disabled:text-gray_r-10
+ disabled:bg-gray_r-6
+ ;
+ }
+
+ .product-card {
+ @apply
+ w-full
+ h-full
+ border
+ border-gray_r-3
+ shadow
+ bg-white
+ rounded
+ relative
+ flex
+ flex-col
+ ;
+ }
+
+ .product-card__image {
+ @apply
+ w-full
+ h-[160px]
+ object-contain
+ object-center
+ border-b
+ border-gray_r-6
+ ;
+ }
+
+ .product-card__content {
+ @apply
+ p-2
+ pb-3
+ flex-1
+ ;
+ }
+
+ .product-card__title {
+ @apply
+ text-caption-1
+ text-gray_r-12
+ leading-5
+ ;
+ }
+
+ .product-card__brand {
+ @apply
+ text-caption-1
+ mb-1
+ block
+ ;
+ }
+}
+
+@layer utilities {
+ .wrap-line-ellipsis-1,
+ .wrap-line-ellipsis-2,
+ .wrap-line-ellipsis-3 {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .wrap-line-ellipsis-1 {
+ -webkit-line-clamp: 1;
+ }
+
+ .wrap-line-ellipsis-2 {
+ -webkit-line-clamp: 2;
+ }
+
+ .wrap-line-ellipsis-3 {
+ -webkit-line-clamp: 3;
+ }
+}
+
+.menu-wrapper {
+ @apply
+ fixed
+ top-0
+ left-0
+ bg-white
+ w-[80%]
+ h-full
+ z-[60]
+ overflow-y-auto
+ translate-x-[-100%]
+ ease-linear
+ duration-150
+ ;
+}
+
+.menu-wrapper.active{
+ @apply translate-x-0;
+}
+
+.menu-overlay {
+ @apply
+ fixed
+ top-0
+ left-0
+ w-full
+ h-full
+ z-[55]
+ bg-gray_r-12/40
+ ;
+}
+
+.sticky-header {
+ @apply
+ px-4
+ py-3
+ bg-gray_r-1/90
+ backdrop-blur-lg
+ sticky
+ top-0
+ border-b
+ border-gray_r-7
+ z-50
+ ;
+}
+
+.content-container {
+ @apply
+ max-w-full
+ overflow-x-hidden
+ ;
+}
+
+#indoteknik_toast {
+ @apply
+ fixed
+ bottom-4
+ translate-y-[200%]
+ left-[50%]
+ translate-x-[-50%]
+ z-[100]
+ flex
+ items-center
+ p-4
+ mb-4
+ w-[90%]
+ text-gray-500
+ bg-white
+ border
+ border-gray-300
+ rounded-lg
+ shadow
+ ease-linear
+ duration-300
+ ;
+}
+
+#indoteknik_toast.active {
+ @apply translate-y-0;
+}
+
+.category-menu {
+ @apply hidden;
+}
+
+.swiper-slide {
+ @apply !h-auto;
+}
+
+.lazy-load-image-background {
+ @apply
+ !block
+ w-full
+ ;
+}
+
+.swiper-pagination-bullet-active {
+ @apply !bg-red_r-11;
+}
+
+.pagination {
+ @apply
+ flex
+ justify-center
+ gap-x-1
+ ;
+}
+
+.pagination-item {
+ @apply
+ p-1
+ flex
+ justify-center
+ items-center
+ w-10
+ rounded
+ ease-linear
+ duration-150
+ border
+ border-gray_r-6
+ bg-gray_r-3
+ hover:bg-gray_r-5
+ text-gray_r-12
+ ;
+}
+
+.pagination-item--active {
+ @apply
+ border-yellow_r-9
+ bg-yellow_r-9
+ ;
+}
+
+.pagination-dots {
+ @apply
+ p-1
+ flex
+ justify-center
+ items-end
+ w-10
+ rounded
+ ease-linear
+ bg-gray_r-3
+ text-caption-2
+ ;
+}
+
+.idt-transition {
+ @apply
+ transition-all
+ ease-linear
+ duration-300
+ ;
+}
+
+.form-select__placeholder {
+ @apply
+ !text-gray_r-9
+ ;
+}
+
+.form-select__control {
+ @apply
+ !shadow-none
+ !border-gray_r-7
+ ;
+}
+
+.form-select__control--menu-is-open {
+ @apply
+ !border-yellow_r-9
+ ;
+} \ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
index 03b05823..1f71fb92 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -1,9 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
- "./src/pages/**/*.{js,ts,tsx}",
- "./src/components/**/*.{js,ts,tsx}",
- "./src/helpers/**/*.{js,ts,tsx}",
+ "./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
@@ -101,5 +99,7 @@ module.exports = {
}
},
},
- plugins: [],
+ plugins: [
+ require('@tailwindcss/line-clamp'),
+ ],
}