summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-30 09:28:19 +0700
committerFIN-IT_AndriFP <andrifebriyadiputra@gmail.com>2026-01-30 09:28:19 +0700
commit0df8e2dc0771853eac3e3db2a3716042681c876e (patch)
tree38d8794ad57de6c470f31376f68658d3e21cc26f
parent4bb444b880d7677ba60f163c78440c2357fb16a4 (diff)
parentec7ab4c654fc5b29b277d42ad84986f4c1220134 (diff)
fix merge
-rw-r--r--src-migrate/modules/product-detail/components/Information.tsx75
-rw-r--r--src-migrate/modules/product-detail/components/ProductDetail.tsx21
-rw-r--r--src/lib/category/components/Breadcrumb.jsx66
-rw-r--r--src/pages/shop/category/[slug].jsx30
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>
);