diff options
| author | HATEC\SPVDEV001 <tri.susilo@altama.co.id> | 2023-05-08 16:44:09 +0700 |
|---|---|---|
| committer | HATEC\SPVDEV001 <tri.susilo@altama.co.id> | 2023-05-08 16:44:09 +0700 |
| commit | 486f85a45fc7e0669576f59824a31be472ed25bb (patch) | |
| tree | 0268afa8efe48746e040611ba41ad2cafda7ad08 /src | |
| parent | cff198277e14450f8d20d9e18548325e6f277682 (diff) | |
| parent | 30fc50600009ca54f085d594d838803c107e87f2 (diff) | |
Merge branch 'master' into development_tri/implementasi_raja_ongkir
# Conflicts:
# src/lib/checkout/components/Checkout.jsx
Diffstat (limited to 'src')
95 files changed, 2035 insertions, 504 deletions
diff --git a/src/core/api/searchSpellApi.js b/src/core/api/searchSpellApi.js new file mode 100644 index 00000000..4794c3e1 --- /dev/null +++ b/src/core/api/searchSpellApi.js @@ -0,0 +1,14 @@ +import axios from 'axios' + +const searchSpellApi = async ({ query }) => { + const dataSearchSpell = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/spell?q=${query.trim()}` + ) + return dataSearchSpell +} + +searchSpellApi.defaultProps = { + query: '' +} + +export default searchSpellApi diff --git a/src/core/api/searchSuggestApi.js b/src/core/api/searchSuggestApi.js index 3dabfbd6..c65405e3 100644 --- a/src/core/api/searchSuggestApi.js +++ b/src/core/api/searchSuggestApi.js @@ -1,8 +1,10 @@ import axios from 'axios' const searchSuggestApi = async ({ query }) => { + query = query.replace('&', '').trim() + if (!query) return {} const dataSearchSuggest = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/suggest?q=${query.trim()}` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/suggest?q=${query}` ) return dataSearchSuggest } diff --git a/src/core/components/elements/CountDown/CountDown.jsx b/src/core/components/elements/CountDown/CountDown.jsx new file mode 100644 index 00000000..91a1f68e --- /dev/null +++ b/src/core/components/elements/CountDown/CountDown.jsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' + +const CountDown = ({ initialTime }) => { + const days = Math.floor(initialTime / 86400) + const hours = Math.floor((initialTime % 86400) / 3600) + const minutes = Math.floor((initialTime % 3600) / 60) + const seconds = initialTime % 60 + + const [timeLeft, setTimeLeft] = useState({ + day: days, + hour: hours, + minute: minutes, + second: seconds + }) + + useEffect(() => { + const timer = setInterval(() => { + const totalSeconds = + timeLeft.day * 86400 + timeLeft.hour * 3600 + timeLeft.minute * 60 + timeLeft.second + const secondsLeft = totalSeconds - 1 + if (secondsLeft < 0) { + clearInterval(timer) + } else { + const days = Math.floor(secondsLeft / 86400) + const hours = Math.floor((secondsLeft % 86400) / 3600) + const minutes = Math.floor((secondsLeft % 3600) / 60) + const seconds = secondsLeft % 60 + setTimeLeft({ day: days, hour: hours, minute: minutes, second: seconds }) + } + }, 1000) + return () => clearInterval(timer) + }, [timeLeft]) + + return ( + <div className='flex gap-x-2.5 w-fit'> + <div className='flex flex-col items-center'> + <span className='bg-red-600 text-white font-semibold w-10 h-10 flex items-center justify-center rounded'> + {timeLeft.day.toString().padStart(2, '0')} + </span> + <span className='text-caption-1 text-gray-700 mt-1'>Hari</span> + </div> + <div className='flex flex-col items-center'> + <span className='bg-red-600 text-white font-semibold w-10 h-10 flex items-center justify-center rounded'> + {timeLeft.hour.toString().padStart(2, '0')} + </span> + <span className='text-caption-1 text-gray-700 mt-1'>Jam</span> + </div> + <div className='flex flex-col items-center'> + <span className='bg-red-600 text-white font-semibold w-10 h-10 flex items-center justify-center rounded'> + {timeLeft.minute.toString().padStart(2, '0')} + </span> + <span className='text-caption-1 text-gray-700 mt-1'>Menit</span> + </div> + <div className='flex flex-col items-center'> + <span className='bg-red-600 text-white font-semibold w-10 h-10 flex items-center justify-center rounded'> + {timeLeft.second.toString().padStart(2, '0')} + </span> + <span className='text-caption-1 text-gray-700 mt-1'>Detik</span> + </div> + </div> + ) +} + +export default CountDown diff --git a/src/core/components/elements/Footer/BasicFooter.jsx b/src/core/components/elements/Footer/BasicFooter.jsx index 42698cb2..d340ff3e 100644 --- a/src/core/components/elements/Footer/BasicFooter.jsx +++ b/src/core/components/elements/Footer/BasicFooter.jsx @@ -8,6 +8,7 @@ import { import Link from '../Link/Link' import MobileView from '../../views/MobileView' import DesktopView from '../../views/DesktopView' +import whatsappUrl from '@/core/utils/whatsappUrl' const BasicFooter = () => { return ( @@ -22,6 +23,7 @@ const BasicFooter = () => { </div> <OfficeLocation /> + <AboutUs /> {/* <WarehouseLocation /> */} <InformationCenter /> <OpenHours /> @@ -29,7 +31,7 @@ const BasicFooter = () => { </div> <div className='w-1/2 flex flex-col gap-y-4 pl-1.5'> - <AboutUs /> + <Form /> <CustomerGuide /> <Payments /> </div> @@ -49,6 +51,7 @@ const BasicFooter = () => { <InformationCenter /> </div> <CustomerGuide /> + <Form /> <AboutUs /> <div className='w-3/12'> <div className='grid grid-cols-1 gap-y-4'> @@ -99,31 +102,22 @@ const AboutUs = () => ( <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> + <InternalItemLink href='/tentang-kami'>Company Profile</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Menjadi Supplier</InternalItemLink> + <InternalItemLink href='/pelanggan-kami'>Pelanggan Kami</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Garansi dan Pengembalian</InternalItemLink> + <InternalItemLink href='/hubungi-kami'>Hubungi Kami</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Metode Pembayaran</InternalItemLink> + <InternalItemLink href='/karir'>Karir</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Metode Pengiriman</InternalItemLink> + <InternalItemLink href='/syarat-ketentuan'>Syarat & Ketentuan</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Testimonial</InternalItemLink> - </li> - <li> - <InternalItemLink href='/'>Kebijakan Privacy</InternalItemLink> + <InternalItemLink href='/kebijakan-privasi'>Kebijakan Privasi</InternalItemLink> </li> </ul> </div> @@ -131,34 +125,53 @@ const AboutUs = () => ( const CustomerGuide = () => ( <div> - <div className={`${headerClassName} mb-3`}>Panduan Pelanggan</div> + <div className={`${headerClassName} mb-3`}>Bantuan & Panduan</div> <ul className='flex flex-col gap-y-2'> <li> - <InternalItemLink href='/'>Panduan Belanja</InternalItemLink> + <InternalItemLink href='/metode-pembayaran'>Metode Pembayaran</InternalItemLink> </li> <li> - <InternalItemLink href='/'>F.A.Q</InternalItemLink> + <InternalItemLink href='/metode-pengiriman'>Metode Pengiriman</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Kebijakan Privasi</InternalItemLink> + <InternalItemLink href='/panduan-belanja'>Panduan Belanja</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Pengajuan Tempo</InternalItemLink> + <InternalItemLink href='/panduan-quotation'>Panduan Quotation</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Garansi Produk</InternalItemLink> + <InternalItemLink href='/pengembalian-barang-dana'> + Pengembalian Barang & Dana + </InternalItemLink> </li> <li> - <InternalItemLink href='/'>Online Quotation</InternalItemLink> + <InternalItemLink href='/informasi-garansi'>Informasi Garansi</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Pengiriman</InternalItemLink> + <InternalItemLink href='/panduan-pick-up-service'>Panduan Pick Up Service</InternalItemLink> + </li> + </ul> + </div> +) + +const Form = () => ( + <div> + <div className={`${headerClassName} mb-3`}>Formulir</div> + <ul className='flex flex-col gap-y-2'> + <li> + <InternalItemLink href='/my/kunjungan-sales'>Kunjungan Sales</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Pembayaran</InternalItemLink> + <InternalItemLink href='/my/kunjungan-service'>Kunjungan Service</InternalItemLink> </li> <li> - <InternalItemLink href='/'>Syarat & Ketentuan</InternalItemLink> + <InternalItemLink href='/my/pembayaran-tempo'>Pembayaran Tempo</InternalItemLink> + </li> + <li> + <InternalItemLink href='/my/surat-dukungan'>Surat Dukungan</InternalItemLink> + </li> + <li> + <InternalItemLink href='/my/daftar-merchant'>Daftar Merchant</InternalItemLink> </li> </ul> </div> @@ -178,7 +191,7 @@ const InformationCenter = () => ( </li> <li className='text-gray_r-12/80 flex items-center'> <DevicePhoneMobileIcon className='w-[18px] mr-2' /> - <a href='https://wa.me/628128080622' target='_blank' rel='noreferrer'> + <a href={whatsappUrl()} target='_blank' rel='noreferrer'> 0812-8080-622 </a> </li> @@ -205,7 +218,7 @@ const OpenHours = () => ( const SocialMedias = () => ( <div> <div className={headerClassName + 'block md:hidden'}>Temukan Kami</div> - <div className='flex flex-wrap gap-2'> + <div className='flex flex-wrap gap-2 mt-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} /> diff --git a/src/core/components/elements/Footer/SimpleFooter.jsx b/src/core/components/elements/Footer/SimpleFooter.jsx index 41ca6094..26f7f786 100644 --- a/src/core/components/elements/Footer/SimpleFooter.jsx +++ b/src/core/components/elements/Footer/SimpleFooter.jsx @@ -1,3 +1,4 @@ +import whatsappUrl from '@/core/utils/whatsappUrl' import { DevicePhoneMobileIcon, EnvelopeIcon, @@ -20,7 +21,7 @@ const SimpleFooter = () => ( </li> <li className='text-gray_r-12/80 flex items-center'> <DevicePhoneMobileIcon className='w-[18px] mr-2' /> - <a href='https://wa.me/628128080622' target='_blank' rel='noreferrer'> + <a href={whatsappUrl()} target='_blank' rel='noreferrer'> 0812-8080-622 </a> </li> diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx index 837d436c..3da0035a 100644 --- a/src/core/components/elements/Navbar/NavbarDesktop.jsx +++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx @@ -14,6 +14,8 @@ import { useEffect, useState } from 'react' import useAuth from '@/core/hooks/useAuth' import NavbarUserDropdown from './NavbarUserDropdown' import { getCart } from '@/core/utils/cart' +import TopBanner from './TopBanner' +import whatsappUrl from '@/core/utils/whatsappUrl' const Search = dynamic(() => import('./Search')) @@ -38,19 +40,17 @@ const NavbarDesktop = () => { return ( <DesktopView> + <TopBanner /> <div className='py-3 bg-warning-400' id='desktop-nav-top'> <div className='container mx-auto flex justify-between'> - <Link href='/' className='!text-gray_r-12'> + <Link href='/tentang-kami' className='!text-gray_r-12'> Tentang Indoteknik.com </Link> <div className='flex gap-x-6'> - <Link href='/' className='!text-gray_r-12'> + <Link href='/my/pembayaran-tempo' className='!text-gray_r-12'> Pembayaran Tempo </Link> <Link href='/' className='!text-gray_r-12'> - F.A.Q - </Link> - <Link href='/' className='!text-gray_r-12'> Fitur Layanan </Link> </div> @@ -90,7 +90,7 @@ const NavbarDesktop = () => { Wishlist </Link> <a - href='https://wa.me/628128080622' + href={whatsappUrl()} target='_blank' rel='noreferrer' className='flex items-center gap-x-1 !text-gray_r-12/80' diff --git a/src/core/components/elements/Navbar/NavbarMobile.jsx b/src/core/components/elements/Navbar/NavbarMobile.jsx index 0502dba5..7ac967fa 100644 --- a/src/core/components/elements/Navbar/NavbarMobile.jsx +++ b/src/core/components/elements/Navbar/NavbarMobile.jsx @@ -7,6 +7,7 @@ import dynamic from 'next/dynamic' import IndoteknikLogo from '@/images/logo.png' import { useEffect, useState } from 'react' import { getCart } from '@/core/utils/cart' +import TopBanner from './TopBanner' const Search = dynamic(() => import('./Search')) @@ -30,6 +31,7 @@ const NavbarMobile = () => { return ( <MobileView> + <TopBanner /> <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='/'> @@ -45,7 +47,7 @@ const NavbarMobile = () => { {cartCount} </span> </Link> - <button type='button' onClick={open}> + <button type='button' aria-label='sidebarMenuButton' onClick={open}> <Bars3Icon className='w-6 text-gray_r-12' /> </button> </div> diff --git a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx index 1d2429a7..7848124c 100644 --- a/src/core/components/elements/Navbar/NavbarUserDropdown.jsx +++ b/src/core/components/elements/Navbar/NavbarUserDropdown.jsx @@ -13,6 +13,7 @@ const NavbarUserDropdown = () => { return ( <div className='navbar-user-dropdown-wrapper'> <div className='navbar-user-dropdown'> + <Link href='/my/quotations'>Daftar Quotation</Link> <Link href='/my/transactions'>Daftar Transaksi</Link> <Link href='/my/invoices'>Invoice & Faktur Pajak</Link> <Link href='/my/wishlist'>Wishlist</Link> diff --git a/src/core/components/elements/Navbar/Search.jsx b/src/core/components/elements/Navbar/Search.jsx index 3046782b..47a9c235 100644 --- a/src/core/components/elements/Navbar/Search.jsx +++ b/src/core/components/elements/Navbar/Search.jsx @@ -13,7 +13,7 @@ const Search = () => { const loadSuggestion = useCallback(() => { if (query && document.activeElement == queryRef.current) { searchSuggestApi({ query }).then((response) => { - setSuggestions(response.data.suggestions) + setSuggestions(response.data?.suggestions || []) }) } else { setSuggestions([]) @@ -56,11 +56,15 @@ const Search = () => { onBlur={onInputBlur} onFocus={loadSuggestion} /> - <button type='submit' className='rounded-r border border-l-0 border-gray_r-6 px-2'> + <button + type='submit' + aria-label='SearchButton' + className='rounded-r border border-l-0 border-gray_r-6 px-2' + > <MagnifyingGlassIcon className='w-6' /> </button> - {suggestions.length > 1 && ( + {suggestions.length > 0 && ( <> <div className='absolute w-full top-[50px] rounded-b bg-gray_r-1 border border-gray_r-6 divide-y divide-gray_r-6 z-50'> {suggestions.map((suggestion, index) => ( diff --git a/src/core/components/elements/Navbar/TopBanner.jsx b/src/core/components/elements/Navbar/TopBanner.jsx new file mode 100644 index 00000000..48b23a3d --- /dev/null +++ b/src/core/components/elements/Navbar/TopBanner.jsx @@ -0,0 +1,25 @@ +import odooApi from '@/core/api/odooApi' +import { useQuery } from 'react-query' +import Image from 'next/image' + +const TopBanner = () => { + const fetchTopBanner = async () => await odooApi('GET', '/api/v1/banner?type=top-banner') + const topBanner = useQuery('topBanner', fetchTopBanner) + + return ( + topBanner.isFetched && + topBanner.data?.length > 0 && ( + <div style={{ backgroundColor: topBanner.data[0]?.backgroundColor || 'transparent' }}> + <Image + src={topBanner.data[0].image} + alt={topBanner.data[0].name} + width={1440} + height={40} + className='object-cover object-center h-full mx-auto' + /> + </div> + ) + ) +} + +export default TopBanner diff --git a/src/core/components/elements/Sidebar/Sidebar.jsx b/src/core/components/elements/Sidebar/Sidebar.jsx index c8fd8bab..7ea8f7c4 100644 --- a/src/core/components/elements/Sidebar/Sidebar.jsx +++ b/src/core/components/elements/Sidebar/Sidebar.jsx @@ -126,12 +126,9 @@ const Sidebar = ({ active, close }) => { <SidebarLink className={itemClassName} href='/video'> Indoteknik TV </SidebarLink> - <SidebarLink className={itemClassName} href='/about-us'> + <SidebarLink className={itemClassName} href='/tentang-kami'> Tentang Indoteknik </SidebarLink> - <SidebarLink className={itemClassName} href='/faqs'> - F.A.Q - </SidebarLink> <SidebarLink className={itemClassName} href='/contact-us'> Hubungi Kami </SidebarLink> diff --git a/src/core/components/layouts/BasicLayout.jsx b/src/core/components/layouts/BasicLayout.jsx index 1a7185cd..df42e403 100644 --- a/src/core/components/layouts/BasicLayout.jsx +++ b/src/core/components/layouts/BasicLayout.jsx @@ -1,5 +1,7 @@ import dynamic from 'next/dynamic' import BasicFooter from '../elements/Footer/BasicFooter' +import Image from 'next/image' +import whatsappUrl from '@/core/utils/whatsappUrl' const Navbar = dynamic(() => import('../elements/Navbar/Navbar')) const AnimationLayout = dynamic(() => import('./AnimationLayout')) @@ -8,7 +10,31 @@ const BasicLayout = ({ children }) => { return ( <> <Navbar /> - <AnimationLayout>{children}</AnimationLayout> + <AnimationLayout> + {children} + <div className='fixed bottom-4 right-4 sm:bottom-10 sm:right-8 z-50'> + <a + href={whatsappUrl(null)} + className='py-2 pl-3 pr-4 rounded-full bg-[#4FB84A] border border-green-300 flex items-center' + > + <Image + src='/images/socials/WHATSAPP.svg' + alt='Whatsapp' + className='block sm:hidden' + width={36} + height={36} + /> + <Image + src='/images/socials/WHATSAPP.svg' + alt='Whatsapp' + className='hidden sm:block' + width={44} + height={44} + /> + <span className='text-white font-bold ml-1.5'>Whatsapp</span> + </a> + </div> + </AnimationLayout> <BasicFooter /> </> ) diff --git a/src/core/utils/slug.js b/src/core/utils/slug.js index d5eecd3e..e91bcf83 100644 --- a/src/core/utils/slug.js +++ b/src/core/utils/slug.js @@ -9,7 +9,7 @@ import toTitleCase from './toTitleCase' * @param {number} id - The ID to be appended to the slug. * @returns {string} - The generated slug with the prefix, name, and ID. */ -const createSlug = (prefix, name, id) => { +const createSlug = (prefix, name, id, withHost = false) => { let slug = name ?.trim() @@ -19,7 +19,9 @@ const createSlug = (prefix, name, id) => { id let splitSlug = slug.split('-') let filterSlugFromEmptyChar = splitSlug.filter((x) => x != '') - return prefix + filterSlugFromEmptyChar.join('-') + slug = prefix + filterSlugFromEmptyChar.join('-') + if (withHost) slug = process.env.NEXT_PUBLIC_SELF_HOST + slug + return slug } /** diff --git a/src/core/utils/whatsappUrl.js b/src/core/utils/whatsappUrl.js new file mode 100644 index 00000000..6ca9722b --- /dev/null +++ b/src/core/utils/whatsappUrl.js @@ -0,0 +1,24 @@ +const whatsappUrl = (template = 'default', payload) => { + let url = 'https://wa.me/628128080622' + let text = '' + switch (template) { + case 'product': + text = `Halo, saya mau tanya ${payload.name}, bisa tolong bantu saya?\n\nBerikut ini linknya: ${payload.url}` + break + case 'productWeight': + text = `Mau tanya untuk berat ${payload.name}, bisa minta tolong informasikan beratnya?\n\nBerikut ini linknya: ${payload.url}` + break + case 'productSearch': + text = `Saya lagi cari-cari produk ${payload.name}, bisa bantu saya cari produknya?` + break + case null: + break; + default: + text = 'Halo, saya mau tanya-tanya seputar produk, bisa tolong bantu saya?' + break + } + if (text) url += `?text=${encodeURI(text)}` + return url +} + +export default whatsappUrl diff --git a/src/lib/auth/components/CompanyProfile.jsx b/src/lib/auth/components/CompanyProfile.jsx index 854aa246..13d4a194 100644 --- a/src/lib/auth/components/CompanyProfile.jsx +++ b/src/lib/auth/components/CompanyProfile.jsx @@ -76,7 +76,7 @@ const CompanyProfile = () => { Dibawah ini adalah data usaha yang anda masukkan, periksa kembali data usaha anda. </div> </div> - <div className='p-2 bg-gray_r-3 rounded'> + <div className='ml-2 p-2 bg-gray_r-3 rounded'> {!isOpen && <ChevronDownIcon className='w-6' />} {isOpen && <ChevronUpIcon className='w-6' />} </div> diff --git a/src/lib/auth/components/Menu.jsx b/src/lib/auth/components/Menu.jsx index 9a73609d..8a8e2e8a 100644 --- a/src/lib/auth/components/Menu.jsx +++ b/src/lib/auth/components/Menu.jsx @@ -9,10 +9,13 @@ const Menu = () => { return ( <div className='grid grid-cols-1 bg-white border border-gray_r-6 rounded py-2 px-4'> <div className='mt-4 mb-1 font-medium'>Menu</div> - <LinkItem href='/my/transactions' active={routeStartWith('/my/transaction')}> + <LinkItem href='/my/quotations' active={routeStartWith('/my/quotations')}> + Daftar Quotation + </LinkItem> + <LinkItem href='/my/transactions' active={routeStartWith('/my/transactions')}> Daftar Transaksi </LinkItem> - <LinkItem href='/my/invoices' active={routeStartWith('/my/invoice')}> + <LinkItem href='/my/invoices' active={routeStartWith('/my/invoices')}> Invoice & Faktur Pajak </LinkItem> <LinkItem href='/my/wishlist' active={routeStartWith('/my/wishlist')}> diff --git a/src/lib/auth/components/PersonalProfile.jsx b/src/lib/auth/components/PersonalProfile.jsx index 4a533ae9..bca54e24 100644 --- a/src/lib/auth/components/PersonalProfile.jsx +++ b/src/lib/auth/components/PersonalProfile.jsx @@ -9,7 +9,7 @@ import editPersonalProfileApi from '../api/editPersonalProfileApi' const PersonalProfile = () => { const auth = useAuth() - const [isOpen, setIsOpen] = useState(false) + const [isOpen, setIsOpen] = useState(true) const toggle = () => setIsOpen(!isOpen) const { register, setValue, handleSubmit } = useForm({ defaultValues: { @@ -54,7 +54,7 @@ const PersonalProfile = () => { Dibawah ini adalah data diri yang anda masukan, periksa kembali data diri anda </div> </div> - <div className='p-2 bg-gray_r-3 rounded'> + <div className='ml-2 p-2 bg-gray_r-3 rounded'> {!isOpen && <ChevronDownIcon className='w-6' />} {isOpen && <ChevronUpIcon className='w-6' />} </div> diff --git a/src/lib/auth/components/RegisterDesktop.jsx b/src/lib/auth/components/RegisterDesktop.jsx index 482a9ce3..f624fba7 100644 --- a/src/lib/auth/components/RegisterDesktop.jsx +++ b/src/lib/auth/components/RegisterDesktop.jsx @@ -4,6 +4,7 @@ import Link from '@/core/components/elements/Link/Link' import Alert from '@/core/components/elements/Alert/Alert' import PageContent from '@/lib/content/components/PageContent' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import ReCAPTCHA from 'react-google-recaptcha' const RegisterDesktop = () => { const { @@ -16,6 +17,7 @@ const RegisterDesktop = () => { fullnameRef, emailRef, passwordRef, + recaptchaRef, tnd, setTnd } = useRegister() @@ -89,6 +91,7 @@ const RegisterDesktop = () => { placeholder='••••••••••••' /> </div> + <ReCAPTCHA ref={recaptchaRef} sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_GOOGLE} /> <div class='flex items-center mt-4 '> <input type='checkbox' @@ -96,11 +99,12 @@ const RegisterDesktop = () => { className='form-input flex items-start w-fit mr-2' required /> - <label - onClick={() => setTnd(true)} - className='inline cursor-pointer text-danger-500' - > - Syarat dan Ketentuan + <label className='inline'> + Dengan ini saya menyetujui{' '} + <span onClick={() => setTnd(true)} className='cursor-pointer text-danger-500'> + syarat dan ketentuan + </span>{' '} + yang berlaku </label> </div> <button diff --git a/src/lib/auth/components/RegisterMobile.jsx b/src/lib/auth/components/RegisterMobile.jsx index f5c818e7..80ea6ab0 100644 --- a/src/lib/auth/components/RegisterMobile.jsx +++ b/src/lib/auth/components/RegisterMobile.jsx @@ -6,6 +6,7 @@ import useRegister from '../hooks/useRegister' import MobileView from '@/core/components/views/MobileView' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' import PageContent from '@/lib/content/components/PageContent' +import ReCAPTCHA from 'react-google-recaptcha' const RegisterMobile = () => { const { @@ -18,6 +19,7 @@ const RegisterMobile = () => { fullnameRef, emailRef, passwordRef, + recaptchaRef, tnd, setTnd } = useRegister() @@ -93,6 +95,7 @@ const RegisterMobile = () => { placeholder='••••••••••••' /> </div> + <ReCAPTCHA ref={recaptchaRef} sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_GOOGLE} /> <div class='flex items-center mt-4 '> <input type='checkbox' @@ -100,8 +103,12 @@ const RegisterMobile = () => { className='form-input flex items-start w-fit mr-2' required /> - <label onClick={() => setTnd(true)} className='inline cursor-pointer text-danger-500'> - Syarat dan Ketentuan + <label className='inline'> + Dengan ini saya menyetujui{' '} + <span onClick={() => setTnd(true)} className='cursor-pointer text-danger-500'> + syarat dan ketentuan + </span>{' '} + yang berlaku </label> </div> <button type='submit' className='btn-yellow w-full mt-2' disabled={!isValid || isLoading}> diff --git a/src/lib/auth/hooks/useRegister.js b/src/lib/auth/hooks/useRegister.js index 4b0b0d60..1a9412f8 100644 --- a/src/lib/auth/hooks/useRegister.js +++ b/src/lib/auth/hooks/useRegister.js @@ -33,8 +33,20 @@ const useRegister = () => { companyNameRef.current.value = '' } + const recaptchaRef = useRef(null) + const handleSubmit = async (e) => { e.preventDefault() + + const recaptchaValue = recaptchaRef.current.getValue() + if (!recaptchaValue) { + setAlert({ + children: 'ReCaptcha harus diisi', + type: 'info' + }) + return + } + setAlert(null) setIsLoading(true) const { fullname, email, password, companyName } = inputVal() @@ -75,6 +87,7 @@ const useRegister = () => { fullnameRef, emailRef, passwordRef, + recaptchaRef, tnd, setTnd } diff --git a/src/lib/brand/components/Brand.jsx b/src/lib/brand/components/Brand.jsx index 6ebb8aa7..3c411969 100644 --- a/src/lib/brand/components/Brand.jsx +++ b/src/lib/brand/components/Brand.jsx @@ -1,5 +1,5 @@ import useBrand from '../hooks/useBrand' -import Image from '@/core/components/elements/Image/Image' +import Image from 'next/image' import { Swiper, SwiperSlide } from 'swiper/react' import { Pagination, Autoplay } from 'swiper' @@ -29,6 +29,15 @@ const Brand = ({ id }) => { <> <div> {brand.isLoading && <ImageSkeleton />} + {brand.data?.banners?.length == 0 && ( + <Image + src='/images/default-banner-brand.jpg' + alt='Brand - Indoteknik' + width={1024} + height={512} + className='w-full h-auto' + /> + )} {brand.data && ( <> <Swiper @@ -43,6 +52,8 @@ const Brand = ({ id }) => { <Image src={banner} alt={`Brand ${brand.data?.name} - Indoteknik`} + width={1024} + height={512} className='w-full h-auto' /> </SwiperSlide> @@ -55,6 +66,8 @@ const Brand = ({ id }) => { src={brand?.data?.logo} alt={brand?.data?.name} className='w-32 p-2 border borde-gray_r-6 rounded' + width={256} + height={128} /> )} {!brand?.data?.logo && ( @@ -69,10 +82,20 @@ const Brand = ({ id }) => { <Divider /> </> </MobileView> + <DesktopView> <div className='container mx-auto mt-10 mb-3'> <div className='min-h-[150px]'> {brand.isLoading && <ImageSkeleton />} + {brand.data?.banners?.length == 0 && ( + <Image + src='/images/default-banner-brand.jpg' + alt='Brand - Indoteknik' + width={1024} + height={512} + className='w-full h-auto' + /> + )} {brand.data && ( <> <Swiper @@ -87,6 +110,8 @@ const Brand = ({ id }) => { <Image src={banner} alt={`Brand ${brand.data?.name} - Indoteknik`} + width={1024} + height={512} className='w-full h-auto' /> </SwiperSlide> @@ -99,6 +124,8 @@ const Brand = ({ id }) => { src={brand?.data?.logo} alt={brand?.data?.name} className='w-32 p-2 border borde-gray_r-6 rounded' + width={1024} + height={512} /> )} {!brand?.data?.logo && ( diff --git a/src/lib/cart/components/Cart.jsx b/src/lib/cart/components/Cart.jsx index d0685fe3..1008bffc 100644 --- a/src/lib/cart/components/Cart.jsx +++ b/src/lib/cart/components/Cart.jsx @@ -16,6 +16,7 @@ import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import ProductCard from '@/lib/product/components/ProductCard' import productSearchApi from '@/lib/product/api/productSearchApi' +import whatsappUrl from '@/core/utils/whatsappUrl' const Cart = () => { const router = useRouter() @@ -72,7 +73,7 @@ const Cart = () => { }, [products]) useEffect(() => { - const LoadProductSImilar = async () => { + const LoadProductSimilar = async () => { const randProductIndex = Math.floor(Math.random() * products.length) const productLoad = await productSearchApi({ query: `q=${products?.[randProductIndex].parent.name}&limit=10` @@ -80,8 +81,8 @@ const Cart = () => { setProductRecomendation(productLoad) } - if (products?.length > 0) LoadProductSImilar() - }, [products]) + if (products?.length > 0 && !productRecomendation) LoadProductSimilar() + }, [products, productRecomendation]) const updateQuantity = (value, productId, operation = '') => { let productIndex = products.findIndex((product) => product.id == productId) @@ -329,91 +330,92 @@ const Cart = () => { <td colSpan={6}>Keranjang belanja anda masih kosong</td> </tr> )} - {products && products?.map((product) => ( - <tr key={product.id}> - <td> - <input - type='checkbox' - onClick={() => toggleSelected(product.id)} - checked={product?.selected} - className='accent-danger-500 w-4' - /> - </td> - <td className='flex'> - <Link - href={createSlug( - '/shop/product/', - product?.parent.name, - product?.parent.id - )} - className='w-[20%] flex-shrink-0' - > - <Image - src={product?.parent?.image} - alt={product?.name} - className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + {products && + products?.map((product) => ( + <tr key={product.id}> + <td> + <input + type='checkbox' + onClick={() => toggleSelected(product.id)} + checked={product?.selected} + className='accent-danger-500 w-4' /> - </Link> - <div className='px-2 text-left'> + </td> + <td className='flex'> <Link href={createSlug( '/shop/product/', product?.parent.name, product?.parent.id )} - className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + className='w-[20%] flex-shrink-0' > - {product?.parent?.name} + <Image + src={product?.parent?.image} + alt={product?.name} + className='object-contain object-center border border-gray_r-6 h-28 w-full rounded-md' + /> </Link> - <div className='text-gray_r-11 mt-2'> - {product?.code}{' '} - {product?.attributes.length > 0 - ? `| ${product?.attributes.join(', ')}` - : ''} - </div> - </div> - </td> - <td> - <input - className='form-input w-16 py-2 text-center bg-gray_r-1' - type='number' - value={product?.quantity} - onChange={(e) => updateQuantity(e.target.value, product?.id)} - onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} - /> - </td> - <td> - {product?.price?.discountPercentage > 0 && ( - <div className='flex gap-x-1 items-center justify-center mt-3'> - <div className='text-gray_r-11 line-through text-caption-1'> - {currencyFormat(product?.price?.price)} + <div className='px-2 text-left'> + <Link + href={createSlug( + '/shop/product/', + product?.parent.name, + product?.parent.id + )} + className='line-clamp-2 leading-6 !text-gray_r-12 font-normal' + > + {product?.parent?.name} + </Link> + <div className='text-gray_r-11 mt-2'> + {product?.code}{' '} + {product?.attributes.length > 0 + ? `| ${product?.attributes.join(', ')}` + : ''} </div> - <div className='badge-solid-red'> - {product?.price?.discountPercentage}% + </div> + </td> + <td> + <input + className='form-input w-16 py-2 text-center bg-gray_r-1' + type='number' + value={product?.quantity} + onChange={(e) => updateQuantity(e.target.value, product?.id)} + onBlur={(e) => updateQuantity(e.target.value, product?.id, 'BLUR')} + /> + </td> + <td> + {product?.price?.discountPercentage > 0 && ( + <div className='flex gap-x-1 items-center justify-center mt-3'> + <div className='text-gray_r-11 line-through text-caption-1'> + {currencyFormat(product?.price?.price)} + </div> + <div className='badge-solid-red'> + {product?.price?.discountPercentage}% + </div> </div> + )} + <div className='font-normal mt-1'> + {currencyFormat(product?.price?.priceDiscount)} </div> - )} - <div className='font-normal mt-1'> - {currencyFormat(product?.price?.priceDiscount)} - </div> - </td> - <td> - <div className='text-danger-500 font-medium'> - {currencyFormat(product?.price?.priceDiscount * product?.quantity)} - </div> - </td> - <td> - <div className='flex justify-center items-center h-full'> - <button - className='btn-red p-1 ml-1' - onClick={() => setDeleteConfirmation(product)} - > - <TrashIcon className='w-4' /> - </button> - </div> - </td> - </tr> - ))} + </td> + <td> + <div className='text-danger-500 font-medium'> + {currencyFormat(product?.price?.priceDiscount * product?.quantity)} + </div> + </td> + <td> + <div className='flex justify-center items-center h-full'> + <button + className='btn-red p-1 ml-1' + onClick={() => setDeleteConfirmation(product)} + > + <TrashIcon className='w-4' /> + </button> + </div> + </td> + </tr> + ))} </tbody> </table> @@ -428,7 +430,7 @@ const Cart = () => { Tanya stock untuk pembelian anda sebelum melanjutkan pembayaran! <span> {' '} - <a href='https://wa.me/628128080622' className='text-danger-500'> + <a href={whatsappUrl()} className='text-danger-500'> Hubungi Kami </a> </span> diff --git a/src/lib/category/components/Category.jsx b/src/lib/category/components/Category.jsx index 9f34362c..884a871f 100644 --- a/src/lib/category/components/Category.jsx +++ b/src/lib/category/components/Category.jsx @@ -1,6 +1,7 @@ import odooApi from '@/core/api/odooApi' import Link from '@/core/components/elements/Link/Link' import DesktopView from '@/core/components/views/DesktopView' +import { createSlug } from '@/core/utils/slug' import { useEffect, useState } from 'react' const Category = () => { @@ -32,7 +33,7 @@ const Category = () => { {categories.map((category) => ( <div key={category.id}> <Link - href={`/shop/search?category=${category.name}`} + href={createSlug('/shop/category/', category.name, category.id)} className='category-mega-box__parent' > {category.name} @@ -42,7 +43,7 @@ const Category = () => { {category.childs.map((child1Category) => ( <div key={child1Category.id}> <Link - href={`/shop/search?category=${child1Category.name}`} + href={createSlug('/shop/category/', child1Category.name, child1Category.id)} className='category-mega-box__child-one mb-4' > {child1Category.name} @@ -50,7 +51,11 @@ const Category = () => { <div className='flex flex-col gap-y-3'> {child1Category.childs.map((child2Category) => ( <Link - href={`/shop/search?category=${child2Category.name}`} + href={createSlug( + '/shop/category/', + child2Category.name, + child2Category.id + )} className='category-mega-box__child-two' key={child2Category.id} > diff --git a/src/lib/checkout/components/Checkout.jsx b/src/lib/checkout/components/Checkout.jsx index 111117c9..f777a3a8 100644 --- a/src/lib/checkout/components/Checkout.jsx +++ b/src/lib/checkout/components/Checkout.jsx @@ -20,6 +20,8 @@ import Image from '@/core/components/elements/Image/Image' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import ExpedisiList from '../api/ExpedisiList' +import whatsappUrl from '@/core/utils/whatsappUrl' +import { createSlug } from '@/core/utils/slug' const Checkout = () => { const router = useRouter() @@ -166,8 +168,8 @@ const Checkout = () => { quantity: product.quantity })) let data = { - partner_shipping_id: selectedAddress.shipping.id, - partner_invoice_id: selectedAddress.invoicing.id, + partner_shipping_id: auth.partnerId, + partner_invoice_id: auth.partnerId, order_line: JSON.stringify(productOrder), delivery_amount : biayaKirim, type: 'sale_order' @@ -342,7 +344,7 @@ const Checkout = () => { <div className='px-4 mb-4'> <span className='text-danger-500'> *) Terdapat produk yang belum memiliki harga,{' '} - <a href='https://wa.me/628128080622' className='underline'> + <a href={whatsappUrl()} className='underline'> Hubungi Kami untuk meminta harga. </a> </span> @@ -465,7 +467,13 @@ const Checkout = () => { {product.price.priceDiscount > 0 ? ( currencyFormat(product?.price?.priceDiscount * product?.quantity) ) : ( - <a href='https://wa.me/628128080622' className='underline'> + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='underline' + > Call For Price{' '} </a> )} @@ -562,7 +570,7 @@ const Checkout = () => { <div className='mt-4'> <span className='text-danger-500'> *) Terdapat produk yang belum memiliki harga,{' '} - <a href='https://wa.me/628128080622' className='underline'> + <a href={whatsappUrl()} className='underline'> Hubungi Kami untuk meminta harga. </a> </span> diff --git a/src/lib/checkout/components/FinishCheckout.jsx b/src/lib/checkout/components/FinishCheckout.jsx index cd93e3a4..92245e31 100644 --- a/src/lib/checkout/components/FinishCheckout.jsx +++ b/src/lib/checkout/components/FinishCheckout.jsx @@ -14,7 +14,7 @@ const FinishCheckout = ({ query }) => { <p className='text-caption-2 text-warning-800'>No. Transaksi</p> </div> <Link - href='/my/transactions' + href='/my/quotations' className='bg-warning-400 text-warning-900 rounded-b-xl py-4 block' > Lihat detail pembelian Anda disini diff --git a/src/lib/checkout/email/FinishCheckoutEmail.jsx b/src/lib/checkout/email/FinishCheckoutEmail.jsx index e8f63afa..d40ce7d4 100644 --- a/src/lib/checkout/email/FinishCheckoutEmail.jsx +++ b/src/lib/checkout/email/FinishCheckoutEmail.jsx @@ -1,5 +1,6 @@ import currencyFormat from '@/core/utils/currencyFormat' import toTitleCase from '@/core/utils/toTitleCase' +import whatsappUrl from '@/core/utils/whatsappUrl' import { Body, Column, @@ -255,7 +256,7 @@ const FinishCheckoutEmail = ({ transaction, payment, statusPayment }) => { Jika ada pertanyaan seputar teknis pembayaran {transaction.address.customer.name}{' '} dapat hubungi kami melalui Email{' '} <a href='mailto:sales@indoteknik.com'>(sales@indoteknik.com)</a> atau Whatsapp{' '} - <a href='https://wa.me/628128080622' target='_blank' rel='noreferrer'> + <a href={whatsappUrl()} target='_blank' rel='noreferrer'> (+62 812-8080-622) </a> . diff --git a/src/lib/content/components/PageContent.jsx b/src/lib/content/components/PageContent.jsx index 61e5381d..8c467999 100644 --- a/src/lib/content/components/PageContent.jsx +++ b/src/lib/content/components/PageContent.jsx @@ -6,13 +6,21 @@ const PageContent = ({ path }) => { const fetchContent = async () => await pageContentApi({ path }) const content = useQuery(`content-${path}`, fetchContent) + if (content.isLoading) { + return ( + <div className='flex justify-center my-6'> + <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> + </div> + ) + } + if (content.data?.id) { let parsedContent = content.data.content parsedContent = parsedContent.replaceAll( 'src="/web/image', `src="${process.env.NEXT_PUBLIC_ODOO_API_HOST}/web/image` ) - const contentClassNames = ` + const contentClassNames = ` prose prose-gray prose-a:text-danger-500 @@ -23,19 +31,12 @@ const PageContent = ({ path }) => { prose-img:mb-1 prose-img:inline-block prose-hr:my-3 + max-w-none ` return <div className={contentClassNames} dangerouslySetInnerHTML={{ __html: parsedContent }} /> } - if (content.isLoading) { - return ( - <div className='flex justify-center my-6'> - <Spinner className='w-6 text-gray_r-12/50 fill-gray_r-12' /> - </div> - ) - } - return <></> } diff --git a/src/lib/flashSale/api/flashSaleApi.js b/src/lib/flashSale/api/flashSaleApi.js new file mode 100644 index 00000000..115b07dc --- /dev/null +++ b/src/lib/flashSale/api/flashSaleApi.js @@ -0,0 +1,8 @@ +import odooApi from '@/core/api/odooApi' + +const flashSaleApi = async () => { + const flashSale = await odooApi('GET', '/api/v1/flashsale/header') + return flashSale +} + +export default flashSaleApi diff --git a/src/lib/flashSale/components/FlashSale.jsx b/src/lib/flashSale/components/FlashSale.jsx new file mode 100644 index 00000000..e4a4a25c --- /dev/null +++ b/src/lib/flashSale/components/FlashSale.jsx @@ -0,0 +1,66 @@ +import { useEffect, useState } from 'react' +import flashSaleApi from '../api/flashSaleApi' +import Image from '@/core/components/elements/Image/Image' +import CountDown from '@/core/components/elements/CountDown/CountDown' +import productSearchApi from '@/lib/product/api/productSearchApi' +import ProductSlider from '@/lib/product/components/ProductSlider' + +const FlashSale = () => { + const [flashSales, setFlashSales] = useState(null) + + useEffect(() => { + const loadFlashSales = async () => { + const dataFlashSales = await flashSaleApi() + setFlashSales(dataFlashSales) + } + loadFlashSales() + }, []) + + return ( + flashSales?.length > 0 && ( + <div className='px-4 sm:px-0 grid grid-cols-1 gap-y-8'> + {flashSales.map((flashSale, index) => ( + <div key={index}> + <div className='flex gap-x-3 mb-4 justify-between sm:justify-start'> + <div className='font-medium sm:text-h-lg mt-1.5'>{flashSale.name}</div> + <CountDown initialTime={flashSale.duration} /> + </div> + + <div className='relative'> + <Image + src={flashSale.banner} + alt={flashSale.name} + className='w-full rounded mb-4 hidden sm:block' + /> + <Image + src={flashSale.bannerMobile} + alt={flashSale.name} + className='w-full rounded mb-4 block sm:hidden' + /> + <FlashSaleProduct flashSaleId={flashSale.pricelistId} /> + </div> + </div> + ))} + </div> + ) + ) +} + +const FlashSaleProduct = ({ flashSaleId }) => { + const [products, setProducts] = useState(null) + + useEffect(() => { + const loadProducts = async () => { + const dataProducts = await productSearchApi({ + query: `fq=flashsale_id_i:${flashSaleId}&fq=flashsale_price_f:[1 TO *]&limit=500`, + operation: 'AND' + }) + setProducts(dataProducts.response) + } + loadProducts() + }, [flashSaleId]) + + return <ProductSlider products={products} /> +} + +export default FlashSale diff --git a/src/lib/form/components/KunjunganService.jsx b/src/lib/form/components/KunjunganService.jsx index dfe5873e..076f6814 100644 --- a/src/lib/form/components/KunjunganService.jsx +++ b/src/lib/form/components/KunjunganService.jsx @@ -6,7 +6,7 @@ import ReCAPTCHA from 'react-google-recaptcha' import { Controller, useForm } from 'react-hook-form' import { toast } from 'react-hot-toast' import * as Yup from 'yup' -import createLeadsApi from '../api/createLeadsApi' +import createLeadApi from '../api/createLeadApi' const CreateKunjunganService = () => { const { @@ -41,18 +41,32 @@ const CreateKunjunganService = () => { } const data = { ...values, - name : 'Pengajuan Kunjungan Service - ' + values.company, - contact_name : values.cp, - email_from : values.email, - phone : values.mobile, - description : "\r\n Nama Perusahaan : " + values.company + " \r\n Alamat : " + values.address + " \r\n Propinsi : " + values.city + " \r\n Telepon: " + values.phone + " \r\n Handphone : " + values.mobile +" \r\n Email : " + values.email + " \r\n Keterangan : " + values.description , + name: 'Pengajuan Kunjungan Service - ' + values.company, + contact_name: values.cp, + email_from: values.email, + phone: values.mobile, + description: + '\r\n Nama Perusahaan : ' + + values.company + + ' \r\n Alamat : ' + + values.address + + ' \r\n Propinsi : ' + + values.city + + ' \r\n Telepon: ' + + values.phone + + ' \r\n Handphone : ' + + values.mobile + + ' \r\n Email : ' + + values.email + + ' \r\n Keterangan : ' + + values.description } - const create_leads = await createLeadsApi({ data }) + const create_leads = await createLeadApi({ data }) if (create_leads) { toast.success('Berhasil menambahkan alamat') reset() - recaptchaRef.current.reset() + recaptchaRef.current.reset() } } return ( @@ -125,9 +139,7 @@ const CreateKunjunganService = () => { type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.cp?.message} - </div> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.cp?.message}</div> </div> </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> @@ -156,7 +168,9 @@ const CreateKunjunganService = () => { </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> <div> - <label className='form-label mb-2'>Sebutkan: Merek, Tipe, Permasalahan, Service, Perawatan</label> + <label className='form-label mb-2'> + Sebutkan: Merek, Tipe, Permasalahan, Service, Perawatan + </label> <textarea {...register('description')} type='text' className='form-input' /> <div className='text-caption-2 text-danger-500 mt-1'> {errors.description?.message} @@ -181,24 +195,24 @@ const CreateKunjunganService = () => { ) } const validationSchema = Yup.object().shape({ - company: Yup.string().required('Harus di-isi'), - email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), - phone: Yup.string().required('Harus di-isi'), - city: Yup.string().required('Harus di-isi'), - cp: Yup.string().required('Harus di-isi'), - mobile: Yup.string().required('Harus di-isi'), - email: Yup.string().required('Harus di-isi'), - address: Yup.string().required('Harus di-isi') + company: Yup.string().required('Harus di-isi'), + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + phone: Yup.string().required('Harus di-isi'), + city: Yup.string().required('Harus di-isi'), + cp: Yup.string().required('Harus di-isi'), + mobile: Yup.string().required('Harus di-isi'), + email: Yup.string().required('Harus di-isi'), + address: Yup.string().required('Harus di-isi') }) const defaultValues = { - company:'', - email: '', - phone: '', - city: '', - cp: '', - mobile: '', - email: '', - address: '' + company: '', + email: '', + phone: '', + city: '', + cp: '', + mobile: '', + email: '', + address: '' } export default CreateKunjunganService diff --git a/src/lib/form/components/MediaRelations.jsx b/src/lib/form/components/MediaRelations.jsx new file mode 100644 index 00000000..05ae7e03 --- /dev/null +++ b/src/lib/form/components/MediaRelations.jsx @@ -0,0 +1,237 @@ +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' +import cityApi from '@/lib/address/api/cityApi' +import { yupResolver } from '@hookform/resolvers/yup' +import React, { useEffect, useRef, useState } from 'react' +import ReCAPTCHA from 'react-google-recaptcha' +import { Controller, useForm } from 'react-hook-form' +import { toast } from 'react-hot-toast' +import * as Yup from 'yup' +import createLeadApi from '../api/createLeadApi' + +const CreateMediaRelations = () => { + const { + register, + handleSubmit, + formState: { errors }, + control, + reset + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues + }) + const list_unit = [ + { + value: 'Media Cetak', + label: 'Media Cetak' + }, + { + value: 'Media', + label: 'Hospitality' + }, + { + value: 'Automotive', + label: 'Automotive' + }, + { + value: 'Retail', + label: 'Retail' + }, + { + value: 'Maining', + label: 'Maining' + }, + { + value: 'Lain - Lain', + label: 'Lain - Lain' + } + ] + const [cities, setCities] = useState([]) + const [company_unit, setCompany_unit] = useState(list_unit) + + const recaptchaRef = useRef(null) + + useEffect(() => { + const loadCities = async () => { + let dataCities = await cityApi() + dataCities = dataCities.map((city) => ({ value: city.id, label: city.name })) + setCities(dataCities) + } + loadCities() + }, []) + + const onSubmitHandler = async (values) => { + const attachment = document.getElementById('attachment').files[0] + + const recaptchaValue = recaptchaRef.current.getValue() + if (!recaptchaValue) { + toast.error('Catcha harus diisi') + return + } + const data = { + ...values, + name: 'Form Merchant - ' + values.company, + contact_name: values.cp, + email_from: values.email, + phone: values.phone, + description: + 'Nama Perusahaan : ' + + values.company + + ' \r\n Alamat : ' + + values.address + + ' \r\n Kota : ' + + values.city + + ' \r\n Unit Perusahaan : ' + + values.company_unit + + ' \r\n Telepon: ' + + values.phone + + ' \r\n Email : ' + + values.email + + ' \r\n Website : ' + + values.website + + ' \r\n No Hp : ' + + values.mobile + + 'Keterangan : ' + + values.description + } + const create_leads = await createLeadApi({ data }) + if (create_leads) { + toast.success('Berhasil menambahkan data') + reset() + recaptchaRef.current.reset() + } + } + return ( + <div className='container mx-auto p-4 md:p-0 my-0 md:my-10'> + <h1 className='text-h-sm md:text-title-sm font-semibold mb-6'>Form Media dan Relasi</h1> + <div className='w-full p-4 bg-white border border-gray_r-6 rounded'> + <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Nama Lengkap *</label> + <input + {...register('name')} + placeholder='Jhone Doe' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.name?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Nama Perusahan *</label> + <input + {...register('company')} + placeholder='PT.Indoteknik' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.company?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Alamat Email *</label> + <input + {...register('email')} + placeholder='contoh@email.com' + type='email' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>No. Telp *</label> + <input + {...register('phone')} + placeholder='021-XXXX' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.phone?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Jenis Media / Partnetship *</label> + <Controller + name='company_unit' + control={control} + render={(props) => <HookFormSelect {...props} options={company_unit} />} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.company_unit?.message} + </div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Jika anda memiliki media lain, sebutkan </label> + <input + {...register('other_media')} + placeholder='021-XXXX' + type='text' + className='form-input' + /> + </div> + </div> + + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'> + Lampiran (Company Peofile, Proposal Kerjasama, dll) + </label> + <input + {...register('attachment')} + type='file' + className='form-input' + accept='application/pdf' + id='attachment' + placeholder='Hello' + /> + </div> + </div> + + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <ReCAPTCHA ref={recaptchaRef} sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_GOOGLE} /> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <button type='submit' className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto'> + Simpan + </button> + </div> + </div> + </form> + </div> + </div> + ) +} +const validationSchema = Yup.object().shape({ + company: Yup.string().required('Harus di-isi'), + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + phone: Yup.string().required('Harus di-isi'), + cp: Yup.string().required('Harus di-isi'), + city: Yup.string().required('Harus di-isi'), + company_unit: Yup.string().required('Harus di-isi'), + address: Yup.string().required('Harus di-isi'), + website: Yup.string().required('Harus di-isi'), + mobile: Yup.string().required('Harus di-isi') +}) +const defaultValues = { + company: '', + email: '', + phone: '', + city: '', + company_unit: '', + cp: '', + address: '', + website: '', + mobile: '' +} + +export default CreateMediaRelations diff --git a/src/lib/form/components/Merchant.jsx b/src/lib/form/components/Merchant.jsx new file mode 100644 index 00000000..75b4e132 --- /dev/null +++ b/src/lib/form/components/Merchant.jsx @@ -0,0 +1,264 @@ +import HookFormSelect from '@/core/components/elements/Select/HookFormSelect' +import cityApi from '@/lib/address/api/cityApi' +import { yupResolver } from '@hookform/resolvers/yup' +import React, { useEffect, useRef, useState } from 'react' +import ReCAPTCHA from 'react-google-recaptcha' +import { Controller, useForm } from 'react-hook-form' +import { toast } from 'react-hot-toast' +import * as Yup from 'yup' +import createLeadApi from '../api/createLeadApi' + +const CreateMerchant = () => { + const { + register, + handleSubmit, + formState: { errors }, + control, + reset + } = useForm({ + resolver: yupResolver(validationSchema), + defaultValues + }) + const list_unit = [ + { + value: 'Manufacturing', + label: 'Manufacturing' + }, + { + value: 'Hospitality', + label: 'Hospitality' + }, + { + value: 'Automotive', + label: 'Automotive' + }, + { + value: 'Retail', + label: 'Retail' + }, + { + value: 'Maining', + label: 'Maining' + }, + { + value: 'Lain - Lain', + label: 'Lain - Lain' + } + ] + const [cities, setCities] = useState([]) + const [company_unit, setCompany_unit] = useState(list_unit) + + const recaptchaRef = useRef(null) + + useEffect(() => { + const loadCities = async () => { + let dataCities = await cityApi() + dataCities = dataCities.map((city) => ({ value: city.id, label: city.name })) + setCities(dataCities) + } + loadCities() + }, []) + + const onSubmitHandler = async (values) => { + const recaptchaValue = recaptchaRef.current.getValue() + if (!recaptchaValue) { + toast.error('Catcha harus diisi') + return + } + const data = { + ...values, + name: 'Form Merchant - ' + values.company, + contact_name: values.cp, + email_from: values.email, + phone: values.phone, + description: + 'Nama Perusahaan : ' + + values.company + + ' \r\n Alamat : ' + + values.address + + ' \r\n Kota : ' + + values.city + + ' \r\n Unit Perusahaan : ' + + values.company_unit + + ' \r\n Telepon: ' + + values.phone + + ' \r\n Email : ' + + values.email + + ' \r\n Website : ' + + values.website + + ' \r\n No Hp : ' + + values.mobile + + 'Keterangan : ' + + values.description + } + const create_leads = await createLeadApi({ data }) + if (create_leads) { + toast.success('Berhasil menambahkan data') + reset() + recaptchaRef.current.reset() + } + } + return ( + <div className='container mx-auto p-4 md:p-0 my-0 md:my-10'> + <h1 className='text-h-sm md:text-title-sm font-semibold mb-6'>Form Merchant</h1> + <div className='w-full p-4 bg-white border border-gray_r-6 rounded'> + <div + class='flex items-center bg-blue-100 border border-blue-400 text-blue-500 font-bold px-4 py-3 mb-4' + role='alert' + > + <p> + Penjualan online adalah hal yang HARUS dilakukan mulai sekarang. Perubahan dalam banyak + industri dan pola pembelian. Gabung dengan platform kami dan mulai penjualan lansung di + ribuan perusahaan d seluruh Indonesia.{' '} + </p> + </div> + <form onSubmit={handleSubmit(onSubmitHandler)}> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Nama Perusahan *</label> + <input + {...register('company')} + placeholder='PT.Indoteknik' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.company?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Alamat*</label> + <input + {...register('address')} + placeholder='jl. Bandengan no.31 ' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.address?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>No. Telp *</label> + <input + {...register('phone')} + placeholder='021-XXXX' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.phone?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Kota*</label> + <Controller + name='city' + control={control} + render={(props) => <HookFormSelect {...props} options={cities} />} + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.city?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Unit Perusahaan*</label> + <Controller + name='company_unit' + control={control} + render={(props) => <HookFormSelect {...props} options={company_unit} />} + /> + <div className='text-caption-2 text-danger-500 mt-1'> + {errors.company_unit?.message} + </div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Website *</label> + <input + {...register('website')} + placeholder='https://indoteknik.com' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.website?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Contact Person*</label> + <input + {...register('cp')} + placeholder='Jhone doe' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.cp?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>No HP *</label> + <input + {...register('mobile')} + placeholder='628XXXXXXX' + type='text' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.mobile?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Alamat Email *</label> + <input + {...register('email')} + placeholder='contoh@email.com' + type='email' + className='form-input' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <ReCAPTCHA ref={recaptchaRef} sitekey={process.env.NEXT_PUBLIC_RECAPTCHA_GOOGLE} /> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <button type='submit' className='btn-yellow w-full md:w-fit mt-6 ml-0 md:ml-auto'> + Simpan + </button> + </div> + </div> + </form> + </div> + </div> + ) +} +const validationSchema = Yup.object().shape({ + company: Yup.string().required('Harus di-isi'), + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + phone: Yup.string().required('Harus di-isi'), + cp: Yup.string().required('Harus di-isi'), + city: Yup.string().required('Harus di-isi'), + company_unit: Yup.string().required('Harus di-isi'), + address: Yup.string().required('Harus di-isi'), + website: Yup.string().required('Harus di-isi'), + mobile: Yup.string().required('Harus di-isi') +}) +const defaultValues = { + company: '', + email: '', + phone: '', + city: '', + company_unit: '', + cp: '', + address: '', + website: '', + mobile: '' +} + +export default CreateMerchant diff --git a/src/lib/form/components/PembayaranTempo.jsx b/src/lib/form/components/PembayaranTempo.jsx index 5f32753e..ffdb0961 100644 --- a/src/lib/form/components/PembayaranTempo.jsx +++ b/src/lib/form/components/PembayaranTempo.jsx @@ -52,7 +52,7 @@ const PembayaranTempo = () => { return ( <div className='container mx-auto p-4 md:p-0 my-0 md:my-10'> <h1 className='text-h-sm md:text-title-sm font-semibold mb-6'>Pembayaran Tempo</h1> - + <div className='w-full grid grid-cols-1 md:grid-cols-2'> <form onSubmit={handleSubmit(onSubmitHandler)} className='grid grid-cols-1 gap-y-6'> <div> diff --git a/src/lib/form/components/SuratDukungan.jsx b/src/lib/form/components/SuratDukungan.jsx index 22452b3c..0eab84a4 100644 --- a/src/lib/form/components/SuratDukungan.jsx +++ b/src/lib/form/components/SuratDukungan.jsx @@ -6,7 +6,7 @@ import ReCAPTCHA from 'react-google-recaptcha' import { Controller, useForm } from 'react-hook-form' import { toast } from 'react-hot-toast' import * as Yup from 'yup' -import createLeadsApi from '../api/createLeadsApi' +import createLeadsApi from '../api/createLeadApi' const CreateSuratDukungan = () => { const { @@ -41,18 +41,34 @@ const CreateSuratDukungan = () => { } const data = { ...values, - name : 'Pengajuan Kunjungan Service - ' + values.company, - contact_name : values.cp, - email_from : values.email, - phone : values.mobile, - description : "\r\n Nama Perusahaan : " + values.company + " \r\n Alamat : " + values.address + " \r\n Propinsi : " + values.city + " \r\n Telepon: " + values.phone + " \r\n Handphone : " + values.mobile +" \r\n Email : " + values.email + " \r\n Keterangan : " + values.description , + name: 'Permintaan Surat Dukungan - ' + values.company, + contact_name: values.company, + email_from: values.email, + phone: values.phone, + description: + 'Nama Perusahaan : ' + + values.company + + ' \r\n Alamat : ' + + values.address + + ' \r\n Npwp : ' + + values.npwp + + ' \r\n Telepon: ' + + values.phone + + ' \r\n Email : ' + + values.email + + ' \r\n Pengadaan : ' + + values.pengadaan + + ' \r\n Alamat 2 : ' + + values.address2 + + 'Keterangan : ' + + values.description } const create_leads = await createLeadsApi({ data }) if (create_leads) { toast.success('Berhasil menambahkan alamat') reset() - recaptchaRef.current.reset() + recaptchaRef.current.reset() } } return ( @@ -63,9 +79,10 @@ const CreateSuratDukungan = () => { class='flex items-center bg-blue-100 border border-blue-400 text-blue-500 font-bold px-4 py-3 mb-4' role='alert' > - <p> - Lengkapi form berikut untuk melakukan konfirmasi pembayaran. - </p> + <p>Lengkapi form berikut untuk melakukan konfirmasi pembayaran.</p> + </div> + <div className=' w-full md:w-[50%] p-4 bg-gray-50 border border-gray_r-6 rounded text-center'> + <h1>Data Peserta</h1> </div> <form onSubmit={handleSubmit(onSubmitHandler)}> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> @@ -82,18 +99,6 @@ const CreateSuratDukungan = () => { </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> <div> - <label className='form-label mb-2'>No. Telp *</label> - <input - {...register('phone')} - placeholder='021-XXXX' - type='text' - className='form-input' - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.phone?.message}</div> - </div> - </div> - <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> - <div> <label className='form-label mb-2'>Alamat*</label> <input {...register('address')} @@ -106,39 +111,26 @@ const CreateSuratDukungan = () => { </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> <div> - <label className='form-label mb-2'>Kota*</label> - <Controller - name='city' - control={control} - render={(props) => <HookFormSelect {...props} options={cities} />} - /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.city?.message}</div> - </div> - </div> - <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> - <div> - <label className='form-label mb-2'>Contact Person*</label> + <label className='form-label mb-2'>No. Telp *</label> <input - {...register('cp')} - placeholder='Jhone doe' + {...register('phone')} + placeholder='021-XXXX' type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'> - {errors.cp?.message} - </div> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.phone?.message}</div> </div> </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> <div> - <label className='form-label mb-2'>No HP *</label> + <label className='form-label mb-2'>NPWP *</label> <input - {...register('mobile')} - placeholder='628XXXXXXX' + {...register('npwp')} + placeholder='npwp number' type='text' className='form-input' /> - <div className='text-caption-2 text-danger-500 mt-1'>{errors.mobile?.message}</div> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.npwp?.message}</div> </div> </div> <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> @@ -153,10 +145,36 @@ const CreateSuratDukungan = () => { <div className='text-caption-2 text-danger-500 mt-1'>{errors.email?.message}</div> </div> </div> + <div className='w-[50%] mt-10 p-4 bg-gray-50 border border-gray_r-6 rounded text-center'> + <h1>Data Peserta</h1> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Pengadaan *</label> + <input {...register('pengadaan')} placeholder='' type='text' className='form-input' /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.pengadaan?.message}</div> + </div> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> + <div> + <label className='form-label mb-2'>Alamat *</label> + <textarea + {...register('address2')} + placeholder='' + type='text' + className='form-input h-[120px]' + /> + <div className='text-caption-2 text-danger-500 mt-1'>{errors.address2?.message}</div> + </div> + </div> + <div className='w-[50%] mt-10 p-4 bg-gray-50 border border-gray_r-6 rounded text-center'> + <h1>Data Produk</h1> + </div> + <div className='grid grid-cols-1 md:grid-cols-2 gap-4 mt-4'> <div> - <label className='form-label mb-2'>Sebutkan: Merek, Tipe, Permasalahan, Service, Perawatan</label> - <textarea {...register('description')} type='text' className='form-input' /> + <label className='form-label mb-2'>Produk</label> + <textarea {...register('description')} type='text' className='form-input h-[120px]' /> <div className='text-caption-2 text-danger-500 mt-1'> {errors.description?.message} </div> @@ -180,24 +198,24 @@ const CreateSuratDukungan = () => { ) } const validationSchema = Yup.object().shape({ - company: Yup.string().required('Harus di-isi'), - email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), - phone: Yup.string().required('Harus di-isi'), - city: Yup.string().required('Harus di-isi'), - cp: Yup.string().required('Harus di-isi'), - mobile: Yup.string().required('Harus di-isi'), - email: Yup.string().required('Harus di-isi'), - address: Yup.string().required('Harus di-isi') + company: Yup.string().required('Harus di-isi'), + email: Yup.string().email('Format harus seperti contoh@email.com').required('Harus di-isi'), + phone: Yup.string().required('Harus di-isi'), + npwp: Yup.string().required('Harus di-isi'), + pengadaan: Yup.string().required('Harus di-isi'), + email: Yup.string().required('Harus di-isi'), + address: Yup.string().required('Harus di-isi'), + address2: Yup.string().required('Harus di-isi') }) const defaultValues = { - company:'', - email: '', - phone: '', - city: '', - cp: '', - mobile: '', - email: '', - address: '' + company: '', + email: '', + phone: '', + pengadaan: '', + npwp: '', + email: '', + address: '', + address2: '' } export default CreateSuratDukungan diff --git a/src/lib/home/components/HeroBanner.jsx b/src/lib/home/components/HeroBanner.jsx index e6136e03..50cfc0ff 100644 --- a/src/lib/home/components/HeroBanner.jsx +++ b/src/lib/home/components/HeroBanner.jsx @@ -10,6 +10,7 @@ import 'swiper/css/pagination' import 'swiper/css/autoplay' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' +import Link from '@/core/components/elements/Link/Link' const HeroBanner = () => { const { heroBanners } = useHeroBanner() @@ -41,7 +42,9 @@ const HeroBanner = () => { <Swiper slidesPerView={1} className='border border-gray_r-6' {...swiperBannerMobile}> {heroBanners.data?.map((banner, index) => ( <SwiperSlide key={index}> - <Image src={banner.image} alt={banner.name} className='w-full h-auto' /> + <Link href={banner.url || ''} className='w-full h-auto'> + <Image src={banner.image} alt={banner.name} className='w-full h-auto' /> + </Link> </SwiperSlide> ))} </Swiper> @@ -50,7 +53,9 @@ const HeroBanner = () => { <Swiper slidesPerView={1} className='border border-gray_r-6' {...swiperBannerDesktop}> {heroBanners.data?.map((banner, index) => ( <SwiperSlide key={index}> - <Image src={banner.image} alt={banner.name} className='w-full h-auto' /> + <Link href={banner.url || ''} className='w-full h-auto'> + <Image src={banner.image} alt={banner.name} className='w-full h-auto' /> + </Link> </SwiperSlide> ))} </Swiper> diff --git a/src/lib/home/components/PreferredBrand.jsx b/src/lib/home/components/PreferredBrand.jsx index f97943cb..34c50220 100644 --- a/src/lib/home/components/PreferredBrand.jsx +++ b/src/lib/home/components/PreferredBrand.jsx @@ -14,7 +14,7 @@ const PreferredBrand = () => { <div className='flex justify-between items-center mb-4'> <div className='font-medium sm:text-h-lg'>Brand Pilihan</div> {isDesktop && ( - <Link href='/' className='btn-yellow !text-gray_r-12'> + <Link href='/shop/brands' className='btn-yellow !text-gray_r-12'> Lihat Semua </Link> )} diff --git a/src/lib/invoice/components/Invoices.jsx b/src/lib/invoice/components/Invoices.jsx index 6f7d54a0..96686bbb 100644 --- a/src/lib/invoice/components/Invoices.jsx +++ b/src/lib/invoice/components/Invoices.jsx @@ -24,7 +24,7 @@ const Invoices = () => { const router = useRouter() const { q = '', page = 1 } = router.query - const limit = 10 + const limit = 15 const query = { name: q, @@ -36,7 +36,7 @@ const Invoices = () => { const [inputQuery, setInputQuery] = useState(q) const [toOthers, setToOthers] = useState(null) - const pageCount = Math.ceil(invoices?.data?.saleOrderTotal / limit) + const pageCount = Math.ceil(invoices?.data?.invoiceTotal / limit) let pageQuery = _.omit(query, ['limit', 'offset']) pageQuery = _.pickBy(pageQuery, _.identity) pageQuery = toQuery(pageQuery) @@ -78,7 +78,7 @@ const Invoices = () => { {invoices.data?.invoices?.map((invoice, index) => ( <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}> <div className='grid grid-cols-2'> - <Link href={`/my/invoice/${invoice.id}`}> + <Link href={`${router.pathname}/${invoice.id}`}> <span className='text-caption-2 text-gray_r-11'>No. Invoice</span> <h2 className='text-danger-500 mt-1'>{invoice.name}</h2> </Link> @@ -91,7 +91,7 @@ const Invoices = () => { <EllipsisVerticalIcon className='w-5 h-5' onClick={() => setToOthers(invoice)} /> </div> </div> - <Link href={`/my/invoice/${invoice.id}`}> + <Link href={`${router.pathname}/${invoice.id}`}> <div className='grid grid-cols-2 text-caption-2 text-gray_r-11 mt-2 font-normal'> <p>{invoice.invoiceDate}</p> <p className='text-right'>{invoice.paymentTerm}</p> @@ -214,7 +214,7 @@ const Invoices = () => { {invoices.data?.invoices?.map((invoice) => ( <tr key={invoice.id}> <td> - <Link href={`/my/invoice/${invoice.id}`}>{invoice.name}</Link> + <Link href={`${router.pathname}/${invoice.id}`}>{invoice.name}</Link> </td> <td>{invoice.purchaseOrderName || '-'}</td> <td>{invoice.invoiceDate}</td> @@ -235,7 +235,7 @@ const Invoices = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`/my/transactions${pageQuery}`} + url={`/my/invoices${pageQuery}`} className='mt-2 mb-2' /> </div> diff --git a/src/lib/product/api/productApi.js b/src/lib/product/api/productApi.js index 8156d1ec..33f1265c 100644 --- a/src/lib/product/api/productApi.js +++ b/src/lib/product/api/productApi.js @@ -1,8 +1,8 @@ import odooApi from '@/core/api/odooApi' -const productApi = async ({ id }) => { +const productApi = async ({ id, headers = {} }) => { if (!id) return - const dataProduct = await odooApi('GET', `/api/v2/product/${id}`) + const dataProduct = await odooApi('GET', `/api/v2/product/${id}`, {}, headers) return dataProduct } diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index 71fb72e6..1626b7b7 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -1,9 +1,9 @@ import _ from 'lodash-contrib' import axios from 'axios' -const productSearchApi = async ({ query }) => { +const productSearchApi = async ({ query, operation = 'AND' }) => { const dataProductSearch = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=OR` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}` ) return dataProductSearch.data } diff --git a/src/lib/product/components/Product/ProductDesktop.jsx b/src/lib/product/components/Product/ProductDesktop.jsx index bb2b2db9..8ce6da00 100644 --- a/src/lib/product/components/Product/ProductDesktop.jsx +++ b/src/lib/product/components/Product/ProductDesktop.jsx @@ -11,7 +11,9 @@ import { updateItemCart } from '@/core/utils/cart' import { useRouter } from 'next/router' import { createSlug } from '@/core/utils/slug' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' -import { Toast } from 'flowbite-react' +import ProductCard from '../ProductCard' +import productSimilarApi from '../../api/productSimilarApi' +import whatsappUrl from '@/core/utils/whatsappUrl' const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { const router = useRouter() @@ -59,7 +61,6 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { selected: true }) setAddCartAlert(true) - // toast.success('Berhasil menambahkan ke keranjang') } const handleBuy = (variantId) => { @@ -85,61 +86,117 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { `fq=-manufacture_id_i:${product.manufacture?.id || 0}` ].join('&') + const [productSimilarInBrand, setProductSimilarInBrand] = useState(null) + + useEffect(() => { + const loadProductSimilarInBrand = async () => { + const productSimilarQuery = [product?.name, `fq=-product_id_i:${product.id}`].join('&') + const dataProductSimilar = await productSimilarApi({ query: productSimilarQuery }) + setProductSimilarInBrand(dataProductSimilar.products) + } + if (!productSimilarInBrand) loadProductSimilarInBrand() + }, [product, productSimilarInBrand]) + return ( <DesktopView> <div className='container mx-auto pt-10'> <div className='flex'> - <div className='w-[30%]'> - <Image - src={product.image} - alt={product.name} - className='h-96 object-contain object-center w-full border border-gray_r-4' - /> - </div> - <div className='w-[50%] px-4'> - <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1> - <div className='mt-10'> - <div className='flex p-3'> - <div className='w-1/4 text-gray_r-12/70'>Nomor SKU</div> - <div className='w-3/4'>SKU-{product.id}</div> - </div> - <div className='flex p-3 bg-gray_r-4'> - <div className='w-1/4 text-gray_r-12/70'>Part Number</div> - <div className='w-3/4'>{product.code || '-'}</div> + <div className='w-full flex flex-wrap'> + <div className='w-5/12'> + <Image + src={product.image} + alt={product.name} + className='h-96 object-contain object-center w-full border border-gray_r-4' + /> + </div> + + <div className='w-7/12 px-4'> + <h1 className='text-title-md leading-10 font-medium'>{product?.name}</h1> + <div className='mt-10'> + <div className='flex p-3'> + <div className='w-1/4 text-gray_r-12/70'>Nomor SKU</div> + <div className='w-3/4'>SKU-{product.id}</div> + </div> + <div className='flex p-3 bg-gray_r-4'> + <div className='w-1/4 text-gray_r-12/70'>Part Number</div> + <div className='w-3/4'>{product.code || '-'}</div> + </div> + <div className='flex p-3'> + <div className='w-1/4 text-gray_r-12/70'>Manufacture</div> + <div className='w-3/4'> + {product.manufacture?.name ? ( + <Link + href={createSlug( + '/shop/brands/', + product.manufacture?.name, + product.manufacture?.id + )} + > + {product.manufacture?.name} + </Link> + ) : ( + <div>-</div> + )} + </div> + </div> + <div className='flex p-3 bg-gray_r-4'> + <div className='w-1/4 text-gray_r-12/70'>Berat Barang</div> + <div className='w-3/4'> + {product?.weight > 0 && <span>{product?.weight} KG</span>} + {product?.weight == 0 && ( + <a + href={whatsappUrl('productWeight', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-danger-500 font-medium' + > + Tanya Berat + </a> + )} + </div> + </div> </div> - <div className='flex p-3'> - <div className='w-1/4 text-gray_r-12/70'>Manufacture</div> - <div className='w-3/4'> - {product.manufacture?.name ? ( - <Link - href={createSlug( - '/shop/brands/', - product.manufacture?.name, - product.manufacture?.id - )} + </div> + + <div className='w-full'> + <div className='mt-12'> + <div className='text-h-lg font-semibold'>Informasi Produk</div> + <div className='flex gap-x-4 mt-6 mb-4'> + {informationTabOptions.map((option) => ( + <TabButton + value={option.value} + key={option.value} + active={informationTab == option.value} + onClick={() => setInformationTab(option.value)} > - {product.manufacture?.name} - </Link> - ) : ( - <div>-</div> - )} + {option.label} + </TabButton> + ))} </div> - </div> - <div className='flex p-3 bg-gray_r-4'> - <div className='w-1/4 text-gray_r-12/70'>Berat Barang</div> - <div className='w-3/4'> - {product?.weight > 0 && <span>{product?.weight} KG</span>} - {product?.weight == 0 && ( - <a href='https://wa.me' className='text-danger-500 font-medium'> - Tanya Berat - </a> - )} + <div className='flex'> + <div className='w-3/4 leading-7 product__description'> + <TabContent active={informationTab == 'description'}> + <span + dangerouslySetInnerHTML={{ + __html: + product.description != '' + ? product.description + : 'Belum ada deskripsi produk.' + }} + /> + </TabContent> + + <TabContent active={informationTab == 'information'}> + Belum ada informasi. + </TabContent> + </div> </div> </div> </div> </div> - <div className='w-[20%]'> + <div className='w-[25%]'> {product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && ( <div className='text-gray_r-12/80'>Harga mulai dari: </div> )} @@ -160,7 +217,13 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { ) : ( <span className='text-gray_r-12/90 font-normal text-h-sm'> Hubungi kami untuk dapatkan harga terbaik, - <a href='https://wa.me/' className='text-danger-500 underline'> + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-danger-500 underline' + > klik disini </a> </span> @@ -209,6 +272,20 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { Wishlist </button> </div> + + <div className='border border-gray_r-6 overflow-auto mt-4'> + <div className='font-medium text-center p-4 bg-gray_r-1 border-b border-gray_r-6 sticky top-0 z-10'> + Produk Serupa + </div> + <div className='h-full divide-y divide-gray_r-6 max-h-96'> + {productSimilarInBrand && + productSimilarInBrand?.map((product) => ( + <div className='py-2' key={product.id}> + <ProductCard product={product} variant='horizontal' /> + </div> + ))} + </div> + </div> </div> </div> @@ -244,7 +321,13 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { {variant.price.priceDiscount > 0 ? ( currencyFormat(variant.price.priceDiscount) ) : ( - <a href='https://wa.me/' className='text-red_r-11'> + <a + href={whatsappUrl('product', { + name: variant.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-red_r-11' + > Call for price </a> )} @@ -283,38 +366,6 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => { </div> )} - <div className='mt-12'> - <div className='text-h-lg font-semibold'>Informasi Produk</div> - <div className='flex gap-x-4 mt-6 mb-4'> - {informationTabOptions.map((option) => ( - <TabButton - value={option.value} - key={option.value} - active={informationTab == option.value} - onClick={() => setInformationTab(option.value)} - > - {option.label} - </TabButton> - ))} - </div> - <div className='flex'> - <div className='w-3/4 leading-7 product__description'> - <TabContent active={informationTab == 'description'}> - <span - dangerouslySetInnerHTML={{ - __html: - product.description != '' - ? product.description - : 'Belum ada deskripsi produk.' - }} - /> - </TabContent> - - <TabContent active={informationTab == 'information'}>Belum ada informasi.</TabContent> - </div> - </div> - </div> - <div className='my-12'> <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div> <LazyLoad> diff --git a/src/lib/product/components/Product/ProductMobile.jsx b/src/lib/product/components/Product/ProductMobile.jsx index b75a191b..426fe1b8 100644 --- a/src/lib/product/components/Product/ProductMobile.jsx +++ b/src/lib/product/components/Product/ProductMobile.jsx @@ -13,6 +13,7 @@ import MobileView from '@/core/components/views/MobileView' import { toast } from 'react-hot-toast' import { createSlug } from '@/core/utils/slug' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' +import whatsappUrl from '@/core/utils/whatsappUrl' const ProductMobile = ({ product, wishlist, toggleWishlist }) => { const router = useRouter() @@ -152,7 +153,13 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { ) : ( <span className='text-gray_r-11 leading-6 font-normal'> Hubungi kami untuk dapatkan harga terbaik, - <a href='https://wa.me/' className='text-danger-500 underline'> + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-danger-500 underline' + > klik disini </a> </span> @@ -236,7 +243,13 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { </span> )} {activeVariant?.stock == 0 && ( - <a href='https://wa.me' className='text-danger-500 font-medium'> + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-danger-500 font-medium' + > Tanya Stok </a> )} @@ -244,7 +257,13 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => { <SpecificationContent label='Berat Barang'> {activeVariant?.weight > 0 && <span>{activeVariant?.weight} KG</span>} {activeVariant?.weight == 0 && ( - <a href='https://wa.me' className='text-danger-500 font-medium'> + <a + href={whatsappUrl('productWeight', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='text-danger-500 font-medium' + > Tanya Berat </a> )} diff --git a/src/lib/product/components/ProductCard.jsx b/src/lib/product/components/ProductCard.jsx index a1c30d00..9300643e 100644 --- a/src/lib/product/components/ProductCard.jsx +++ b/src/lib/product/components/ProductCard.jsx @@ -2,8 +2,14 @@ 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' +import whatsappUrl from '@/core/utils/whatsappUrl' const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { + const callForPriceWhatsapp = whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + }) + if (variant == 'vertical') { return ( <div className='rounded shadow-sm border border-gray_r-4 h-full bg-white'> @@ -58,7 +64,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice.priceDiscount) ) : ( - <a href='https://wa.me/'>Call for price</a> + <a href={callForPriceWhatsapp}>Call for price</a> )} </div> @@ -130,7 +136,7 @@ const ProductCard = ({ product, simpleTitle, variant = 'vertical' }) => { {product?.lowestPrice?.priceDiscount > 0 ? ( currencyFormat(product?.lowestPrice?.priceDiscount) ) : ( - <a href='https://wa.me/'>Call for price</a> + <a href={callForPriceWhatsapp}>Call for price</a> )} </div> diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index 81e7948b..cc85589d 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' import useProductSearch from '../hooks/useProductSearch' import ProductCard from './ProductCard' import Pagination from '@/core/components/elements/Pagination/Pagination' @@ -12,6 +12,9 @@ import DesktopView from '@/core/components/views/DesktopView' import NextImage from 'next/image' import ProductFilterDesktop from './ProductFilterDesktop' import { useRouter } from 'next/router' +import searchSpellApi from '@/core/api/searchSpellApi' +import Link from '@/core/components/elements/Link/Link' +import whatsappUrl from '@/core/utils/whatsappUrl' const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { const router = useRouter() @@ -19,6 +22,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { if (defaultBrand) query.brand = defaultBrand.toLowerCase() const { productSearch } = useProductSearch({ query }) const [products, setProducts] = useState(null) + const [spellings, setSpellings] = useState(null) const popup = useActive() const pageCount = Math.ceil( @@ -28,6 +32,30 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { const productRows = productSearch.data?.responseHeader.params.rows const productFound = productSearch.data?.response.numFound + useEffect(() => { + if (productFound == 0 && query.q) { + searchSpellApi({ query: query.q }).then((response) => { + const oddIndexSuggestions = response.data.spellcheck.suggestions.filter( + (_, index) => index % 2 === 1 + ) + const oddIndexCollations = response.data.spellcheck.collations.filter( + (_, index) => index % 2 === 1 + ) + const dataSpellings = oddIndexSuggestions.reduce((acc, curr) => { + oddIndexCollations.forEach((collation) => { + acc.push(collation.collationQuery) + }) + curr.suggestion.forEach((s) => { + if (!acc.includes(s.word)) acc.push(s.word) + }) + return acc + }, []) + + setSpellings(dataSpellings) + }) + } + }, [productFound, query]) + const brands = productSearch.data?.facetCounts?.facetFields?.manufactureName?.filter( (value, index) => { if (index % 2 === 0) { @@ -66,6 +94,20 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { } }, [query, products, productSearch]) + const SpellingComponent = useMemo(() => { + return ( + <> + Mungkin yang anda cari{' '} + {spellings?.map((spelling, i) => ( + <Link href={`/shop/search?q=${spelling}`} key={i} className='inline'> + {spelling} + {i + 1 < spellings.length ? ', ' : ''} + </Link> + ))} + </> + ) + }, [spellings]) + return ( <> <MobileView> @@ -97,13 +139,15 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { )} </> ) : ( - 'Mungkin yang anda cari' + SpellingComponent )} </div> - <button className='btn-light mb-6 py-2 px-5' onClick={popup.activate}> - Filter - </button> + {productFound > 0 && ( + <button className='btn-light mb-6 py-2 px-5' onClick={popup.activate}> + Filter + </button> + )} <div className='grid grid-cols-2 gap-3'> {products && @@ -127,6 +171,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { /> </div> </MobileView> + <DesktopView> <div className='container mx-auto mt-10 flex mb-3'> <div className='w-3/12'> @@ -164,7 +209,7 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { )} </> ) : ( - 'Mungkin yang anda cari' + SpellingComponent )} </div> <div className='justify-end flex '> @@ -202,7 +247,12 @@ const ProductSearch = ({ query, prefixUrl, defaultBrand = null }) => { <div className='text-gray_r-12/90'> <span> Barang yang anda cari tidak ada?{' '} - <a href='#' className='text-danger-500'> + <a + href={whatsappUrl('productSearch', { + name: query.q + })} + className='text-danger-500' + > Hubungi Kami </a> </span> diff --git a/src/lib/quotation/components/Quotation.jsx b/src/lib/quotation/components/Quotation.jsx index a68889f5..6ff40327 100644 --- a/src/lib/quotation/components/Quotation.jsx +++ b/src/lib/quotation/components/Quotation.jsx @@ -15,7 +15,6 @@ import VariantGroupCard from '@/lib/variant/components/VariantGroupCard' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import Image from '@/core/components/elements/Image/Image' -import variantPriceApi from '@/lib/variant/api/variantPriceApi' const Quotation = () => { const router = useRouter() @@ -32,15 +31,9 @@ const Quotation = () => { .map((o) => o.productId) .join(',') const dataProducts = await CartApi({ variantIds }) - const productsWithQuantity = dataProducts?.map(async (product) => { - const productPrice = await variantPriceApi({ id: product.id }) + const productsWithQuantity = dataProducts?.map((product) => { return { ...product, - price: { - price: productPrice.priceExclude, - discountPercentage: productPrice.discount, - priceDiscount: productPrice.priceExcludeAfterDiscount - }, quantity: getItemCart({ productId: product.id }).quantity } }) diff --git a/src/lib/transaction/components/Transaction.jsx b/src/lib/transaction/components/Transaction.jsx index 3e3f2cc7..30bb454a 100644 --- a/src/lib/transaction/components/Transaction.jsx +++ b/src/lib/transaction/components/Transaction.jsx @@ -2,7 +2,7 @@ import Spinner from '@/core/components/elements/Spinner/Spinner' import useTransaction from '../hooks/useTransaction' import TransactionStatusBadge from './TransactionStatusBadge' import Divider from '@/core/components/elements/Divider/Divider' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { downloadPurchaseOrder, downloadQuotation } from '../utils/transactions' import BottomPopup from '@/core/components/elements/Popup/BottomPopup' import uploadPoApi from '../api/uploadPoApi' diff --git a/src/lib/transaction/components/Transactions.jsx b/src/lib/transaction/components/Transactions.jsx index 13417707..bfb8c66b 100644 --- a/src/lib/transaction/components/Transactions.jsx +++ b/src/lib/transaction/components/Transactions.jsx @@ -19,15 +19,16 @@ import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' import Menu from '@/lib/auth/components/Menu' -const Transactions = () => { +const Transactions = ({ context = '' }) => { const router = useRouter() const { q = '', page = 1 } = router.query - const limit = 10 + const limit = 15 const query = { name: q, offset: (page - 1) * limit, + context, limit } const { transactions } = useTransactions({ query }) @@ -48,13 +49,13 @@ const Transactions = () => { } const pageCount = Math.ceil(transactions?.data?.saleOrderTotal / limit) - let pageQuery = _.omit(query, ['limit', 'offset']) + let pageQuery = _.omit(query, ['limit', 'offset', 'context']) pageQuery = _.pickBy(pageQuery, _.identity) pageQuery = toQuery(pageQuery) const handleSubmit = (e) => { e.preventDefault() - router.push(`/my/transactions?q=${inputQuery}`) + router.push(`${router.pathname}?q=${inputQuery}`) } return ( @@ -89,7 +90,7 @@ const Transactions = () => { {transactions.data?.saleOrders?.map((saleOrder, index) => ( <div className='p-4 shadow border border-gray_r-3 rounded-md' key={index}> <div className='grid grid-cols-2'> - <Link href={`/my/transaction/${saleOrder.id}`}> + <Link href={`${router.pathname}/${saleOrder.id}`}> <span className='text-caption-2 text-gray_r-11'>No. Transaksi</span> <h2 className='text-danger-500 mt-1'>{saleOrder.name}</h2> </Link> @@ -101,7 +102,7 @@ const Transactions = () => { /> </div> </div> - <Link href={`/my/transaction/${saleOrder.id}`}> + <Link href={`${router.pathname}/${saleOrder.id}`}> <div className='grid grid-cols-2 mt-3'> <div> <span className='text-caption-2 text-gray_r-11'>No. Purchase Order</span> @@ -135,7 +136,7 @@ const Transactions = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`/my/transactions${pageQuery}`} + url={router.pathname + pageQuery} className='mt-2 mb-2' /> @@ -249,7 +250,7 @@ const Transactions = () => { {transactions.data?.saleOrders?.map((saleOrder) => ( <tr key={saleOrder.id}> <td> - <Link href={`/my/transaction/${saleOrder.id}`}>{saleOrder.name}</Link> + <Link href={`${router.pathname}/${saleOrder.id}`}>{saleOrder.name}</Link> </td> <td>-</td> <td className='!text-left'>{saleOrder.sales}</td> @@ -267,7 +268,7 @@ const Transactions = () => { <Pagination pageCount={pageCount} currentPage={parseInt(page)} - url={`/my/transactions${pageQuery}`} + url={router.pathname + pageQuery} className='mt-2 mb-2' /> </div> diff --git a/src/lib/variant/components/VariantCard.jsx b/src/lib/variant/components/VariantCard.jsx index 28f3e193..0f9f02f6 100644 --- a/src/lib/variant/components/VariantCard.jsx +++ b/src/lib/variant/components/VariantCard.jsx @@ -6,6 +6,7 @@ import Link from '@/core/components/elements/Link/Link' import { createSlug } from '@/core/utils/slug' import currencyFormat from '@/core/utils/currencyFormat' import { updateItemCart } from '@/core/utils/cart' +import whatsappUrl from '@/core/utils/whatsappUrl' const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { const router = useRouter() @@ -60,7 +61,13 @@ const VariantCard = ({ product, openOnClick = true, buyMore = false }) => { {product.price.priceDiscount > 0 ? ( currencyFormat(product.quantity * product.price.priceDiscount) ) : ( - <a href='https://wa.me/628128080622' className='underline text-danger-500'> + <a + href={whatsappUrl('product', { + name: product.name, + url: createSlug('/shop/product/', product.name, product.id, true) + })} + className='underline text-danger-500' + > Call For Price{' '} </a> )} diff --git a/src/pages/404.jsx b/src/pages/404.jsx new file mode 100644 index 00000000..f73e6383 --- /dev/null +++ b/src/pages/404.jsx @@ -0,0 +1,22 @@ +import Seo from '@/core/components/Seo' +import Link from '@/core/components/elements/Link/Link' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import Image from 'next/image' + +export default function PageNotFound() { + return ( + <BasicLayout> + <Seo title='Halaman tidak ditemukan - Indoteknik.com' /> + <div className='container mx-auto'> + <Image + src='/images/page-not-found.svg' + alt='Page not found - Indoteknik' + className='w-full' + width={1024} + height={512} + /> + <Link href='/' className='btn-yellow text-black mx-auto mt-4'>Kembali ke halaman utama</Link> + </div> + </BasicLayout> + ) +} diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 3c93d27f..2d74d8b7 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,7 +1,7 @@ import '../styles/globals.css' import NextProgress from 'next-progress' import { useRouter, Router } from 'next/router' -import { AnimatePresence } from 'framer-motion' +import { AnimatePresence, motion } from 'framer-motion' import { Toaster } from 'react-hot-toast' import { QueryClient, QueryClientProvider } from 'react-query' import useDevice from '@/core/hooks/useDevice' @@ -17,12 +17,17 @@ function MyApp({ Component, pageProps }) { const [animateLoader, setAnimateLoader] = useState(false) useEffect(() => { - Router.events.on('routeChangeStart', () => setAnimateLoader(true)) - Router.events.on('routeChangeComplete', () => setAnimateLoader(false)) + const handleRouteChangeStart = () => setAnimateLoader(true) + const handleRouteChangeComplete = () => setAnimateLoader(false) + + Router.events.on('routeChangeStart', handleRouteChangeStart) + Router.events.on('routeChangeComplete', handleRouteChangeComplete) + Router.events.on('routeChangeError', handleRouteChangeComplete) return () => { - Router.events.off('routeChangeStart', () => setAnimateLoader(true)) - Router.events.off('routeChangeComplete', () => setAnimateLoader(false)) + Router.events.off('routeChangeStart', handleRouteChangeStart) + Router.events.off('routeChangeComplete', handleRouteChangeComplete) + Router.events.off('routeChangeError', handleRouteChangeComplete) } }, []) @@ -43,11 +48,21 @@ function MyApp({ Component, pageProps }) { return ( <> - {animateLoader && ( - <div className='w-screen h-screen z-[500] flex justify-center items-center backdrop-blur'> - <LogoSpinner /> - </div> - )} + <AnimatePresence> + {animateLoader && ( + <motion.div + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + transition={{ + duration: 0.1 + }} + className='fixed w-screen h-screen z-[500] bg-white flex justify-center items-center' + > + <LogoSpinner /> + </motion.div> + )} + </AnimatePresence> <Toaster position='top-center' containerStyle={toasterStyle} diff --git a/src/pages/_document.jsx b/src/pages/_document.jsx index ff205a38..dbe210aa 100644 --- a/src/pages/_document.jsx +++ b/src/pages/_document.jsx @@ -2,9 +2,13 @@ import { Html, Head, Main, NextScript } from 'next/document' import Script from 'next/script' export default function MyDocument() { + const env = process.env.NODE_ENV + return ( <Html> <Head> + <link rel='icon' href='/favicon.ico' /> + <meta name='facebook-domain-verification' content='328wmjs7hcnz74rwsqzxvq50rmbtm2' /> <Script async @@ -57,6 +61,24 @@ export default function MyDocument() { }} /> + {/* <Script + id='tawk-script-tag' + strategy='afterInteractive' + dangerouslySetInnerHTML={{ + __html: ` + var Tawk_API=Tawk_API||{}, Tawk_LoadStart=new Date(); + (function(){ + var s1=document.createElement("script"),s0=document.getElementsByTagName("script")[0]; + s1.async=true; + s1.src='https://embed.tawk.to/643e00614247f20fefec36c5/1gu932vhh'; + s1.charset='UTF-8'; + s1.setAttribute('crossorigin','*'); + s0.parentNode.insertBefore(s1,s0); + })(); + ` + }} + /> */} + <meta name='google-site-verification' content='uHuW_mZhjv_fUg3do-lV8Mo7R2vVjF4MPQhTXFP3jH4' diff --git a/src/pages/activate.jsx b/src/pages/activate.jsx index 48d9c4d3..8e9d8fbb 100644 --- a/src/pages/activate.jsx +++ b/src/pages/activate.jsx @@ -1,9 +1,11 @@ import Seo from '@/core/components/Seo' -import SimpleFooter from '@/core/components/elements/Footer/SimpleFooter' -import BasicLayout from '@/core/components/layouts/BasicLayout' import DesktopView from '@/core/components/views/DesktopView' import MobileView from '@/core/components/views/MobileView' -import ActivateComponent from '@/lib/auth/components/Activate' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const ActivateComponent = dynamic(() => import('@/lib/auth/components/Activate')) +const SimpleFooter = dynamic(() => import('@/core/components/elements/Footer/SimpleFooter')) export default function Activate() { return ( diff --git a/src/pages/api/shop/finish-checkout.js b/src/pages/api/shop/finish-checkout.js index 04e82118..9eaa36db 100644 --- a/src/pages/api/shop/finish-checkout.js +++ b/src/pages/api/shop/finish-checkout.js @@ -46,7 +46,7 @@ export default async function handler(req, res) { } } - const query = `name=${orderName.replaceAll('-', '/')}&limit=1` + const query = `name=${orderName.replaceAll('-', '/')}&limit=1&context=quotation` const searchTransaction = await odooApi( 'GET', `/api/v1/partner/${auth.partnerId}/sale_order?${query}`, diff --git a/src/pages/api/shop/midtrans-payment.js b/src/pages/api/shop/midtrans-payment.js index 1772e9e0..12aaa51f 100644 --- a/src/pages/api/shop/midtrans-payment.js +++ b/src/pages/api/shop/midtrans-payment.js @@ -22,6 +22,7 @@ export default async function handler(req, res) { {}, { Token: auth.token } ) + if (!transaction?.id) { return res.status(400).json({ error: 'No Data' }) } @@ -48,7 +49,7 @@ export default async function handler(req, res) { const parameter = { transaction_details: { order_id: transaction.name?.replaceAll('/', '-'), - gross_amount: transaction.amountTotal + gross_amount: Math.floor(transaction.amountTotal) }, credit_card: { secure: true @@ -56,17 +57,7 @@ export default async function handler(req, res) { customer_details: { first_name: transaction.address.customer.name, email: transaction.address.customer.email || '', - phone: transaction.address.customer.phone || '', - billing_address: { - first_name: transaction.address.invoice.name, - email: transaction.address.invoice.email || '', - phone: transaction.address.invoice.phone || '' - }, - shipping_address: { - first_name: transaction.address.shipping.name, - email: transaction.address.shipping.email || '', - phone: transaction.address.shipping.phone || '' - } + phone: transaction.address.customer.phone || '' } } diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 19b9655a..2e6764fd 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -29,6 +29,9 @@ export default async function handler(req, res) { case 'stock': paramOrderBy += 'stock_total_f DESC' break + default: + paramOrderBy += 'product_rating_f DESC, price_discount_f DESC' + break } let offset = (page - 1) * limit @@ -43,10 +46,17 @@ export default async function handler(req, res) { 'qf=name_s', `start=${offset}`, `rows=${limit}`, - `sort=${paramOrderBy}`, - `fq=price_discount_f:[${priceFrom == '' ? '*' : priceFrom} TO ${priceTo == '' ? '*' : priceTo}]` + `sort=${paramOrderBy}` ] + if (priceFrom > 0 || priceTo > 0) { + parameter.push( + `fq=price_discount_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ + priceTo == '' ? '*' : priceTo + }]` + ) + } + if (brand) parameter.push(`fq=manufacture_name:${brand}`) if (category) parameter.push(`fq=category_name:${category}`) @@ -57,7 +67,12 @@ export default async function handler(req, res) { let result = await axios(process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&')) try { - result.data.response.products = productResponseMap(result.data.response.docs) + let { auth } = req.cookies + if (auth) auth = JSON.parse(auth) + result.data.response.products = productResponseMap( + result.data.response.docs, + auth?.pricelist || false + ) 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 @@ -83,23 +98,42 @@ const escapeSolrQuery = (query) => { return escapedWords.join(' ') } -const productResponseMap = (products) => { +const productResponseMap = (products, pricelist) => { return products.map((product) => { + let price = product.price_f || 0 + let priceDiscount = product.price_discount_f || 0 + let discountPercentage = product.discount_f || 0 + + if (pricelist) { + const pricelistDiscount = product?.[`price_${pricelist}_f`] || false + const pricelistDiscountPerc = product?.[`discount_${pricelist}_f`] || false + + if (pricelistDiscount && pricelistDiscount > 0) priceDiscount = pricelistDiscount + if (pricelistDiscountPerc && pricelistDiscountPerc > 0) + discountPercentage = pricelistDiscountPerc + } + + if (product?.flashsale_id_i > 0) { + price = product?.flashsale_base_price_f || 0 + priceDiscount = product?.flashsale_price_f || 0 + discountPercentage = product?.flashsale_discount_f || 0 + } + let productMapped = { id: product.product_id_i || '', image: product.image_s || '', code: product.default_code_s || '', name: product.name_s || '', - lowestPrice: { - price: product.price_f || 0, - priceDiscount: product.price_discount_f || 0, - discountPercentage: product.discount_f || 0 - }, + lowestPrice: { price, priceDiscount, discountPercentage }, variantTotal: product.variant_total_i || 0, stockTotal: product.stock_total_f || 0, weight: product.weight_f || 0, manufacture: {}, - categories: [] + categories: [], + flashSale: { + id: product?.flashsale_id_i, + name: product?.product?.flashsale_name_s + } } if (product.manufacture_id_i && product.manufacture_name_s) { diff --git a/src/pages/api/shop/spell.js b/src/pages/api/shop/spell.js new file mode 100644 index 00000000..4c01765c --- /dev/null +++ b/src/pages/api/shop/spell.js @@ -0,0 +1,18 @@ +import axios from 'axios' + +export default async function handler(req, res) { + const { q = '' } = req.query + + let result = await axios( + process.env.SOLR_HOST + `/solr/product/spell?indent=true&q.op=AND&q=${q}` + ) + + try { + res.status(200).json(result.data) + } catch (error) { + res.status(400).json({ + numFound: 0, + suggestions: [] + }) + } +} diff --git a/src/pages/blog/[slug].jsx b/src/pages/blog/[slug].jsx index 9305411d..39c0bb50 100644 --- a/src/pages/blog/[slug].jsx +++ b/src/pages/blog/[slug].jsx @@ -1,10 +1,11 @@ import Seo from '@/core/components/Seo' -import Link from '@/core/components/elements/Link/Link' import Spinner from '@/core/components/elements/Spinner/Spinner' -import BasicLayout from '@/core/components/layouts/BasicLayout' -import { createSlug, getIdFromSlug } from '@/core/utils/slug' +import { getIdFromSlug } from '@/core/utils/slug' import useBlog from '@/lib/blog/hooks/useBlog' import { useRouter } from 'next/router' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) export default function BlogDetail() { const router = useRouter() diff --git a/src/pages/blog/index.jsx b/src/pages/blog/index.jsx index c65fe07d..17446935 100644 --- a/src/pages/blog/index.jsx +++ b/src/pages/blog/index.jsx @@ -1,12 +1,14 @@ -import BasicLayout from '@/core/components/layouts/BasicLayout' import Link from 'next/link' +import { useRouter } from 'next/router' import { createSlug } from '@/core/utils/slug' -import Image from '@/core/components/elements/Image/Image' import useBlogs from '@/lib/blog/hooks/useBlogs' -import { useRouter } from 'next/router' -import Pagination from '@/core/components/elements/Pagination/Pagination' -import Spinner from '@/core/components/elements/Spinner/Spinner' import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const Pagination = dynamic(() => import('@/core/components/elements/Pagination/Pagination')) +const Spinner = dynamic(() => import('@/core/components/elements/Spinner/Spinner')) +const Image = dynamic(() => import('@/core/components/elements/Image/Image')) export default function Blogs() { const router = useRouter() diff --git a/src/pages/contact-us.jsx b/src/pages/contact-us.jsx deleted file mode 100644 index adbf9439..00000000 --- a/src/pages/contact-us.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import Seo from '@/core/components/Seo' -import BasicLayout from '@/core/components/layouts/BasicLayout' -import PageContent from '@/lib/content/components/PageContent' - -export default function ContactUs() { - return ( - <BasicLayout> - <Seo title='Hubungi Indoteknik.com' /> - <PageContent path='/contact-us' /> - </BasicLayout> - ) -} diff --git a/src/pages/faqs.jsx b/src/pages/faqs.jsx deleted file mode 100644 index e8ffb4e3..00000000 --- a/src/pages/faqs.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import Seo from '@/core/components/Seo' -import BasicLayout from '@/core/components/layouts/BasicLayout' -import PageContent from '@/lib/content/components/PageContent' - -export default function ContactUs() { - return ( - <BasicLayout> - <Seo title='FAQS Indoteknik.com' /> - <PageContent path='/faqs' /> - </BasicLayout> - ) -} diff --git a/src/pages/hubungi-kami.jsx b/src/pages/hubungi-kami.jsx new file mode 100644 index 00000000..a246b515 --- /dev/null +++ b/src/pages/hubungi-kami.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function HubungiKami() { + return ( + <BasicLayout> + <Seo title='Hubungi Kami - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/hubungi-kami' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 2a996b5d..5ca7b939 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -3,33 +3,29 @@ import ImageSkeleton from '@/core/components/elements/Skeleton/ImageSkeleton' import PopularProductSkeleton from '@/lib/home/components/Skeleton/PopularProductSkeleton' import MobileView from '@/core/components/views/MobileView' import DesktopView from '@/core/components/views/DesktopView' -import { useEffect, useRef, useState } from 'react' -import { NextSeo } from 'next-seo' +import { useRef } from 'react' import Seo from '@/core/components/Seo' import { useQuery } from 'react-query' import odooApi from '@/core/api/odooApi' -import Image from '@/core/components/elements/Image/Image' +import Image from 'next/image' const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) - +const FlashSale = dynamic(() => import('@/lib/flashSale/components/FlashSale')) const HeroBanner = dynamic(() => import('@/lib/home/components/HeroBanner'), { loading: () => <ImageSkeleton /> }) - const PreferredBrand = dynamic(() => import('@/lib/home/components/PreferredBrand'), { loading: () => <PopularProductSkeleton /> }) - const PopularProduct = dynamic(() => import('@/lib/home/components/PopularProduct'), { loading: () => <PopularProductSkeleton /> }) - const CategoryHomeId = dynamic(() => import('@/lib/home/components/CategoryHomeId'), { loading: () => <PopularProductSkeleton /> }) export default function Home() { - const fetchSecondHeroBanner = async () => await odooApi('GET', '/api/v1/banner?type=index-b-2') + const fetchSecondHeroBanner = async () => await odooApi('GET', '/api/v1/banner?type=index-a-2') const secondHeroBanner = useQuery('secondHeroBanner', fetchSecondHeroBanner) const bannerRef = useRef(null) @@ -57,12 +53,18 @@ export default function Home() { <DesktopView> <div className='container mx-auto'> <div className='flex h-[360px]' ref={wrapperRef} onLoad={handleOnLoad}> - <div className='w-3/12'> + <div className='w-2/12'> {secondHeroBanner.isFetched && ( - <Image src={secondHeroBanner.data[0].image} alt={secondHeroBanner.data[0].name} /> + <Image + src={secondHeroBanner.data[0].image} + width={512} + height={1024} + alt={secondHeroBanner.data[0].name} + className='object-cover object-center h-full' + /> )} </div> - <div className='w-6/12 px-1' ref={bannerRef}> + <div className='w-7/12 px-1' ref={bannerRef}> <HeroBanner /> </div> <div className='w-3/12'> @@ -72,6 +74,7 @@ export default function Home() { <div className='my-16 flex flex-col gap-y-10'> <PreferredBrand /> + <FlashSale /> <CategoryHomeId /> </div> </div> @@ -81,6 +84,7 @@ export default function Home() { <HeroBanner /> <div className='flex flex-col gap-y-6 my-6'> <PreferredBrand /> + <FlashSale /> <PopularProduct /> <CategoryHomeId /> </div> diff --git a/src/pages/informasi-garansi.jsx b/src/pages/informasi-garansi.jsx new file mode 100644 index 00000000..c4511d30 --- /dev/null +++ b/src/pages/informasi-garansi.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function InformasiGaransi() { + return ( + <BasicLayout> + <Seo title='Informasi Garansi - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/informasi-garansi' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/karir.jsx b/src/pages/karir.jsx new file mode 100644 index 00000000..406397d9 --- /dev/null +++ b/src/pages/karir.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function KebijakanPrivasi() { + return ( + <BasicLayout> + <Seo title='Karir - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/karir' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/kebijakan-privasi.jsx b/src/pages/kebijakan-privasi.jsx new file mode 100644 index 00000000..8662d56d --- /dev/null +++ b/src/pages/kebijakan-privasi.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function KebijakanPrivasi() { + return ( + <BasicLayout> + <Seo title='Kebijakan Privasi - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/kebijakan-privasi' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/metode-pembayaran.jsx b/src/pages/metode-pembayaran.jsx new file mode 100644 index 00000000..8e5d6467 --- /dev/null +++ b/src/pages/metode-pembayaran.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function MetodePembayaran() { + return ( + <BasicLayout> + <Seo title='Metode Pembayaran - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/metode-pembayaran' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/metode-pengiriman.jsx b/src/pages/metode-pengiriman.jsx new file mode 100644 index 00000000..2b0b65fa --- /dev/null +++ b/src/pages/metode-pengiriman.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function MetodePengiriman() { + return ( + <BasicLayout> + <Seo title='Metode Pengiriman - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/metode-pengiriman' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/my/daftar-merchant.jsx b/src/pages/my/daftar-merchant.jsx new file mode 100644 index 00000000..e1fa9bcb --- /dev/null +++ b/src/pages/my/daftar-merchant.jsx @@ -0,0 +1,15 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import CreateMerchant from '@/lib/form/components/Merchant' + +export default function DaftarMerchant() { + return ( + <> + <Seo title='Daftar Merchant - Indoteknik.com' /> + + <BasicLayout> + <CreateMerchant></CreateMerchant> + </BasicLayout> + </> + ) +} diff --git a/src/pages/my/invoice/[id].jsx b/src/pages/my/invoices/[id].jsx index 740819cc..5972b382 100644 --- a/src/pages/my/invoice/[id].jsx +++ b/src/pages/my/invoices/[id].jsx @@ -6,7 +6,7 @@ import IsAuth from '@/lib/auth/components/IsAuth' import InvoiceComponent from '@/lib/invoice/components/Invoice' import { useRouter } from 'next/router' -export default function Invoice() { +export default function MyInvoice() { const router = useRouter() return ( diff --git a/src/pages/my/invoices.jsx b/src/pages/my/invoices/index.jsx index 59059b2e..73c7b9fe 100644 --- a/src/pages/my/invoices.jsx +++ b/src/pages/my/invoices/index.jsx @@ -5,7 +5,7 @@ import MobileView from '@/core/components/views/MobileView' import IsAuth from '@/lib/auth/components/IsAuth' import InvoicesComponent from '@/lib/invoice/components/Invoices' -export default function Invoices() { +export default function MyInvoices() { return ( <IsAuth> <MobileView> diff --git a/src/pages/my/kunjungan-sales.jsx b/src/pages/my/kunjungan-sales.jsx index 63e95837..052991d9 100644 --- a/src/pages/my/kunjungan-sales.jsx +++ b/src/pages/my/kunjungan-sales.jsx @@ -2,7 +2,7 @@ import Seo from '@/core/components/Seo' import BasicLayout from '@/core/components/layouts/BasicLayout' import KunjunganSales from '@/lib/form/components/KunjunganSales' -export default function pembayaran_tempo() { +export default function kunjungan_sales() { return ( <> <Seo title='Kunjungan Sales - Indoteknik.com' /> diff --git a/src/pages/my/kunjungan-service.jsx b/src/pages/my/kunjungan-service.jsx index 80d1ecbc..37de5a0b 100644 --- a/src/pages/my/kunjungan-service.jsx +++ b/src/pages/my/kunjungan-service.jsx @@ -2,8 +2,7 @@ import Seo from '@/core/components/Seo' import BasicLayout from '@/core/components/layouts/BasicLayout' import CreateKunjunganService from '@/lib/form/components/KunjunganService' -export default function pembayaran_tempo() { - +export default function kunjungan_service() { return ( <> <Seo title='Kunjungan Service - Indoteknik.com' /> diff --git a/src/pages/my/media-relations.jsx b/src/pages/my/media-relations.jsx new file mode 100644 index 00000000..a965f39a --- /dev/null +++ b/src/pages/my/media-relations.jsx @@ -0,0 +1,15 @@ +import Seo from '@/core/components/Seo' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import CreateMediaRelations from '@/lib/form/components/MediaRelations' + +export default function media_relations() { + return ( + <> + <Seo title='Media Relations - Indoteknik.com' /> + + <BasicLayout> + <CreateMediaRelations></CreateMediaRelations> + </BasicLayout> + </> + ) +} diff --git a/src/pages/my/menu.jsx b/src/pages/my/menu.jsx index e7ae27fd..c8e1e7e9 100644 --- a/src/pages/my/menu.jsx +++ b/src/pages/my/menu.jsx @@ -40,6 +40,7 @@ export default function Menu() { <MenuHeader>Aktivitas Pembelian</MenuHeader> <div className='divide-y divide-gray_r-6 border-y border-gray_r-6 mt-4'> + <LinkItem href='/my/quotations'>Daftar Quotation</LinkItem> <LinkItem href='/my/transactions'>Daftar Transaksi</LinkItem> <LinkItem href='/my/invoices'>Invoice & Faktur Pajak</LinkItem> <LinkItem href='/my/wishlist'>Wishlist</LinkItem> diff --git a/src/pages/my/pembayaran-tempo.jsx b/src/pages/my/pembayaran-tempo.jsx index 1e4fc80d..8947bdd9 100644 --- a/src/pages/my/pembayaran-tempo.jsx +++ b/src/pages/my/pembayaran-tempo.jsx @@ -2,7 +2,7 @@ import Seo from '@/core/components/Seo' import BasicLayout from '@/core/components/layouts/BasicLayout' import PembayaranTempo from '@/lib/form/components/PembayaranTempo' -export default function pembayaran_tempo() { +export default function pembayaranTempo() { return ( <> <Seo title='Pembayaran Tempo - Indoteknik.com' /> diff --git a/src/pages/my/transaction/[id].jsx b/src/pages/my/quotations/[id].jsx index c3283783..1fbbf34a 100644 --- a/src/pages/my/transaction/[id].jsx +++ b/src/pages/my/quotations/[id].jsx @@ -6,7 +6,7 @@ import IsAuth from '@/lib/auth/components/IsAuth' import TransactionComponent from '@/lib/transaction/components/Transaction' import { useRouter } from 'next/router' -export default function Transaction() { +export default function MyQuotation() { const router = useRouter() return ( diff --git a/src/pages/my/quotations/index.jsx b/src/pages/my/quotations/index.jsx new file mode 100644 index 00000000..bd91d9d0 --- /dev/null +++ b/src/pages/my/quotations/index.jsx @@ -0,0 +1,26 @@ +import AppLayout from '@/core/components/layouts/AppLayout' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import DesktopView from '@/core/components/views/DesktopView' +import MobileView from '@/core/components/views/MobileView' +import IsAuth from '@/lib/auth/components/IsAuth' +import dynamic from 'next/dynamic' + +const TransactionsComponent = dynamic(() => import('@/lib/transaction/components/Transactions')) + +export default function MyQuotations() { + return ( + <IsAuth> + <MobileView> + <AppLayout title='Quotation'> + <TransactionsComponent context='quotation' /> + </AppLayout> + </MobileView> + + <DesktopView> + <BasicLayout> + <TransactionsComponent context='quotation' /> + </BasicLayout> + </DesktopView> + </IsAuth> + ) +} diff --git a/src/pages/my/surat-dukungan.jsx b/src/pages/my/surat-dukungan.jsx index 8c469d02..8058f34d 100644 --- a/src/pages/my/surat-dukungan.jsx +++ b/src/pages/my/surat-dukungan.jsx @@ -3,8 +3,7 @@ import BasicLayout from '@/core/components/layouts/BasicLayout' import CreateKunjunganService from '@/lib/form/components/KunjunganService' import CreateSuratDukungan from '@/lib/form/components/SuratDukungan' -export default function pembayaran_tempo() { - +export default function surat_dukungan() { return ( <> <Seo title='Surat Dukungan - Indoteknik.com' /> diff --git a/src/pages/my/transactions/[id].jsx b/src/pages/my/transactions/[id].jsx new file mode 100644 index 00000000..e36c1ca4 --- /dev/null +++ b/src/pages/my/transactions/[id].jsx @@ -0,0 +1,27 @@ +import AppLayout from '@/core/components/layouts/AppLayout' +import BasicLayout from '@/core/components/layouts/BasicLayout' +import DesktopView from '@/core/components/views/DesktopView' +import MobileView from '@/core/components/views/MobileView' +import IsAuth from '@/lib/auth/components/IsAuth' +import TransactionComponent from '@/lib/transaction/components/Transaction' +import { useRouter } from 'next/router' + +export default function MyTransaction() { + const router = useRouter() + + return ( + <IsAuth> + <MobileView> + <AppLayout title='Transaksi'> + <TransactionComponent id={router.query.id} /> + </AppLayout> + </MobileView> + + <DesktopView> + <BasicLayout> + <TransactionComponent id={router.query.id} /> + </BasicLayout> + </DesktopView> + </IsAuth> + ) +} diff --git a/src/pages/my/transactions.jsx b/src/pages/my/transactions/index.jsx index a8ca78b8..91482a39 100644 --- a/src/pages/my/transactions.jsx +++ b/src/pages/my/transactions/index.jsx @@ -7,7 +7,7 @@ import dynamic from 'next/dynamic' const TransactionsComponent = dynamic(() => import('@/lib/transaction/components/Transactions')) -export default function Transactions() { +export default function MyTransactions() { return ( <IsAuth> <MobileView> diff --git a/src/pages/panduan-belanja.jsx b/src/pages/panduan-belanja.jsx new file mode 100644 index 00000000..27be13da --- /dev/null +++ b/src/pages/panduan-belanja.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function PanduanBelanja() { + return ( + <BasicLayout> + <Seo title='Panduan Belanja - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/panduan-belanja' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/panduan-pick-up-service.jsx b/src/pages/panduan-pick-up-service.jsx new file mode 100644 index 00000000..57f78a4c --- /dev/null +++ b/src/pages/panduan-pick-up-service.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function PanduanPickUpService() { + return ( + <BasicLayout> + <Seo title='Panduan Pick Up Service - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/panduan-pick-up-service' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/panduan-quotation.jsx b/src/pages/panduan-quotation.jsx new file mode 100644 index 00000000..e4ecdd95 --- /dev/null +++ b/src/pages/panduan-quotation.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function PanduanQuotation() { + return ( + <BasicLayout> + <Seo title='Panduan Quotation - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/panduan-quotation' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/pelanggan-kami.jsx b/src/pages/pelanggan-kami.jsx new file mode 100644 index 00000000..48b92339 --- /dev/null +++ b/src/pages/pelanggan-kami.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function PelangganKami() { + return ( + <BasicLayout> + <Seo title='Pelanggan Kami - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/pelanggan-kami' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/pengembalian-barang-dana.jsx b/src/pages/pengembalian-barang-dana.jsx new file mode 100644 index 00000000..57b5c9c5 --- /dev/null +++ b/src/pages/pengembalian-barang-dana.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function PengembalianBarangDana() { + return ( + <BasicLayout> + <Seo title='Pengembalian Barang & Dana - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/pengembalian-barang-dana' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx new file mode 100644 index 00000000..dbc17c06 --- /dev/null +++ b/src/pages/shop/category/[slug].jsx @@ -0,0 +1,37 @@ +import dynamic from 'next/dynamic' +import { getIdFromSlug, getNameFromSlug } from '@/core/utils/slug' +import { useRouter } from 'next/router' +import _ from 'lodash' +import Seo from '@/core/components/Seo' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch')) + +export default function CategoryDetail() { + const router = useRouter() + const { slug = '' } = router.query + + const categoryName = getNameFromSlug(slug) + const categoryId = getIdFromSlug(slug) + const query = { + fq: `manufacture_id_i:${categoryId}` + } + return ( + <BasicLayout> + <Seo + title={`Beli ${categoryName} di Indoteknik`} + description={`Jual ${categoryName} Kirim Jakarta Surabaya Semarang Makassar Manado Denpasar Balikpapan Medan Palembang Lampung Bali Bandung Makassar Manado.`} + additionalMetaTags={[ + { + property: 'keywords', + content: `Jual ${categoryName}, harga ${categoryName}, ${categoryName} murah, toko ${categoryName}, ${categoryName} jakarta, ${categoryName} surabaya` + } + ]} + /> + + {!_.isEmpty(router.query) && ( + <ProductSearch query={query} prefixUrl={`/shop/category/${slug}`} /> + )} + </BasicLayout> + ) +} diff --git a/src/pages/shop/product/[slug].jsx b/src/pages/shop/product/[slug].jsx index 84b848f0..5d706dec 100644 --- a/src/pages/shop/product/[slug].jsx +++ b/src/pages/shop/product/[slug].jsx @@ -1,48 +1,75 @@ import Seo from '@/core/components/Seo' +import LogoSpinner from '@/core/components/elements/Spinner/LogoSpinner' import { getIdFromSlug } from '@/core/utils/slug' import productApi from '@/lib/product/api/productApi' +import PageNotFound from '@/pages/404' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' +import cookie from 'cookie' const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) const Product = dynamic(() => import('@/lib/product/components/Product/Product')) export async function getServerSideProps(context) { const { slug } = context.query - let product = await productApi({ id: getIdFromSlug(slug) }) + const cookies = context.req.headers.cookie + const cookieObj = cookies ? cookie.parse(cookies) : {} + const auth = cookieObj.auth ? JSON.parse(cookieObj.auth) : {} + const authToken = auth?.token || '' + + let product = await productApi({ id: getIdFromSlug(slug), headers: { Token: authToken } }) if (product?.length == 1) { product = product[0] const regexHtmlTags = /(<([^>]+)>)/gi const regexHtmlTagsExceptP = /<\/?(?!p\b)[^>]*>/g - if (product.description.replace(regexHtmlTags, ' ').trim() == '') { - product.description = '' - } - product.description = product.description.replace(regexHtmlTagsExceptP, ' ') - product.description = product.description.trim() + product.description = product.description + .replace(regexHtmlTagsExceptP, ' ') + .replace(regexHtmlTags, ' ') + .trim() + } else { + product = null + } + + return { + props: { product } } - return { props: { product } } } export default function ProductDetail({ product }) { const router = useRouter() + + if (!product) return <PageNotFound /> + return ( <BasicLayout> <Seo - title={product?.name + ' - Indoteknik.com' || ''} + title={product?.name || '' + ' - Indoteknik.com' || ''} description='Temukan pilihan produk B2B Industri & Alat Teknik untuk Perusahaan, UMKM & Pemerintah dengan lengkap, mudah dan transparan.' openGraph={{ url: process.env.NEXT_PUBLIC_SELF_HOST + router.asPath, - image: product.image, + images: [ + { + url: product?.image, + width: 800, + height: 800, + alt: product?.name + } + ], type: 'product' }} additionalMetaTags={[ { name: 'keywords', - content: `${product.name}, Harga ${product.name}, Beli ${product.name}, Spesifikasi ${product.name}` + content: `${product?.name}, Harga ${product?.name}, Beli ${product?.name}, Spesifikasi ${product?.name}` } ]} /> - <Product product={product} /> + {!product && ( + <div className='container mx-auto flex justify-center pt-10'> + <LogoSpinner width={36} height={36} /> + </div> + )} + {product && <Product product={product} />} </BasicLayout> ) } diff --git a/src/pages/shop/quotation/finish.jsx b/src/pages/shop/quotation/finish.jsx index 0e9c5672..3850fef8 100644 --- a/src/pages/shop/quotation/finish.jsx +++ b/src/pages/shop/quotation/finish.jsx @@ -30,7 +30,7 @@ export default function FinishQuotation() { </p> {id && ( <Link - href={`/my/transaction/${id}`} + href={`/my/quotations/${id}`} className='btn-yellow !text-gray_r-12 mt-6 w-full md:w-1/3 md:mx-auto' > Lihat Penawaran diff --git a/src/pages/sitemap/blogs.xml.js b/src/pages/sitemap/blogs.xml.js new file mode 100644 index 00000000..7f41ad0f --- /dev/null +++ b/src/pages/sitemap/blogs.xml.js @@ -0,0 +1,31 @@ +import { createSlug } from '@/core/utils/slug' +import blogsApi from '@/lib/blog/api/blogsApi' +import { create } from 'xmlbuilder' + +export async function getServerSideProps({ res }) { + const baseUrl = process.env.SELF_HOST + '/blog/' + const blogs = await blogsApi({ limit: 0, offset: 0 }) + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + const date = new Date() + blogs.blogs.forEach((blog) => { + const url = sitemap.ele('url') + url.ele('loc', createSlug(baseUrl, blog.title, blog.id)) + url.ele('lastmod', date.toISOString().slice(0, 10)) + url.ele('changefreq', 'weekly') + url.ele('priority', '0.6') + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemap.end()) + res.end() + + return { props: {} } +} + +export default function SitemapProducts() { + return null +} diff --git a/src/pages/sitemap/brands.xml.js b/src/pages/sitemap/brands.xml.js new file mode 100644 index 00000000..c85c40e9 --- /dev/null +++ b/src/pages/sitemap/brands.xml.js @@ -0,0 +1,31 @@ +import { create } from 'xmlbuilder' +import { createSlug } from '@/core/utils/slug' +import odooApi from '@/core/api/odooApi' + +export async function getServerSideProps({ res }) { + const baseUrl = process.env.SELF_HOST + '/shop/brands/' + const brands = await odooApi('GET', `/api/v1/manufacture?limit=0`) + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + const date = new Date() + brands.manufactures.forEach((brand) => { + const url = sitemap.ele('url') + url.ele('loc', createSlug(baseUrl, brand.name, brand.id)) + url.ele('lastmod', date.toISOString().slice(0, 10)) + url.ele('changefreq', 'weekly') + url.ele('priority', '0.6') + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemap.end()) + res.end() + + return { props: {} } +} + +export default function SitemapProducts() { + return null +} diff --git a/src/pages/sitemap/categories.xml.js b/src/pages/sitemap/categories.xml.js new file mode 100644 index 00000000..1c684576 --- /dev/null +++ b/src/pages/sitemap/categories.xml.js @@ -0,0 +1,41 @@ +import { create } from 'xmlbuilder' +import { createSlug } from '@/core/utils/slug' +import odooApi from '@/core/api/odooApi' + +export async function getServerSideProps({ res }) { + const categories = await odooApi('GET', '/api/v1/category/tree') + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + categories.forEach((category) => { + addUrlToSitemap(sitemap, category.name, category.id) + category.childs.forEach((child1Category) => { + addUrlToSitemap(sitemap, child1Category.name, child1Category.id) + child1Category.childs.forEach((child2Category) => { + addUrlToSitemap(sitemap, child2Category.name, child2Category.id) + }) + }) + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemap.end()) + res.end() + + return { props: {} } +} + +function addUrlToSitemap(sitemap, name, id) { + const baseUrl = process.env.SELF_HOST + '/shop/category/' + const date = new Date() + const url = sitemap.ele('url') + url.ele('loc', createSlug(baseUrl, name, id)) + url.ele('lastmod', date.toISOString().slice(0, 10)) + url.ele('changefreq', 'weekly') + url.ele('priority', '0.6') +} + +export default function SitemapProducts() { + return null +} diff --git a/src/pages/sitemap/products.xml.js b/src/pages/sitemap/products.xml.js index e5fc8029..a2067308 100644 --- a/src/pages/sitemap/products.xml.js +++ b/src/pages/sitemap/products.xml.js @@ -9,7 +9,10 @@ export async function getServerSideProps({ res }) { const products = await productSearchApi({ query: _.toQuery(query) }) const pageCount = Math.ceil(products.response.numFound / limit) const pages = Array.from({ length: pageCount }, (_, i) => i + 1) - const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }) + const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) const date = new Date() pages.forEach((page) => { diff --git a/src/pages/sitemap/products/[id].xml.js b/src/pages/sitemap/products/[id].xml.js deleted file mode 100644 index 98c2bf40..00000000 --- a/src/pages/sitemap/products/[id].xml.js +++ /dev/null @@ -1,32 +0,0 @@ -import productSearchApi from '@/lib/product/api/productSearchApi' -import { create } from 'xmlbuilder' -import _ from 'lodash-contrib' -import { createSlug } from '@/core/utils/slug' - -export async function getServerSideProps({ res }) { - const baseUrl = process.env.SELF_HOST + '/shop/product' - const limit = 2500 - const query = { limit } - const products = await productSearchApi({ query: _.toQuery(query) }) - console.log(products) - // const sitemap = create('urlset', { encoding: 'UTF-8' }) - - // const date = new Date() - // pages.forEach((page) => { - // const url = sitemap.ele('url') - // url.ele('loc', createSlug(baseUrl, '', '')) - // url.ele('lastmod', date.toISOString().slice(0, 10)) - // url.ele('changefreq', 'weekly') - // url.ele('priority', '0.8') - // }) - - // res.setHeader('Content-Type', 'text/xml') - // res.write(sitemap.end()) - // res.end() - - return { props: {} } -} - -export default function SitemapProducts() { - return <></> -} diff --git a/src/pages/sitemap/products/[page].js b/src/pages/sitemap/products/[page].js new file mode 100644 index 00000000..2f9c3198 --- /dev/null +++ b/src/pages/sitemap/products/[page].js @@ -0,0 +1,35 @@ +import productSearchApi from '@/lib/product/api/productSearchApi' +import { create } from 'xmlbuilder' +import _ from 'lodash-contrib' +import { createSlug } from '@/core/utils/slug' + +export async function getServerSideProps({ query, res }) { + const baseUrl = process.env.SELF_HOST + '/shop/product/' + const { page } = query + const limit = 2500 + const queries = { limit, page: page.replace('.xml', '') } + const products = await productSearchApi({ query: _.toQuery(queries) }) + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ) + + const date = new Date() + products.response.products.forEach((product) => { + const url = sitemap.ele('url') + url.ele('loc', createSlug(baseUrl, product.name, product.id)) + url.ele('lastmod', date.toISOString().slice(0, 10)) + url.ele('changefreq', 'weekly') + url.ele('priority', '0.8') + }) + + res.setHeader('Content-Type', 'text/xml') + res.write(sitemap.end()) + res.end() + + return { props: {} } +} + +export default function SitemapProducts() { + return null +} diff --git a/src/pages/syarat-ketentuan.jsx b/src/pages/syarat-ketentuan.jsx new file mode 100644 index 00000000..20a3aa63 --- /dev/null +++ b/src/pages/syarat-ketentuan.jsx @@ -0,0 +1,17 @@ +import Seo from '@/core/components/Seo' +import dynamic from 'next/dynamic' + +const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout')) +const PageContent = dynamic(() => import('@/lib/content/components/PageContent')) + +export default function SyaratKetentuan() { + return ( + <BasicLayout> + <Seo title='Syarat & Ketentuan - Indoteknik.com' /> + + <article className='container mx-auto py-6 px-4 sm:px-0'> + <PageContent path='/syarat-ketentuan' /> + </article> + </BasicLayout> + ) +} diff --git a/src/pages/about-us.jsx b/src/pages/tentang-kami.jsx index c7e546df..d7029a50 100644 --- a/src/pages/about-us.jsx +++ b/src/pages/tentang-kami.jsx @@ -2,7 +2,7 @@ import Seo from '@/core/components/Seo' import BasicLayout from '@/core/components/layouts/BasicLayout' import IframeContent from '@/lib/iframe/components/IframeContent' -export default function AboutUs() { +export default function TentangKami() { return ( <BasicLayout> <Seo title='Tentang Indoteknik.com' /> |
