diff options
| author | Rafi Zadanly <zadanlyr@gmail.com> | 2023-03-15 14:52:16 +0700 |
|---|---|---|
| committer | Rafi Zadanly <zadanlyr@gmail.com> | 2023-03-15 14:52:16 +0700 |
| commit | 4e634a9d3556e94c7ce0729ef9f15b73495b2e28 (patch) | |
| tree | 1f04e5bb360ea4801dfc47a58354ff21b93b34a1 | |
| parent | 3b19ddcd0051f094b4659a35107646d678c2fd0c (diff) | |
product detail desktop
| -rw-r--r-- | src/core/components/elements/Navbar/NavbarDesktop.jsx | 72 | ||||
| -rw-r--r-- | src/core/components/layouts/AnimationLayout.jsx | 32 | ||||
| -rw-r--r-- | src/lib/home/components/Skeleton/PopularProductSkeleton.jsx | 23 | ||||
| -rw-r--r-- | src/lib/product/api/productSimilarApi.js | 2 | ||||
| -rw-r--r-- | src/lib/product/components/ProductDesktop.jsx | 123 | ||||
| -rw-r--r-- | src/lib/product/components/ProductMobile.jsx | 18 | ||||
| -rw-r--r-- | src/pages/_app.jsx | 2 | ||||
| -rw-r--r-- | src/pages/api/shop/search.js | 17 | ||||
| -rw-r--r-- | src/styles/globals.css | 17 |
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 { |
