diff options
| -rw-r--r-- | src-migrate/modules/product-detail/components/Information.tsx | 75 | ||||
| -rw-r--r-- | src-migrate/modules/product-detail/components/ProductDetail.tsx | 21 | ||||
| -rw-r--r-- | src/lib/category/components/Breadcrumb.jsx | 66 | ||||
| -rw-r--r-- | src/pages/shop/category/[slug].jsx | 30 |
4 files changed, 127 insertions, 65 deletions
diff --git a/src-migrate/modules/product-detail/components/Information.tsx b/src-migrate/modules/product-detail/components/Information.tsx index cc16fb6d..900225a5 100644 --- a/src-migrate/modules/product-detail/components/Information.tsx +++ b/src-migrate/modules/product-detail/components/Information.tsx @@ -38,15 +38,16 @@ type Props = { const Information = ({ product }: Props) => { const { selectedVariant, setSelectedVariant, setSla, sla } = useProductDetail(); - const [inputValue, setInputValue] = useState<string | null>( - selectedVariant?.code + ' - ' + selectedVariant?.attributes[0] - ); + const [inputValue, setInputValue] = useState<string>(''); const [disableFilter, setDisableFilter] = useState<boolean>(false); const inputRef = useRef<HTMLInputElement>(null); + // source of truth + // const variantOptions = product.variants; const [variantOptions, setVariantOptions] = useState<any[]>( product?.variants ); + const variantId = selectedVariant?.id; const { slaVariant, isLoading } = useVariant({ variantId }); @@ -83,25 +84,32 @@ const Information = ({ product }: Props) => { }, [product]); useEffect(() => { - if (selectedVariant) { - setInputValue( - selectedVariant?.code + - (selectedVariant?.attributes[0] - ? ' - ' + selectedVariant?.attributes[0] - : '') - ); - } + if (!selectedVariant) return; + + setInputValue( + selectedVariant.code + + (selectedVariant.attributes?.[0] + ? ` - ${selectedVariant.attributes[0]}` + : '') + ); }, [selectedVariant]); + /* ====================== + * Sync SLA + * ====================== */ useEffect(() => { if (isLoading) { setSla(null); + return; } if (slaVariant) { setSla(slaVariant); } }, [slaVariant, isLoading, setSla]); + /* ====================== + * Handlers + * ====================== */ const handleOnChange = (vals: any) => { setDisableFilter(true); let code = vals.replace(/\s-\s.*$/, '').trim(); @@ -132,30 +140,32 @@ const Information = ({ product }: Props) => { return ( <div className={style['wrapper']}> - <div className='realtive mb-5'> + {/* ===== Variant Selector ===== */} + <div className='relative mb-5'> <label className='form-label mb-2 text-lg text-red-600'> Pilih Variant * :{' '} <span className='text-gray_r-9 text-sm'> - {product?.variants?.length} Variants - </span>{' '} + {product.variants.length} Variants + </span> </label> + <AutoComplete disableFilter={disableFilter} openOnFocus className='form-input' - onChange={(vals) => handleOnChange(vals)} + onChange={handleOnChange} > <InputGroup> <AutoCompleteInput ref={inputRef} - value={inputValue as string} - onChange={(e) => handleOnKeyUp(e)} + value={inputValue} + onChange={handleOnKeyUp} onFocus={() => setDisableFilter(true)} /> <InputRightElement className='mr-4'> <ChevronDownIcon - className='h-6 w-6 text-gray-500' - onClick={() => inputRef?.current?.focus()} + className='h-6 w-6 text-gray-500 cursor-pointer' + onClick={() => inputRef.current?.focus()} /> </InputRightElement> </InputGroup> @@ -167,41 +177,32 @@ const Information = ({ product }: Props) => { }) .map((option, cid) => ( <AutoCompleteItem - key={`option-${cid}`} - value={ - option.code + - (option?.attributes[0] ? ' - ' + option?.attributes[0] : '') - } + key={option.id} + value={String(option.id)} _selected={ option.id === selectedVariant?.id ? { bg: 'gray.300' } : undefined } - textTransform='capitalize' > - <div - key={cid} - className='flex gap-x-2 w-full justify-between px-3 items-center p-2' - > + <div className='flex gap-x-2 w-full justify-between px-3 items-center p-2'> <div className='text-small'> - {option.code + - (option?.attributes[0] - ? ' - ' + option?.attributes[0] - : '')} + {option.code} + {option.attributes?.[0] ? ` - ${option.attributes[0]}` : ''} </div> <div className={option?.price?.discount_percentage ? 'flex gap-x-4 items-center justify-between' : ''}> {option?.price?.discount_percentage > 0 && ( <> <div className='badge-solid-red text-xs'> - {Math.floor(option?.price?.discount_percentage)}% + {Math.floor(option.price.discount_percentage)}% </div> <div className='min-w-16 sm:min-w-24 text-gray_r-11 line-through text-[11px] sm:text-caption-2'> - {currencyFormat(option?.price?.price)} + {currencyFormat(option.price.price)} </div> </> )} <div className='min-w-20 sm:min-w-28 text-danger-500 font-semibold'> - {currencyFormat(option?.price?.price_discount)} + {currencyFormat(option.price.price_discount)} </div> </div> </div> @@ -285,7 +286,7 @@ const Information = ({ product }: Props) => { <div className={style['row']} style={rowStyle}> <div className={style['label']} style={{ color: '#6b7280' }}>Berat Barang</div> <div className={style['value']}> - {selectedVariant?.weight > 0 ? `${selectedVariant?.weight} Kg` : '-'} + {selectedVariant?.weight > 0 ? `${selectedVariant.weight} Kg` : '-'} </div> </div> diff --git a/src-migrate/modules/product-detail/components/ProductDetail.tsx b/src-migrate/modules/product-detail/components/ProductDetail.tsx index 03089afa..b6db3f69 100644 --- a/src-migrate/modules/product-detail/components/ProductDetail.tsx +++ b/src-migrate/modules/product-detail/components/ProductDetail.tsx @@ -156,9 +156,19 @@ const ProductDetail = ({ product }: Props) => { setAskAdminUrl(createdAskUrl); }, [router.asPath, product.manufacture.name, product.name, setAskAdminUrl]); - // ========================================================================= + // useEffect(() => { + // if (!product?.variants?.length) return; + + // setIsApproval(auth?.feature?.soApproval); + + // setSelectedVariant((prev: any) => { + // if (prev) return prev; + + // return product.variants[0]; + // }); + // }, [product?.id]); + // 1. LOGIC INISIALISASI VARIANT - // ========================================================================= useEffect(() => { if (typeof auth === 'object') { setIsApproval(auth?.feature?.soApproval); @@ -175,9 +185,8 @@ const ProductDetail = ({ product }: Props) => { }, [product, auth]); - // ========================================================================= + // 2. LOGIC FETCH DATA - // ========================================================================= useEffect(() => { const fetchMagentoData = async () => { const allVariantIds = product.variants.map(v => v.id); @@ -247,9 +256,7 @@ const ProductDetail = ({ product }: Props) => { }, [product.id]); - // ========================================================================= // HELPER 1: GROUPING DATA BY LABEL - // ========================================================================= const processMatrixData = (rawMatrix: any[]) => { const groups: any = {}; const result: any[] = []; @@ -283,9 +290,7 @@ const ProductDetail = ({ product }: Props) => { }; - // ========================================================================= // HELPER 2: RENDER SPEC VALUE - // ========================================================================= const renderSpecValue = (val: any) => { if (!val) return '-'; const strVal = String(val).trim(); diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index 50557c3e..8579bb14 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,17 +11,21 @@ import React from 'react'; import { useQuery } from 'react-query'; import useDevice from '@/core/hooks/useDevice'; -const Breadcrumb = ({ categoryId }) => { +const Breadcrumb = ({ categoryId, shortDesc }) => { const breadcrumbs = useQuery( ['category-breadcrumbs', categoryId], async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`) ); + const { isDesktop, isMobile } = useDevice(); const items = breadcrumbs.data ?? []; const lastIdx = items.length - 1; + /* ========================= + DESKTOP + ========================= */ if (isDesktop) { return ( <div className='container mx-auto py-4 md:py-6'> @@ -79,16 +83,34 @@ const Breadcrumb = ({ categoryId }) => { })} </ChakraBreadcrumb> </Skeleton> + {shortDesc && ( + <div + className=' + w-full mt-2 + text-sm text-neutral-600 + leading-7 + text-justify + break-words + [hyphens:auto] + max-w-none + ' + > + {shortDesc} + </div> + )} </div> ); } + /* ========================= + MOBILE + ========================= */ if (isMobile) { - const items = breadcrumbs.data ?? []; const n = items.length; - const lastCat = n >= 1 ? items[n - 1] : null; // terakhir (current) - const secondLast = n >= 2 ? items[n - 2] : null; // sebelum current - const beforeSecond = n >= 3 ? items[n - 3] : null; // sebelum secondLast + const lastCat = n >= 1 ? items[n - 1] : null; + const secondLast = n >= 2 ? items[n - 2] : null; + const beforeSecond = n >= 3 ? items[n - 3] : null; + const hiddenText = n >= 3 ? items @@ -101,15 +123,15 @@ const Breadcrumb = ({ categoryId }) => { <div className='container mx-auto py-2 mt-2'> <Skeleton isLoaded={!breadcrumbs.isLoading} className='w-full'> <ChakraBreadcrumb - separator={<span className='mx-1'>/</span>} // lebih rapat + separator={<span className='mx-1'>/</span>} spacing='4px' sx={{ '& ol': { display: 'flex', alignItems: 'center', - overflow: 'hidden', // untuk ellipsis - whiteSpace: 'nowrap', // untuk ellipsis - gap: '0', // no extra gap + overflow: 'hidden', + whiteSpace: 'nowrap', + gap: '0', }, '& li': { display: 'inline-flex', alignItems: 'center' }, '& li:not(:last-of-type)': { @@ -117,7 +139,7 @@ const Breadcrumb = ({ categoryId }) => { whiteSpace: 'nowrap', }, '& li:last-of-type': { - flex: '0 1 auto', // jangan ambil full space biar gak keliatan “space kosong” + flex: '0 1 auto', minWidth: 0, }, }} @@ -130,7 +152,6 @@ const Breadcrumb = ({ categoryId }) => { </BreadcrumbLink> </BreadcrumbItem> - {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */} {beforeSecond && ( <BreadcrumbItem> <BreadcrumbLink @@ -141,7 +162,6 @@ const Breadcrumb = ({ categoryId }) => { beforeSecond.id )} title={hiddenText} - aria-label={`Kembali ke ${beforeSecond.name}`} className='!text-danger-500' > .. @@ -149,7 +169,6 @@ const Breadcrumb = ({ categoryId }) => { </BreadcrumbItem> )} - {/* secondLast sebagai link (kalau ada) */} {secondLast && ( <BreadcrumbItem> <BreadcrumbLink @@ -166,12 +185,11 @@ const Breadcrumb = ({ categoryId }) => { </BreadcrumbItem> )} - {/* lastCat (current) dengan truncate & lebar dibatasi */} {lastCat && ( <BreadcrumbItem isCurrentPage> <span className='inline-block truncate align-bottom' - style={{ maxWidth: '60vw' }} // batasi lebar supaya gak “makan” baris & keliatan space kosong + style={{ maxWidth: '60vw' }} title={lastCat.name} > {lastCat.name} @@ -180,9 +198,27 @@ const Breadcrumb = ({ categoryId }) => { )} </ChakraBreadcrumb> </Skeleton> + + {shortDesc && ( + <div + className=' + w-full mt-2 + text-sm text-neutral-600 + leading-7 + text-justify + break-words + [hyphens:auto] + max-w-none + ' + > + {shortDesc} + </div> + )} </div> ); } + + return null; }; export default Breadcrumb; diff --git a/src/pages/shop/category/[slug].jsx b/src/pages/shop/category/[slug].jsx index 11840d47..e515e3f4 100644 --- a/src/pages/shop/category/[slug].jsx +++ b/src/pages/shop/category/[slug].jsx @@ -16,13 +16,14 @@ const ProductSearch = dynamic(() => ); const CategorySection = dynamic(() => import('@/lib/product/components/CategorySection') -) +); export default function CategoryDetail() { const router = useRouter(); const { slug = '', page = 1 } = router.query; - const [dataCategories, setDataCategories] = useState([]) + const [dataCategories, setDataCategories] = useState([]); + const [shortDesc, setShortDesc] = useState(''); const categoryName = getNameFromSlug(slug); const categoryId = getIdFromSlug(slug); const q = router?.query.q || null; @@ -33,6 +34,22 @@ export default function CategoryDetail() { if (q) { query.q = q; } + useEffect(() => { + if (!router.isReady) return; + if (!categoryId) return; + + const loadShortDesc = async () => { + const res = await odooApi( + 'GET', + `/api/v1/category/${categoryId}/short-desc` + ); + + const desc = res?.shortDesc || ''; + setShortDesc(desc); + }; + + loadShortDesc(); + }, [router.isReady, categoryId]); return ( <BasicLayout> @@ -47,11 +64,14 @@ export default function CategoryDetail() { ]} /> - <Breadcrumb categoryId={categoryId} /> - + <Breadcrumb categoryId={categoryId} shortDesc={shortDesc} /> {!_.isEmpty(router.query) && ( - <ProductSearch query={query} categories ={categoryId} prefixUrl={`/shop/category/${slug}`} /> + <ProductSearch + query={query} + categories={categoryId} + prefixUrl={`/shop/category/${slug}`} + /> )} </BasicLayout> ); |
