summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRafi Zadanly <zadanlyr@gmail.com>2023-03-15 14:52:16 +0700
committerRafi Zadanly <zadanlyr@gmail.com>2023-03-15 14:52:16 +0700
commit4e634a9d3556e94c7ce0729ef9f15b73495b2e28 (patch)
tree1f04e5bb360ea4801dfc47a58354ff21b93b34a1
parent3b19ddcd0051f094b4659a35107646d678c2fd0c (diff)
product detail desktop
-rw-r--r--src/core/components/elements/Navbar/NavbarDesktop.jsx72
-rw-r--r--src/core/components/layouts/AnimationLayout.jsx32
-rw-r--r--src/lib/home/components/Skeleton/PopularProductSkeleton.jsx23
-rw-r--r--src/lib/product/api/productSimilarApi.js2
-rw-r--r--src/lib/product/components/ProductDesktop.jsx123
-rw-r--r--src/lib/product/components/ProductMobile.jsx18
-rw-r--r--src/pages/_app.jsx2
-rw-r--r--src/pages/api/shop/search.js17
-rw-r--r--src/styles/globals.css17
9 files changed, 203 insertions, 103 deletions
diff --git a/src/core/components/elements/Navbar/NavbarDesktop.jsx b/src/core/components/elements/Navbar/NavbarDesktop.jsx
index 2f7a6e23..fa620eb2 100644
--- a/src/core/components/elements/Navbar/NavbarDesktop.jsx
+++ b/src/core/components/elements/Navbar/NavbarDesktop.jsx
@@ -21,29 +21,17 @@ const NavbarDesktop = () => {
<DesktopView>
<div className='py-3 bg-yellow_r-10/60'>
<div className='container mx-auto flex justify-between'>
- <Link
- href='/'
- className='!text-gray_r-12'
- >
+ <Link href='/' className='!text-gray_r-12'>
Tentang Indoteknik.com
</Link>
<div className='flex gap-x-6'>
- <Link
- href='/'
- className='!text-gray_r-12'
- >
+ <Link href='/' className='!text-gray_r-12'>
Pembayaran Tempo
</Link>
- <Link
- href='/'
- className='!text-gray_r-12'
- >
+ <Link href='/' className='!text-gray_r-12'>
F.A.Q
</Link>
- <Link
- href='/'
- className='!text-gray_r-12'
- >
+ <Link href='/' className='!text-gray_r-12'>
Fitur Layanan
</Link>
</div>
@@ -53,50 +41,28 @@ const NavbarDesktop = () => {
<nav className='py-6 sticky top-0 z-50 bg-white border-b border-gray_r-6'>
<div className='container mx-auto flex gap-x-6'>
<Link href='/'>
- <Image
- src={IndoteknikLogo}
- alt='Indoteknik Logo'
- width={180}
- height={60}
- />
+ <Image src={IndoteknikLogo} alt='Indoteknik Logo' width={180} height={60} />
</Link>
<Search />
<div className='flex gap-x-4'>
- <Link
- href='/my/transactions'
- className='flex items-center gap-x-2 !text-gray_r-12/80'
- >
+ <Link href='/my/transactions' className='flex items-center gap-x-2 !text-gray_r-12/80'>
<DocumentCheckIcon className='w-7' />
Pending
<br />
Quotation
</Link>
- <Link
- href='/shop/cart'
- className='flex items-center gap-x-2 !text-gray_r-12/80'
- >
+ <Link href='/shop/cart' className='flex items-center gap-x-2 !text-gray_r-12/80'>
<ShoppingCartIcon className='w-7' />
Keranjang
<br />
Belanja
</Link>
- <Link
- href='/my/wishlist'
- className='flex items-center gap-x-2 !text-gray_r-12/80'
- >
+ <Link href='/my/wishlist' className='flex items-center gap-x-2 !text-gray_r-12/80'>
<HeartIcon className='w-7' />
Wishlist
</Link>
- <a
- href='https://wa.me/628'
- className='flex items-center gap-x-1 !text-gray_r-12/80'
- >
- <Image
- src='/images/socials/Whatsapp-2.png'
- alt='Whatsapp'
- width={48}
- height={48}
- />
+ <a href='https://wa.me/628' className='flex items-center gap-x-1 !text-gray_r-12/80'>
+ <Image src='/images/socials/Whatsapp-2.png' alt='Whatsapp' width={48} height={48} />
<div>
<div className='font-semibold'>Order Via WA</div>
0812 8080 622 (Chat)
@@ -108,19 +74,19 @@ const NavbarDesktop = () => {
<div className='container mx-auto mt-6'>
<div className='flex bg-gray_r-2'>
- <div className='w-3/12 p-4 font-semibold border border-gray_r-6 rounded-tl-xl flex items-center relative'>
+ <button
+ type='button'
+ onClick={() => setIsOpenCategory((isOpen) => !isOpen)}
+ onBlur={() => setIsOpenCategory(false)}
+ className='w-3/12 p-4 font-semibold border border-gray_r-6 rounded-tl-xl flex items-center relative'
+ >
<div>Kategori Produk</div>
- <button
- type='button'
- className='ml-auto pl-3'
- onClick={() => setIsOpenCategory((category) => !category)}
- >
- <ChevronDownIcon className={`w-6 ${isOpenCategory ? 'rotate-180' : ''}`} />
- </button>
+ <ChevronDownIcon className={`ml-auto w-6 ${isOpenCategory ? 'rotate-180' : ''}`} />
+
<div className={`category-mega-box-wrapper ${isOpenCategory ? 'show' : ''}`}>
<Category />
</div>
- </div>
+ </button>
<div className='w-6/12 flex gap-x-1 px-1 bg-gray_r-1'>
<Link
href='/'
diff --git a/src/core/components/layouts/AnimationLayout.jsx b/src/core/components/layouts/AnimationLayout.jsx
index c4dee606..7acf21dc 100644
--- a/src/core/components/layouts/AnimationLayout.jsx
+++ b/src/core/components/layouts/AnimationLayout.jsx
@@ -1,20 +1,32 @@
+import useDevice from '@/core/hooks/useDevice'
import { motion } from 'framer-motion'
const AnimationLayout = ({ children, ...props }) => {
- const transition = {
- ease: 'easeIn',
- duration: 0.2
+ const { isMobile } = useDevice()
+
+ const initialConfig = {
+ opacity: 0,
+ x: 0,
+ y: 0
+ }
+
+ const animateConfig = {
+ opacity: 1,
+ x: 0,
+ y: 0,
+ transition: { duration: 0.2, delay: 0.2, ease: 'easeInOut' }
+ }
+
+ const exitConfig = {
+ opacity: 0,
+ x: isMobile ? 30 : 0,
+ y: 0,
+ transition: { duration: 0.2, ease: 'easeInOut' }
}
return (
children && (
- <motion.main
- initial={{ opacity: 0, x: 0, y: 0 }}
- animate={{ opacity: 1, x: 0, y: 0 }}
- exit={{ opacity: 0, x: 30, y: 0 }}
- transition={transition}
- {...props}
- >
+ <motion.main initial={initialConfig} animate={animateConfig} exit={exitConfig} {...props}>
{children}
</motion.main>
)
diff --git a/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
index 18a1b3d3..29fda966 100644
--- a/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
+++ b/src/lib/home/components/Skeleton/PopularProductSkeleton.jsx
@@ -1,10 +1,25 @@
import ProductCardSkeleton from '@/core/components/elements/Skeleton/ProductCardSkeleton'
+import DesktopView from '@/core/components/views/DesktopView'
+import MobileView from '@/core/components/views/MobileView'
const PopularProductSkeleton = () => (
- <div className='grid grid-cols-2 gap-x-3'>
- <ProductCardSkeleton />
- <ProductCardSkeleton />
- </div>
+ <>
+ <MobileView>
+ <div className='grid grid-cols-2 gap-x-3'>
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+ </MobileView>
+ <DesktopView>
+ <div className='grid grid-cols-5 gap-x-3'>
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ <ProductCardSkeleton />
+ </div>
+ </DesktopView>
+ </>
)
export default PopularProductSkeleton
diff --git a/src/lib/product/api/productSimilarApi.js b/src/lib/product/api/productSimilarApi.js
index 8fd17ab9..93c7f22c 100644
--- a/src/lib/product/api/productSimilarApi.js
+++ b/src/lib/product/api/productSimilarApi.js
@@ -2,7 +2,7 @@ import axios from 'axios'
const productSimilarApi = async ({ query }) => {
const dataProductSimilar = await axios(
- `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular`
+ `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?q=${query}&page=1&orderBy=popular&operation=OR`
)
return dataProductSimilar.data.response
}
diff --git a/src/lib/product/components/ProductDesktop.jsx b/src/lib/product/components/ProductDesktop.jsx
index dc733eac..6eba2aed 100644
--- a/src/lib/product/components/ProductDesktop.jsx
+++ b/src/lib/product/components/ProductDesktop.jsx
@@ -3,14 +3,17 @@ import Link from '@/core/components/elements/Link/Link'
import DesktopView from '@/core/components/views/DesktopView'
import currencyFormat from '@/core/utils/currencyFormat'
import { HeartIcon } from '@heroicons/react/24/outline'
-import { useEffect, useState } from 'react'
+import { Fragment, useEffect, useRef, useState } from 'react'
+import LazyLoad from 'react-lazy-load'
+import ProductSimilar from './ProductSimilar'
const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
const [variantQuantity, setVariantQuantity] = useState(null)
+ const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
useEffect(() => {
const mapVariantQuantity = product.variants.reduce((acc, cur) => {
- acc[cur.id] = 1
+ acc[cur.id] = '1'
return acc
}, {})
setVariantQuantity(mapVariantQuantity)
@@ -20,6 +23,23 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
setVariantQuantity((variantQuantity) => ({ ...variantQuantity, [variantId]: quantity }))
}
+ const variantSectionRef = useRef(null)
+ const goToVariantSection = () => {
+ if (variantSectionRef.current) {
+ const position = variantSectionRef.current.getBoundingClientRect()
+ window.scrollTo({
+ top: position.top - 120 + window.pageYOffset,
+ behavior: 'smooth'
+ })
+ }
+ }
+
+ const productSimilarQuery = [
+ product?.name.replace(/[()/"&]/g, ''),
+ `fq=-product_id:${product.id}`,
+ `fq=-manufacture_id:${product.manufacture?.id || 0}`
+ ].join('&')
+
return (
<DesktopView>
<div className='container mx-auto mt-10'>
@@ -32,18 +52,18 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
/>
</div>
<div className='w-6/12 px-4'>
- <h1 className='text-title-md leading-8 font-medium'>{product?.name}</h1>
- <div className='mt-6 flex flex-col gap-y-4'>
- <div className='flex'>
- <div className='w-1/4 text-gray_r-12/60'>Nomor SKU</div>
+ <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'>
- <div className='w-1/4 text-gray_r-12/60'>Part Number</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'>
- <div className='w-1/4 text-gray_r-12/60'>Manufacture</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='/'>{product.manufacture?.name}</Link>
@@ -52,8 +72,8 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
)}
</div>
</div>
- <div className='flex'>
- <div className='w-1/4 text-gray_r-12/60'>Berat Barang</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 && (
@@ -65,6 +85,7 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
</div>
</div>
</div>
+
<div className='w-3/12'>
{product.variants.length > 1 && product.lowestPrice.priceDiscount > 0 && (
<div className='text-gray_r-12/80'>Harga mulai dari: </div>
@@ -79,7 +100,7 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
</div>
</div>
)}
- <h3 className='text-red_r-11 font-semibold mt-1 text-title-lg'>
+ <h3 className='text-red_r-11 font-semibold mt-1 text-title-md'>
{product?.lowestPrice.priceDiscount > 0 ? (
currencyFormat(product?.lowestPrice.priceDiscount)
) : (
@@ -91,7 +112,11 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
</span>
)}
</h3>
- <button type='button' className='btn-solid-red w-full mt-6'>
+ <button
+ type='button'
+ onClick={goToVariantSection}
+ className='btn-solid-red w-full mt-6'
+ >
Lihat Varian
</button>
@@ -108,13 +133,13 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
</div>
</div>
- <div className='mt-12'>
+ <div className='mt-12' ref={variantSectionRef}>
<div className='text-h-lg font-semibold mb-6'>Varian Produk</div>
<div className='table-specification'>
<table>
<thead>
<tr>
- <th>No. SKU</th>
+ <th>Part Number</th>
<th>Harga</th>
<th>Jumlah</th>
<th>Action</th>
@@ -137,8 +162,8 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
<td>
<input
type='number'
- className='form-input w-16 text-center'
- value={variantQuantity[variant.id]}
+ className='form-input w-16 py-2 text-center bg-gray_r-1'
+ value={variantQuantity?.[variant.id]}
onChange={(e) => changeQuantity(variant.id, e.target.value)}
/>
</td>
@@ -152,9 +177,71 @@ const ProductDesktop = ({ product, wishlist, toggleWishlist }) => {
</table>
</div>
</div>
+
+ <div className='mt-12'>
+ <div className='text-h-lg font-semibold'>Informasi Produk</div>
+ <div className='my-5 h-0.5 bg-gray_r-6' />
+ <div className='flex gap-x-4 mb-5'>
+ {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 rounded'>
+ <TabContent active={informationTab == 'description'}>
+ <div className='w-3/4 leading-7 product__description'>
+ <span
+ dangerouslySetInnerHTML={{
+ __html:
+ product.description != ''
+ ? product.description
+ : 'Belum ada deskripsi produk.'
+ }}
+ />
+ </div>
+ </TabContent>
+
+ <TabContent active={informationTab == 'information'}>Belum ada informasi.</TabContent>
+ </div>
+ </div>
+
+ <div className='mt-12'>
+ <div className='text-h-lg font-semibold mb-6'>Kamu Mungkin Juga Suka</div>
+ <LazyLoad>
+ <ProductSimilar query={productSimilarQuery} />
+ </LazyLoad>
+ </div>
</div>
</DesktopView>
)
}
+const informationTabOptions = [
+ { value: 'description', label: 'Deskripsi' },
+ { value: 'information', label: 'Info Penting' }
+]
+
+const TabButton = ({ children, active, ...props }) => {
+ const activeClassName = active
+ ? 'text-red_r-11 underline underline-offset-4'
+ : 'text-gray_r-12/80'
+ return (
+ <button {...props} type='button' className={`font-medium ${activeClassName}`}>
+ {children}
+ </button>
+ )
+}
+
+const TabContent = ({ children, active, className, ...props }) => (
+ <div {...props} className={`${active ? 'block' : 'hidden'} ${className}`}>
+ {children}
+ </div>
+)
+
export default ProductDesktop
diff --git a/src/lib/product/components/ProductMobile.jsx b/src/lib/product/components/ProductMobile.jsx
index 790fcd57..07da876e 100644
--- a/src/lib/product/components/ProductMobile.jsx
+++ b/src/lib/product/components/ProductMobile.jsx
@@ -17,7 +17,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
const [quantity, setQuantity] = useState('1')
const [selectedVariant, setSelectedVariant] = useState(null)
- const [informationTab, setInformationTab] = useState(null)
+ const [informationTab, setInformationTab] = useState(informationTabOptions[0].value)
const [activeVariant, setActiveVariant] = useState({
id: product.id,
@@ -58,12 +58,6 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
}
}, [selectedVariant, product])
- useEffect(() => {
- if (!informationTab) {
- setInformationTab(informationTabOptions[0].value)
- }
- }, [informationTab])
-
const validAction = () => {
let isValid = true
if (!selectedVariant) {
@@ -91,6 +85,12 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
router.push(`/shop/checkout?productId=${activeVariant.id}&quantity=${quantity}`)
}
+ const productSimilarQuery = [
+ product?.name.replace(/[()/"&]/g, ''),
+ `fq=-product_id:${product.id}`,
+ `fq=-manufacture_id:${product.manufacture?.id || 0}`
+ ].join('&')
+
return (
<MobileView>
<Image
@@ -242,7 +242,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
<div className='p-4'>
<h2 className='font-semibold mb-4'>Kamu Mungkin Juga Suka</h2>
<LazyLoad>
- <ProductSimilar query={product?.name.split(' ').slice(1, 3).join(' ')} />
+ <ProductSimilar query={productSimilarQuery} />
</LazyLoad>
</div>
</MobileView>
@@ -252,7 +252,7 @@ const ProductMobile = ({ product, wishlist, toggleWishlist }) => {
const informationTabOptions = [
{ value: 'specification', label: 'Spesifikasi' },
{ value: 'description', label: 'Deskripsi' },
- { value: 'important', label: 'Info Penting' }
+ { value: 'information', label: 'Info Penting' }
]
const TabButton = ({ children, active, ...props }) => {
diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx
index e32efc19..6fe07136 100644
--- a/src/pages/_app.jsx
+++ b/src/pages/_app.jsx
@@ -25,7 +25,7 @@ function MyApp({ Component, pageProps }) {
/>
<QueryClientProvider client={queryClient}>
<AnimatePresence
- mode='wait'
+ mode='sync'
initial={false}
onExitComplete={() => window.scrollTo(0, 0)}
>
diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js
index c1e00d16..3d6b3f26 100644
--- a/src/pages/api/shop/search.js
+++ b/src/pages/api/shop/search.js
@@ -46,7 +46,9 @@ export default async function handler(req, res) {
category = '',
priceFrom = 0,
priceTo = 0,
- orderBy = ''
+ orderBy = '',
+ operation = 'AND',
+ fq = ''
} = req.query
let paramOrderBy = ''
@@ -68,13 +70,13 @@ export default async function handler(req, res) {
let limit = 30
let offset = (page - 1) * limit
let parameter = [
- `facet.query=${q}`,
+ 'facet.field=brand_str',
+ 'facet.field=category_name_str',
'facet=true',
'indent=true',
- 'q.op=AND',
+ `facet.query=${q}`,
+ `q.op=${operation}`,
`q=${q}`,
- 'facet.field=brand_str',
- 'facet.field=category_name_str',
`start=${offset}`,
`rows=${limit}`,
`sort=product_rating DESC ${paramOrderBy}`,
@@ -84,6 +86,11 @@ export default async function handler(req, res) {
if (brand) parameter.push(`fq=brand:${brand}`)
if (category) parameter.push(`fq=category_name:${category}`)
+ // Single fq in url params
+ if (typeof fq === 'string') parameter.push(`fq=${fq}`)
+ // Multi fq in url params
+ if (Array.isArray(fq)) parameter = parameter.concat(fq.map((val) => `fq=${val}`))
+
let result = await axios(process.env.SOLR_HOST + '/solr/products/select?' + parameter.join('&'))
try {
result.data.response.products = productResponseMap(result.data.response.docs)
diff --git a/src/styles/globals.css b/src/styles/globals.css
index b9dfbe38..63fa729e 100644
--- a/src/styles/globals.css
+++ b/src/styles/globals.css
@@ -211,6 +211,18 @@ button {
mb-1
block;
}
+
+ .product__description {
+ @apply text-gray_r-12/90;
+ }
+
+ .product__description br {
+ @apply block my-1;
+ }
+
+ .product__description b {
+ @apply font-semibold;
+ }
}
@layer utilities {
@@ -406,7 +418,7 @@ button {
.table-specification th,
.table-specification td {
- @apply py-4 px-2 text-center;
+ @apply p-4 text-center;
}
.table-specification > table > tbody > tr {
@@ -424,7 +436,8 @@ button {
transition-all
ease-in
duration-200
- pointer-events-none;
+ pointer-events-none
+ text-left;
}
.category-mega-box-wrapper.show {