summaryrefslogtreecommitdiff
path: root/src/core
diff options
context:
space:
mode:
Diffstat (limited to 'src/core')
-rw-r--r--src/core/api/odooApi.js49
-rw-r--r--src/core/api/searchSuggestApi.js14
-rw-r--r--src/core/components/Seo.jsx11
-rw-r--r--src/core/components/elements/Alert/Alert.jsx21
-rw-r--r--src/core/components/elements/Appbar/Appbar.jsx44
-rw-r--r--src/core/components/elements/Badge/Badge.jsx29
-rw-r--r--src/core/components/elements/Divider/Divider.jsx7
-rw-r--r--src/core/components/elements/Footer/BasicFooter.jsx268
-rw-r--r--src/core/components/elements/Footer/SimpleFooter.jsx43
-rw-r--r--src/core/components/elements/Image/Image.jsx18
-rw-r--r--src/core/components/elements/Link/Link.jsx17
-rw-r--r--src/core/components/elements/Navbar/Navbar.jsx46
-rw-r--r--src/core/components/elements/Navbar/Search.jsx93
-rw-r--r--src/core/components/elements/Pagination/Pagination.js83
-rw-r--r--src/core/components/elements/Popup/BottomPopup.jsx54
-rw-r--r--src/core/components/elements/Select/HookFormSelect.jsx14
-rw-r--r--src/core/components/elements/Sidebar/Sidebar.jsx220
-rw-r--r--src/core/components/elements/Skeleton/BrandSkeleton.jsx11
-rw-r--r--src/core/components/elements/Skeleton/ImageSkeleton.jsx24
-rw-r--r--src/core/components/elements/Skeleton/ProductCardSkeleton.jsx29
-rw-r--r--src/core/components/elements/Spinner/Spinner.jsx25
-rw-r--r--src/core/components/layouts/AnimationLayout.jsx24
-rw-r--r--src/core/components/layouts/AppLayout.jsx17
-rw-r--r--src/core/components/layouts/BasicLayout.jsx17
-rw-r--r--src/core/hooks/useActive.js21
-rw-r--r--src/core/hooks/useAuth.js15
-rw-r--r--src/core/hooks/useSidebar.js27
-rw-r--r--src/core/utils/address.js33
-rw-r--r--src/core/utils/apiOdoo.js44
-rw-r--r--src/core/utils/auth.js35
-rw-r--r--src/core/utils/cart.js48
-rw-r--r--src/core/utils/convertToOption.js11
-rw-r--r--src/core/utils/currencyFormat.js12
-rw-r--r--src/core/utils/formValidation.js107
-rw-r--r--src/core/utils/getFileBase64.js21
-rw-r--r--src/core/utils/greeting.js12
-rw-r--r--src/core/utils/mailer.js6
-rw-r--r--src/core/utils/slug.js34
-rw-r--r--src/core/utils/toTitleCase.js15
39 files changed, 1343 insertions, 276 deletions
diff --git a/src/core/api/odooApi.js b/src/core/api/odooApi.js
new file mode 100644
index 00000000..202c355e
--- /dev/null
+++ b/src/core/api/odooApi.js
@@ -0,0 +1,49 @@
+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
diff --git a/src/core/api/searchSuggestApi.js b/src/core/api/searchSuggestApi.js
new file mode 100644
index 00000000..e4445c9a
--- /dev/null
+++ b/src/core/api/searchSuggestApi.js
@@ -0,0 +1,14 @@
+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
diff --git a/src/core/components/Seo.jsx b/src/core/components/Seo.jsx
new file mode 100644
index 00000000..e688077e
--- /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
diff --git a/src/core/components/elements/Alert/Alert.jsx b/src/core/components/elements/Alert/Alert.jsx
new file mode 100644
index 00000000..695be8a3
--- /dev/null
+++ b/src/core/components/elements/Alert/Alert.jsx
@@ -0,0 +1,21 @@
+const Alert = ({ children, className, type }) => {
+ let typeClass = ''
+ switch (type) {
+ case 'info':
+ typeClass = 'bg-blue-100 text-blue-900 border-blue-400'
+ break
+ case 'success':
+ typeClass = 'bg-green-100 text-green-900 border-green-400'
+ break
+ case 'warning':
+ typeClass = 'bg-yellow-100 text-yellow-900 border-yellow-400'
+ break
+ }
+ return (
+ <div className={`rounded-md w-full text-medium p-3 border ${typeClass} ${className}`}>
+ {children}
+ </div>
+ )
+}
+
+export default Alert
diff --git a/src/core/components/elements/Appbar/Appbar.jsx b/src/core/components/elements/Appbar/Appbar.jsx
new file mode 100644
index 00000000..098d0a33
--- /dev/null
+++ b/src/core/components/elements/Appbar/Appbar.jsx
@@ -0,0 +1,44 @@
+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 border-b border-gray_r-6 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-medium text-h-sm line-clamp-1'>{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='/'
+ className='py-4 px-2'
+ >
+ <HomeIcon className='w-6 text-gray_r-12' />
+ </Link>
+ <Link
+ href='/my/menu'
+ className='py-4 px-2'
+ >
+ <Bars3Icon className='w-6 text-gray_r-12' />
+ </Link>
+ </div>
+ </nav>
+ )
+}
+
+export default AppBar
diff --git a/src/core/components/elements/Badge/Badge.jsx b/src/core/components/elements/Badge/Badge.jsx
new file mode 100644
index 00000000..e50cdc78
--- /dev/null
+++ b/src/core/components/elements/Badge/Badge.jsx
@@ -0,0 +1,29 @@
+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
diff --git a/src/core/components/elements/Divider/Divider.jsx b/src/core/components/elements/Divider/Divider.jsx
new file mode 100644
index 00000000..ce54a2ea
--- /dev/null
+++ b/src/core/components/elements/Divider/Divider.jsx
@@ -0,0 +1,7 @@
+const Divider = (props) => <div className={`h-1 bg-gray_r-4 ${props.className}`} />
+
+Divider.defaultProps = {
+ className: ''
+}
+
+export default Divider
diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx
new file mode 100644
index 00000000..76a7cac2
--- /dev/null
+++ b/src/core/components/elements/Footer/BasicFooter.jsx
@@ -0,0 +1,268 @@
+import NextImage from 'next/image'
+import IndoteknikLogo from '@/images/logo.png'
+import {
+ DevicePhoneMobileIcon,
+ EnvelopeIcon,
+ PhoneArrowUpRightIcon
+} from '@heroicons/react/24/outline'
+import Link from '../Link/Link'
+
+const BasicFooter = () => {
+ return (
+ <footer className='flex flex-wrap p-4 bg-gray_r-3 text-caption-1'>
+ <div className='w-1/2 flex flex-col gap-y-4 pr-1.5'>
+ <div>
+ <NextImage
+ src={IndoteknikLogo}
+ alt='Logo Indoteknik'
+ width={90}
+ height={30}
+ />
+
+ <div className='font-semibold mt-2'>PT. Indoteknik Dotcom Gemilang</div>
+ </div>
+
+ <OfficeLocation />
+ <WarehouseLocation />
+ <InformationCenter />
+ <OpenHours />
+ <SocialMedias />
+ </div>
+
+ <div className='w-1/2 flex flex-col gap-y-4 pl-1.5'>
+ <AboutUs />
+ <CustomerGuide />
+ <Payments />
+ </div>
+
+ <div className='w-full mt-4 leading-5 text-caption-2 text-gray_r-12/80'>
+ Copyright © 2007 - 2022, PT. Indoteknik Dotcom Gemilang
+ </div>
+ </footer>
+ )
+}
+
+const headerClassName = 'font-semibold mb-2'
+
+const OfficeLocation = () => (
+ <div>
+ <div className={headerClassName}>Kantor Pusat</div>
+ <div className='leading-6 text-gray_r-12/80'>
+ Jl. Bandengan Utara 85A No. 8-9 RT.3/RW.16, Penjaringan, Kec. Penjaringan, Jakarta Utara
+ </div>
+ </div>
+)
+
+const WarehouseLocation = () => (
+ <div>
+ <div className={headerClassName}>Gudang Indoteknik</div>
+ <div className='leading-6 text-gray_r-12/80'>
+ Jl. Bandengan Utara Komp. 85 A dan B, Penjaringan, Kec. Penjaringan, Jakarta Utara
+ </div>
+ </div>
+)
+
+const AboutUs = () => (
+ <div>
+ <div className={`${headerClassName} mb-3`}>Tentang Kami</div>
+ <ul className='flex flex-col gap-y-2'>
+ <li>
+ <InternalItemLink href='/'>Company Profile</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Karir</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Pelanggan Kami</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Menjadi Supplier</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Garansi dan Pengembalian</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Metode Pembayaran</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Metode Pengiriman</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Testimonial</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Kebijakan Privacy</InternalItemLink>
+ </li>
+ </ul>
+ </div>
+)
+
+const CustomerGuide = () => (
+ <div>
+ <div className={`${headerClassName} mb-3`}>Panduan Pelanggan</div>
+ <ul className='flex flex-col gap-y-2'>
+ <li>
+ <InternalItemLink href='/'>Panduan Belanja</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>F.A.Q</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Kebijakan Privasi</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Pengajuan Tempo</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Garansi Produk</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Online Quotation</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Pengiriman</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Pembayaran</InternalItemLink>
+ </li>
+ <li>
+ <InternalItemLink href='/'>Syarat & Ketentuan</InternalItemLink>
+ </li>
+ </ul>
+ </div>
+)
+
+const InformationCenter = () => (
+ <div>
+ <div className={`${headerClassName} mb-3`}>Layanan Informasi</div>
+ <ul className='flex flex-col gap-y-2'>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <PhoneArrowUpRightIcon className='w-[18px] mr-2' />
+ <a href='tel:02129338828'>(021) 2933-8828 / 29</a>
+ </li>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <EnvelopeIcon className='w-[18px] mr-2' />
+ <a href='mailto:sales@indoteknik.com'>sales@indoteknik.com</a>
+ </li>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <DevicePhoneMobileIcon className='w-[18px] mr-2' />
+ <a href='https://wa.me/+628128080622'>0812-8080-622</a>
+ </li>
+ </ul>
+ </div>
+)
+
+const OpenHours = () => (
+ <div>
+ <div className={headerClassName}>Jam Operasional</div>
+ <ul className='flex flex-col gap-y-1'>
+ <li>
+ <div className='text-gray_r-12'>Senin - Jumat:</div>
+ <div className='text-gray_r-12/80'>08:30 - 17:00</div>
+ </li>
+ <li>
+ <div className='text-gray_r-12'>Sabtu:</div>
+ <div className='text-gray_r-12/80'>08:30 - 14:00</div>
+ </li>
+ </ul>
+ </div>
+)
+
+const SocialMedias = () => (
+ <div>
+ <div className={headerClassName}>Temukan Kami</div>
+ <div className='flex flex-wrap gap-2'>
+ <NextImage
+ src='/images/socials/Whatsapp.png'
+ alt='Whatsapp Logo'
+ width={24}
+ height={24}
+ />
+ <NextImage
+ src='/images/socials/Facebook.png'
+ alt='Facebook Logo'
+ width={24}
+ height={24}
+ />
+ <NextImage
+ src='/images/socials/Twitter.png'
+ alt='Twitter Logo'
+ width={24}
+ height={24}
+ />
+ <NextImage
+ src='/images/socials/Linkedin.png'
+ alt='Linkedin Logo'
+ width={24}
+ height={24}
+ />
+ </div>
+ </div>
+)
+
+const Payments = () => (
+ <div>
+ <div className={headerClassName}>Pembayaran</div>
+ <div className='flex flex-wrap gap-2'>
+ <NextImage
+ src='/images/payments/bca.png'
+ alt='Bank BCA Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/bni.png'
+ alt='Bank BNI Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/bri.png'
+ alt='Bank BRI Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/gopay.png'
+ alt='Gopay Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/mandiri.png'
+ alt='Bank Mandiri Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/mastercard.png'
+ alt='Mastercard Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/permata.png'
+ alt='Bank Permata Logo'
+ width={48}
+ height={24}
+ />
+ <NextImage
+ src='/images/payments/visa.png'
+ alt='Visa Logo'
+ width={48}
+ height={24}
+ />
+ </div>
+ </div>
+)
+
+const InternalItemLink = ({ href, children }) => (
+ <Link
+ href={href}
+ className='!text-gray_r-12/80 font-normal line-clamp-1'
+ >
+ {children}
+ </Link>
+)
+
+export default BasicFooter
diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx
new file mode 100644
index 00000000..0fbc8f62
--- /dev/null
+++ b/src/core/components/elements/Footer/SimpleFooter.jsx
@@ -0,0 +1,43 @@
+import {
+ DevicePhoneMobileIcon,
+ EnvelopeIcon,
+ PhoneArrowUpRightIcon
+} from '@heroicons/react/24/outline'
+
+const SimpleFooter = () => (
+ <footer className='flex flex-wrap p-4 bg-gray_r-3 text-caption-1'>
+ <div className='w-full font-semibold mb-4 text-body-2'>Butuh bantuan? Hubungi Kami</div>
+ <div className='w-1/2 pr-2'>
+ <div className='font-semibold mb-3'>Hubungi Kami</div>
+ <ul className='flex flex-col gap-y-2'>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <PhoneArrowUpRightIcon className='w-[18px] mr-2' />
+ <a href='tel:02129338828'>(021) 2933-8828 / 29</a>
+ </li>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <EnvelopeIcon className='w-[18px] mr-2' />
+ <a href='mailto:sales@indoteknik.com'>sales@indoteknik.com</a>
+ </li>
+ <li className='text-gray_r-12/80 flex items-center'>
+ <DevicePhoneMobileIcon className='w-[18px] mr-2' />
+ <a href='https://wa.me/+628128080622'>0812-8080-622</a>
+ </li>
+ </ul>
+ </div>
+ <div className='w-1/2 pl-2'>
+ <div className='font-semibold mb-3'>Jam Operasional</div>
+ <ul className='flex flex-col gap-y-1'>
+ <li>
+ <div className='text-gray_r-12 mb-0.5'>Senin - Jumat:</div>
+ <div className='text-gray_r-12/80'>08:30 - 17:00</div>
+ </li>
+ <li>
+ <div className='text-gray_r-12 mb-0.5'>Sabtu:</div>
+ <div className='text-gray_r-12/80'>08:30 - 14:00</div>
+ </li>
+ </ul>
+ </div>
+ </footer>
+)
+
+export default SimpleFooter
diff --git a/src/core/components/elements/Image/Image.jsx b/src/core/components/elements/Image/Image.jsx
new file mode 100644
index 00000000..ac82aaaf
--- /dev/null
+++ b/src/core/components/elements/Image/Image.jsx
@@ -0,0 +1,18 @@
+import { LazyLoadImage } from 'react-lazy-load-image-component'
+import 'react-lazy-load-image-component/src/effects/opacity.css'
+
+const Image = ({ ...props }) => (
+ <>
+ <LazyLoadImage
+ {...props}
+ src={props.src || '/images/noimage.jpeg'}
+ placeholderSrc='/images/indoteknik-placeholder.png'
+ alt={props.src ? props.alt : 'Image Not Found - Indoteknik'}
+ wrapperClassName='bg-white'
+ />
+ </>
+)
+
+Image.defaultProps = LazyLoadImage.defaultProps
+
+export default Image
diff --git a/src/core/components/elements/Link/Link.jsx b/src/core/components/elements/Link/Link.jsx
new file mode 100644
index 00000000..dbc65338
--- /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
diff --git a/src/core/components/elements/Navbar/Navbar.jsx b/src/core/components/elements/Navbar/Navbar.jsx
new file mode 100644
index 00000000..8cecee5b
--- /dev/null
+++ b/src/core/components/elements/Navbar/Navbar.jsx
@@ -0,0 +1,46 @@
+import dynamic from 'next/dynamic'
+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 useSidebar from '@/core/hooks/useSidebar'
+
+const Search = dynamic(() => import('./Search'))
+
+const Navbar = () => {
+ const { Sidebar, open } = useSidebar()
+ 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'>
+ <Link href='/my/wishlist'>
+ <HeartIcon className='w-6 text-gray_r-12' />
+ </Link>
+ <Link href='/shop/cart'>
+ <ShoppingCartIcon className='w-6 text-gray_r-12' />
+ </Link>
+ <button
+ type='button'
+ onClick={open}
+ >
+ <Bars3Icon className='w-6 text-gray_r-12' />
+ </button>
+ </div>
+ </div>
+ <Search />
+ </nav>
+ {Sidebar}
+ </>
+ )
+}
+
+export default Navbar
diff --git a/src/core/components/elements/Navbar/Search.jsx b/src/core/components/elements/Navbar/Search.jsx
new file mode 100644
index 00000000..ff2c7adb
--- /dev/null
+++ b/src/core/components/elements/Navbar/Search.jsx
@@ -0,0 +1,93 @@
+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
diff --git a/src/core/components/elements/Pagination/Pagination.js b/src/core/components/elements/Pagination/Pagination.js
new file mode 100644
index 00000000..18964fc4
--- /dev/null
+++ b/src/core/components/elements/Pagination/Pagination.js
@@ -0,0 +1,83 @@
+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
diff --git a/src/core/components/elements/Popup/BottomPopup.jsx b/src/core/components/elements/Popup/BottomPopup.jsx
new file mode 100644
index 00000000..24366802
--- /dev/null
+++ b/src/core/components/elements/Popup/BottomPopup.jsx
@@ -0,0 +1,54 @@
+import { XMarkIcon } from '@heroicons/react/24/outline'
+import { AnimatePresence, motion } from 'framer-motion'
+import { useEffect } from 'react'
+
+const transition = { ease: 'linear', duration: 0.2 }
+
+const BottomPopup = ({ children, active = false, title, close }) => {
+ useEffect(() => {
+ if (active) {
+ document.querySelector('html, body').classList.add('overflow-hidden')
+ } else {
+ document.querySelector('html, body').classList.remove('overflow-hidden')
+ }
+ }, [active])
+
+ return (
+ <>
+ <AnimatePresence>
+ {active && (
+ <>
+ <motion.div
+ className='overlay'
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ transition={transition}
+ onClick={close}
+ />
+ <motion.div
+ initial={{ bottom: '-100%' }}
+ animate={{ bottom: 0 }}
+ exit={{ bottom: '-100%' }}
+ transition={transition}
+ className='fixed left-0 w-full border-t border-gray_r-6 rounded-t-xl z-[60] p-4 pt-0 bg-white'
+ >
+ <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}
+ </motion.div>
+ </>
+ )}
+ </AnimatePresence>
+ </>
+ )
+}
+
+export default BottomPopup
diff --git a/src/core/components/elements/Select/HookFormSelect.jsx b/src/core/components/elements/Select/HookFormSelect.jsx
new file mode 100644
index 00000000..055a9c68
--- /dev/null
+++ b/src/core/components/elements/Select/HookFormSelect.jsx
@@ -0,0 +1,14 @@
+import ReactSelect from 'react-select'
+
+const HookFormSelect = ({ field, ...props }) => (
+ <ReactSelect
+ classNamePrefix='form-select'
+ ref={field.ref}
+ onChange={(option) => field.onChange(option.value)}
+ value={field.value ? props.options.find((option) => option.value === field.value) : ''}
+ isDisabled={props.disabled}
+ {...props}
+ />
+)
+
+export default HookFormSelect
diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx
new file mode 100644
index 00000000..c39b5e34
--- /dev/null
+++ b/src/core/components/elements/Sidebar/Sidebar.jsx
@@ -0,0 +1,220 @@
+import Link from '../Link/Link'
+import greeting from '@/core/utils/greeting'
+import useAuth from '@/core/hooks/useAuth'
+import { AnimatePresence, motion } from 'framer-motion'
+import { ChevronDownIcon, ChevronUpIcon, CogIcon } from '@heroicons/react/24/outline'
+import { Fragment, useEffect, useState } from 'react'
+import odooApi from '@/core/api/odooApi'
+
+const Sidebar = ({ active, close }) => {
+ const auth = useAuth()
+
+ const SidebarLink = ({ children, ...props }) => (
+ <Link
+ {...props}
+ onClick={close}
+ >
+ {children}
+ </Link>
+ )
+
+ const itemClassName = 'px-4 py-3 block !text-gray_r-12/80 font-normal'
+ const transition = { ease: 'linear', duration: 0.2 }
+
+ const [isOpenCategory, setOpenCategory] = useState(false)
+ const [categories, setCategories] = useState([])
+
+ useEffect(() => {
+ const loadCategories = async () => {
+ if (isOpenCategory && categories.length == 0) {
+ let dataCategories = await odooApi('GET', '/api/v1/category/tree')
+ dataCategories = dataCategories.map((category) => {
+ category.childs = category.childs.map((child1Category) => {
+ return {
+ ...child1Category,
+ isOpen: false
+ }
+ })
+ return {
+ ...category,
+ isOpen: false
+ }
+ })
+ setCategories(dataCategories)
+ }
+ }
+ loadCategories()
+ }, [isOpenCategory, categories])
+
+ const toggleCategories = (id = 0) => {
+ let newCategories = categories.map((category) => {
+ category.childs = category.childs.map((child1Category) => {
+ return {
+ ...child1Category,
+ isOpen: id == child1Category.id ? !child1Category.isOpen : child1Category.isOpen
+ }
+ })
+ return {
+ ...category,
+ isOpen: id == category.id ? !category.isOpen : category.isOpen
+ }
+ })
+ setCategories(newCategories)
+ }
+
+ return (
+ <>
+ <AnimatePresence>
+ {active && (
+ <>
+ <motion.div
+ className='overlay z-50'
+ initial={{ opacity: 0 }}
+ animate={{ opacity: 1 }}
+ exit={{ opacity: 0 }}
+ transition={transition}
+ onClick={close}
+ />
+ <motion.div
+ className='fixed z-[55] top-0 h-full w-[80%] bg-white'
+ initial={{ left: '-80%' }}
+ animate={{ left: 0 }}
+ exit={{ left: '-80%' }}
+ transition={transition}
+ >
+ <div className='divide-y divide-gray_r-6'>
+ <div className='p-4 flex gap-x-3'>
+ {!auth && (
+ <>
+ <Link
+ onClick={close}
+ href='/register'
+ className='btn-yellow !text-gray_r-12 py-2 flex-1'
+ >
+ Daftar
+ </Link>
+ <Link
+ onClick={close}
+ href='/login'
+ className='btn-solid-red !text-gray_r-1 py-2 flex-1'
+ >
+ Masuk
+ </Link>
+ </>
+ )}
+ {auth && (
+ <>
+ <div className='text-caption-2 text-gray_r-11'>
+ {greeting()},
+ <span className='text-body-2 text-gray_r-12 block mt-1 font-medium'>
+ {auth?.name}
+ </span>
+ </div>
+ <Link
+ onClick={close}
+ href='/my/menu'
+ className='!text-gray_r-11 ml-auto my-auto'
+ >
+ <CogIcon className='w-6' />
+ </Link>
+ </>
+ )}
+ </div>
+ <SidebarLink
+ className={itemClassName}
+ href='/shop/brands'
+ >
+ Semua Brand
+ </SidebarLink>
+ <SidebarLink
+ className={itemClassName}
+ href='/'
+ >
+ Tentang Indoteknik
+ </SidebarLink>
+ <SidebarLink
+ className={itemClassName}
+ href='/'
+ >
+ Pusat Bantuan
+ </SidebarLink>
+ <button
+ className={`${itemClassName} w-full text-left flex`}
+ onClick={() => setOpenCategory(!isOpenCategory)}
+ >
+ Kategori
+ <div className='ml-auto'>
+ {!isOpenCategory && <ChevronDownIcon className='text-gray_r-12 w-5' />}
+ {isOpenCategory && <ChevronUpIcon className='text-gray_r-12 w-5' />}
+ </div>
+ </button>
+ {isOpenCategory &&
+ categories.map((category) => (
+ <Fragment key={category.id}>
+ <div className='flex w-full text-gray_r-11 border-b border-gray_r-6 px-4 pl-8 items-center'>
+ <Link
+ href={`/shop/search?category=${category.name}`}
+ className='flex-1 font-normal !text-gray_r-11 py-4'
+ >
+ {category.name}
+ </Link>
+ <div
+ className='ml-4 h-full py-4'
+ onClick={() => toggleCategories(category.id)}
+ >
+ {!category.isOpen && <ChevronDownIcon className='text-gray_r-11 w-5' />}
+ {category.isOpen && <ChevronUpIcon className='text-gray_r-11 w-5' />}
+ </div>
+ </div>
+ {category.isOpen &&
+ category.childs.map((child1Category) => (
+ <Fragment key={child1Category.id}>
+ <div
+ className={`flex w-full !text-gray_r-11 border-b border-gray_r-6 p-4 pl-12 ${
+ category.isOpen ? 'bg-gray_r-2' : ''
+ }`}
+ >
+ <Link
+ href={`/shop/search?category=${child1Category.name}`}
+ className='flex-1 font-normal !text-gray_r-11'
+ >
+ {child1Category.name}
+ </Link>
+ {child1Category.childs.length > 0 && (
+ <div
+ className='ml-4 h-full'
+ onClick={() => toggleCategories(child1Category.id)}
+ >
+ {!child1Category.isOpen && (
+ <ChevronDownIcon className='text-gray_r-11 w-5' />
+ )}
+ {child1Category.isOpen && (
+ <ChevronUpIcon className='text-gray_r-11 w-5' />
+ )}
+ </div>
+ )}
+ </div>
+ {child1Category.isOpen &&
+ child1Category.childs.map((child2Category) => (
+ <Link
+ key={child2Category.id}
+ href={`/shop/search?category=${child2Category.name}`}
+ className='flex w-full font-normal !text-gray_r-11 border-b border-gray_r-6 p-4 pl-16'
+ >
+ {child2Category.name}
+ </Link>
+ ))}
+ </Fragment>
+ ))}
+ </Fragment>
+ ))}
+ </div>
+ </motion.div>
+ </>
+ )}
+ </AnimatePresence>
+ </>
+ )
+}
+
+export default Sidebar
diff --git a/src/core/components/elements/Skeleton/BrandSkeleton.jsx b/src/core/components/elements/Skeleton/BrandSkeleton.jsx
new file mode 100644
index 00000000..9a34fb9b
--- /dev/null
+++ b/src/core/components/elements/Skeleton/BrandSkeleton.jsx
@@ -0,0 +1,11 @@
+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
diff --git a/src/core/components/elements/Skeleton/ImageSkeleton.jsx b/src/core/components/elements/Skeleton/ImageSkeleton.jsx
new file mode 100644
index 00000000..39d06331
--- /dev/null
+++ b/src/core/components/elements/Skeleton/ImageSkeleton.jsx
@@ -0,0 +1,24 @@
+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
diff --git a/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx
new file mode 100644
index 00000000..ddc0d3bc
--- /dev/null
+++ b/src/core/components/elements/Skeleton/ProductCardSkeleton.jsx
@@ -0,0 +1,29 @@
+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
diff --git a/src/core/components/elements/Spinner/Spinner.jsx b/src/core/components/elements/Spinner/Spinner.jsx
new file mode 100644
index 00000000..4639db1d
--- /dev/null
+++ b/src/core/components/elements/Spinner/Spinner.jsx
@@ -0,0 +1,25 @@
+const Spinner = ({ className }) => {
+ return (
+ <div role='status'>
+ <svg
+ aria-hidden='true'
+ className={'animate-spin ' + className}
+ viewBox='0 0 100 101'
+ fill='none'
+ xmlns='http://www.w3.org/2000/svg'
+ >
+ <path
+ d='M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z'
+ fill='currentColor'
+ />
+ <path
+ d='M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z'
+ fill='currentFill'
+ />
+ </svg>
+ <span className='sr-only'>Loading...</span>
+ </div>
+ )
+}
+
+export default Spinner
diff --git a/src/core/components/layouts/AnimationLayout.jsx b/src/core/components/layouts/AnimationLayout.jsx
new file mode 100644
index 00000000..c4dee606
--- /dev/null
+++ b/src/core/components/layouts/AnimationLayout.jsx
@@ -0,0 +1,24 @@
+import { motion } from 'framer-motion'
+
+const AnimationLayout = ({ children, ...props }) => {
+ const transition = {
+ ease: 'easeIn',
+ duration: 0.2
+ }
+
+ return (
+ children && (
+ <motion.main
+ initial={{ opacity: 0, x: 0, y: 0 }}
+ animate={{ opacity: 1, x: 0, y: 0 }}
+ exit={{ opacity: 0, x: 30, y: 0 }}
+ transition={transition}
+ {...props}
+ >
+ {children}
+ </motion.main>
+ )
+ )
+}
+
+export default AnimationLayout
diff --git a/src/core/components/layouts/AppLayout.jsx b/src/core/components/layouts/AppLayout.jsx
new file mode 100644
index 00000000..a325b1c1
--- /dev/null
+++ b/src/core/components/layouts/AppLayout.jsx
@@ -0,0 +1,17 @@
+import AppBar from '../elements/Appbar/Appbar'
+import BasicFooter from '../elements/Footer/BasicFooter'
+import AnimationLayout from './AnimationLayout'
+
+const AppLayout = ({ children, title }) => {
+ return (
+ <>
+ <AnimationLayout>
+ <AppBar title={title} />
+ {children}
+ </AnimationLayout>
+ <BasicFooter />
+ </>
+ )
+}
+
+export default AppLayout
diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx
new file mode 100644
index 00000000..1a7185cd
--- /dev/null
+++ b/src/core/components/layouts/BasicLayout.jsx
@@ -0,0 +1,17 @@
+import dynamic from 'next/dynamic'
+import BasicFooter from '../elements/Footer/BasicFooter'
+
+const Navbar = dynamic(() => import('../elements/Navbar/Navbar'))
+const AnimationLayout = dynamic(() => import('./AnimationLayout'))
+
+const BasicLayout = ({ children }) => {
+ return (
+ <>
+ <Navbar />
+ <AnimationLayout>{children}</AnimationLayout>
+ <BasicFooter />
+ </>
+ )
+}
+
+export default BasicLayout
diff --git a/src/core/hooks/useActive.js b/src/core/hooks/useActive.js
new file mode 100644
index 00000000..c39cbdca
--- /dev/null
+++ b/src/core/hooks/useActive.js
@@ -0,0 +1,21 @@
+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
diff --git a/src/core/hooks/useAuth.js b/src/core/hooks/useAuth.js
new file mode 100644
index 00000000..af62e45c
--- /dev/null
+++ b/src/core/hooks/useAuth.js
@@ -0,0 +1,15 @@
+import { useEffect, useState } from 'react'
+import { getAuth } from '../utils/auth'
+
+const useAuth = () => {
+ const [auth, setAuth] = useState(null)
+
+ useEffect(() => {
+ const handleIsAuthenticated = () => setAuth(getAuth())
+ handleIsAuthenticated()
+ }, [])
+
+ return auth
+}
+
+export default useAuth
diff --git a/src/core/hooks/useSidebar.js b/src/core/hooks/useSidebar.js
new file mode 100644
index 00000000..4da61ac2
--- /dev/null
+++ b/src/core/hooks/useSidebar.js
@@ -0,0 +1,27 @@
+import useActive from './useActive'
+import SidebarComponent from '../components/elements/Sidebar/Sidebar'
+import { useEffect } from 'react'
+
+const useSidebar = () => {
+ const { active, activate, deactivate } = useActive()
+
+ useEffect(() => {
+ if (active) {
+ document.querySelector('html, body').classList.add('overflow-hidden')
+ } else {
+ document.querySelector('html, body').classList.remove('overflow-hidden')
+ }
+ }, [active])
+
+ return {
+ open: activate,
+ Sidebar: (
+ <SidebarComponent
+ active={active}
+ close={deactivate}
+ />
+ )
+ }
+}
+
+export default useSidebar
diff --git a/src/core/utils/address.js b/src/core/utils/address.js
index c4a19af5..c545d34b 100644
--- a/src/core/utils/address.js
+++ b/src/core/utils/address.js
@@ -1,27 +1,28 @@
const getAddress = () => {
- const address = localStorage.getItem('address');
- if (address) return JSON.parse(address);
- return {};
+ if (typeof window !== 'undefined') {
+ const address = localStorage.getItem('address')
+ if (address) return JSON.parse(address)
+ }
+ return {}
}
const setAddress = (address) => {
- localStorage.setItem('address', JSON.stringify(address));
- return true;
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('address', JSON.stringify(address))
+ }
+ return
}
const getItemAddress = (key) => {
- let address = getAddress();
- return address[key];
+ let address = getAddress()
+ return address[key]
}
-const createOrUpdateItemAddress = (key, value) => {
- let address = getAddress();
- address[key] = value;
- setAddress(address);
- return true;
+const updateItemAddress = (key, value) => {
+ let address = getAddress()
+ address[key] = value
+ setAddress(address)
+ return
}
-export {
- getItemAddress,
- createOrUpdateItemAddress
-}; \ No newline at end of file
+export { getItemAddress, updateItemAddress }
diff --git a/src/core/utils/apiOdoo.js b/src/core/utils/apiOdoo.js
deleted file mode 100644
index 4d0adae3..00000000
--- a/src/core/utils/apiOdoo.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { getCookie, setCookie } from 'cookies-next';
-import axios from 'axios';
-import { getAuth } from './auth';
-
-const renewToken = async () => {
- let token = await axios.get(process.env.SELF_HOST + '/api/token');
- setCookie('token', token.data);
- return token.data;
-};
-
-const getToken = async () => {
- let token = getCookie('token');
- if (token == undefined) token = await renewToken();
- return token;
-};
-
-let connectionTry = 0;
-const apiOdoo = async (method, url, data = {}, headers = {}) => {
- try {
- connectionTry++;
- let token = await getToken();
- let axiosParameter = {
- method,
- url: process.env.ODOO_HOST + url,
- headers: {'Authorization': token, ...headers}
- }
- const auth = getAuth();
-
- 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 && connectionTry < 15) {
- await renewToken();
- return apiOdoo(method, url, data, headers);
- }
- return res.data.result || [];
- } catch (error) {
- console.log(error)
- }
-}
-
-export default apiOdoo; \ No newline at end of file
diff --git a/src/core/utils/auth.js b/src/core/utils/auth.js
index 62eba2c0..13e0e79d 100644
--- a/src/core/utils/auth.js
+++ b/src/core/utils/auth.js
@@ -1,38 +1,21 @@
-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;
+ 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
+export { getAuth, setAuth, deleteAuth }
diff --git a/src/core/utils/cart.js b/src/core/utils/cart.js
index 66efcbf2..fd42ee4e 100644
--- a/src/core/utils/cart.js
+++ b/src/core/utils/cart.js
@@ -1,36 +1,36 @@
const getCart = () => {
- const cart = localStorage.getItem('cart');
- if (cart) return JSON.parse(cart);
- return {};
+ if (typeof window !== 'undefined') {
+ const cart = localStorage.getItem('cart')
+ if (cart) return JSON.parse(cart)
+ }
+ return {}
}
const setCart = (cart) => {
- localStorage.setItem('cart', JSON.stringify(cart));
- return true;
+ if (typeof window !== 'undefined') {
+ 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 updateItemCart = ({ 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,
- deleteItemCart
-} \ No newline at end of file
+export { getCart, getItemCart, updateItemCart, deleteItemCart }
diff --git a/src/core/utils/convertToOption.js b/src/core/utils/convertToOption.js
deleted file mode 100644
index 08fec08f..00000000
--- a/src/core/utils/convertToOption.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const convertToOption = (data) => {
- if (data) {
- return {
- value: data.id,
- label: data.name,
- }
- }
- return null;
-};
-
-export default convertToOption; \ No newline at end of file
diff --git a/src/core/utils/currencyFormat.js b/src/core/utils/currencyFormat.js
index dadeaec6..12b68111 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',
+ style: 'currency',
currency: 'IDR',
maximumFractionDigits: 0
- });
- return currency.format(value);
-} \ No newline at end of file
+ })
+ return currency.format(value)
+}
+
+export default currencyFormat
diff --git a/src/core/utils/formValidation.js b/src/core/utils/formValidation.js
deleted file mode 100644
index 0e83f4cc..00000000
--- a/src/core/utils/formValidation.js
+++ /dev/null
@@ -1,107 +0,0 @@
-import { useCallback, useEffect, useState } from "react";
-
-const validateForm = (data, queries, hasChangedInputs = null) => {
- let result = { valid: true, errors: {} };
-
- for (const query in queries) {
- if (!hasChangedInputs || (hasChangedInputs && hasChangedInputs[query])) {
- const value = data[query];
- const rules = queries[query];
- let errors = [];
- let label = null;
- for (const rule of rules) {
- let emailValidationRegex = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- if (rule.startsWith('label:')) {
- label = rule.replace('label:', '');
- } else if (rule === 'required' && !value) {
- errors.push('tidak boleh kosong');
- } else if (rule === 'email' && !value.match(emailValidationRegex)) {
- errors.push('harus format johndoe@example.com');
- } else if (rule.startsWith('maxLength:')) {
- let maxLength = parseInt(rule.replace('maxLength:', ''));
- if (value && value.length > maxLength) errors.push(`maksimal ${maxLength} karakter`);
- }
- }
- if (errors.length > 0) {
- result.errors[query] = (label || query) + ' ' + errors.join(', ');
- }
- }
- }
-
- if (Object.keys(result.errors).length > 0) {
- result.valid = false;
- }
-
- return result;
-}
-
-const useFormValidation = ({ initialFormValue = {}, validationScheme = {} }) => {
- const [ formInputs, setFormInputs ] = useState(initialFormValue);
- const [ formErrors, setFormErrors ] = useState({});
- const [ formValidation ] = useState(validationScheme);
- const [ hasChangedInputs, setHasChangedInputs ] = useState({});
-
- const handleFormSubmit = (event, func) => {
- if (event) {
- event.preventDefault();
-
- // Make all input to be has changed mode to revalidate
- const changedInputs = {};
- for (const key in formInputs) changedInputs[key] = true;
- setHasChangedInputs(changedInputs);
-
- const { valid, errors } = validateForm(formInputs, formValidation, changedInputs);
- setFormErrors(errors);
-
- if (valid) func();
- }
- };
-
- const setChangedInput = (name, value = true) => {
- setHasChangedInputs((hasChangedInputs) => ({
- ...hasChangedInputs,
- [name]: value
- }));
- };
-
- const handleInputChange = (event) => {
- setFormInputs((formInputs) => ({
- ...formInputs,
- [event.target.name]: event.target.value
- }));
- setChangedInput(event.target.name);
- };
-
- const handleSelectChange = useCallback((name, value) => {
- setFormInputs((formInputs) => ({
- ...formInputs,
- [name]: value
- }));
- setChangedInput(name);
- }, []);
-
- const handleFormReset = () => {
- setFormInputs(initialFormValue);
- setFormErrors({});
- setHasChangedInputs({});
- }
-
- useEffect(() => {
- if (formInputs) {
- const { errors } = validateForm(formInputs, formValidation, hasChangedInputs);
- setFormErrors(errors);
- }
- }, [ formInputs, formValidation, hasChangedInputs ])
-
- return {
- handleFormReset,
- handleFormSubmit,
- handleInputChange,
- handleSelectChange,
- hasChangedInputs,
- formInputs,
- formErrors
- };
- };
-
-export default useFormValidation; \ No newline at end of file
diff --git a/src/core/utils/getFileBase64.js b/src/core/utils/getFileBase64.js
index 78013e43..4fa7316b 100644
--- a/src/core/utils/getFileBase64.js
+++ b/src/core/utils/getFileBase64.js
@@ -1,11 +1,12 @@
-const getFileBase64 = file => new Promise((resolve, reject) => {
- let reader = new FileReader();
- reader.readAsBinaryString(file);
- reader.onload = () => {
- let result = reader.result;
- resolve(btoa(result));
- };
- reader.onerror = error => reject(error);
-});
+const getFileBase64 = (file) =>
+ new Promise((resolve, reject) => {
+ let reader = new FileReader()
+ reader.readAsBinaryString(file)
+ reader.onload = () => {
+ let result = reader.result
+ resolve(btoa(result))
+ }
+ reader.onerror = (error) => reject(error)
+ })
-export default getFileBase64; \ No newline at end of file
+export default getFileBase64
diff --git a/src/core/utils/greeting.js b/src/core/utils/greeting.js
index 7dc19f8f..aaaade7a 100644
--- a/src/core/utils/greeting.js
+++ b/src/core/utils/greeting.js
@@ -1,9 +1,9 @@
const greeting = () => {
- let hours = new Date().getHours();
- if (hours < 11) return 'Selamat Pagi';
- if (hours < 15) return 'Selamat Siang';
- if (hours < 18) return 'Selamat Sore';
- return 'Selamat Malam';
+ let hours = new Date().getHours()
+ if (hours < 11) return 'Selamat Pagi'
+ if (hours < 15) return 'Selamat Siang'
+ if (hours < 18) return 'Selamat Sore'
+ return 'Selamat Malam'
}
-export default greeting; \ No newline at end of file
+export default greeting
diff --git a/src/core/utils/mailer.js b/src/core/utils/mailer.js
index 4e7ff7cc..cab66bec 100644
--- a/src/core/utils/mailer.js
+++ b/src/core/utils/mailer.js
@@ -1,4 +1,4 @@
-const nodemailer = require('nodemailer');
+const nodemailer = require('nodemailer')
const mailer = nodemailer.createTransport({
port: process.env.MAIL_PORT,
host: process.env.MAIL_HOST,
@@ -7,6 +7,6 @@ const mailer = nodemailer.createTransport({
pass: process.env.MAIL_PASS
},
secure: true
-});
+})
-export default mailer; \ No newline at end of file
+export default mailer
diff --git a/src/core/utils/slug.js b/src/core/utils/slug.js
index 0a7d30fc..7010008a 100644
--- a/src/core/utils/slug.js
+++ b/src/core/utils/slug.js
@@ -1,25 +1,27 @@
-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
+export { createSlug, getIdFromSlug, getNameFromSlug }
diff --git a/src/core/utils/toTitleCase.js b/src/core/utils/toTitleCase.js
index 5cfd70d0..4335824d 100644
--- a/src/core/utils/toTitleCase.js
+++ b/src/core/utils/toTitleCase.js
@@ -1,8 +1,7 @@
-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
+const toTitleCase = (str) => {
+ return str.replace(/\w\S*/g, function (txt) {
+ return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
+ })
+}
+
+export default toTitleCase