Loading components/marketplace/navigation-tree.tsx +53 −15 Original line number Diff line number Diff line Loading @@ -25,19 +25,43 @@ export default function NavigationTree({ const treeRef = useRef<HTMLDivElement>(null); const [isOpen, setIsOpen] = useState(false); const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined); // const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined); const selectedPath = searchParams?.get('categoryPath') || undefined; useEffect(() => { if (level == 0) { if (level === 0) { setIsOpen(true); } else if (selectedPath && hierarchy?.children?.some((child) => child.path === selectedPath)) { setIsOpen(true); } }, [level]); }, [level, selectedPath, hierarchy?.children]); useEffect(() => { if (level !== 0) return; // Attach listener only at root tree level const handleClickOutside = (event: MouseEvent) => { if (treeRef.current && !treeRef.current.contains(event.target as Node)) { setSelectedPath(undefined); router.replace(pathname || ''); const target = event.target as HTMLElement; // Ignore if click is inside the tree itself if (treeRef.current && treeRef.current.contains(target)) { return; } // Ignore if click is on a spec card, modal, search input, or sorting dropdown if ( target.closest('.group\\/item') || // Spec card target.closest('[role="dialog"]') || // Modal detail view target.closest('input') || // Search box target.closest('button') // Sorting / load buttons ) { return; } // Clear category filter when clicking empty background space const params = new URLSearchParams(searchParams?.toString() || ''); if (params.has('categoryPath')) { params.delete('categoryPath'); const queryString = params.toString(); router.replace(queryString ? `${pathname}?${queryString}` : pathname); } }; Loading @@ -45,18 +69,26 @@ export default function NavigationTree({ return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [treeRef]); }, [level, pathname, searchParams, router]); const onSelected = (path: string | undefined) => { const params = new URLSearchParams(searchParams?.toString() || ''); params.set('searchText', ''); params.set('categoryPath', path || ''); router.replace(pathname + '?' + params.toString()); setSelectedPath(path); if (path) { params.set('categoryPath', path); } else { params.delete('categoryPath'); } // Retains all other search params (like searchText, status, etc.) const queryString = params.toString(); const newUrl = queryString ? `${pathname}?${queryString}` : pathname; router.replace(newUrl); }; const isCurrentPath = (path: string | undefined) => { return selectedPath === path; return Boolean(path && selectedPath === path); }; const levelPaddings = ['pl-0', 'pl-0', 'pl-6', 'pl-10']; Loading @@ -74,13 +106,19 @@ export default function NavigationTree({ <IoMdArrowDropdown className="min-w-4" color="#98A2B3" onClick={() => setIsOpen(!isOpen)} onClick={(e) => { e.stopPropagation(); setIsOpen(!isOpen); }} /> ) : ( <IoMdArrowDropright className="min-w-4" color="#98A2B3" onClick={() => setIsOpen(!isOpen)} onClick={(e) => { e.stopPropagation(); setIsOpen(!isOpen); }} /> ) ) : ( Loading components/marketplace/search-box.tsx +7 −3 Original line number Diff line number Diff line Loading @@ -26,11 +26,15 @@ export function SearchBox({ useEffect(() => { const params = new URLSearchParams(searchParams?.toString() || ''); if (debouncedValue) { params.set('categoryPath', ''); } params.set('searchText', debouncedValue); router.replace(pathname + '?' + params.toString()); } else { params.delete('searchText'); } const queryString = params.toString(); router.replace(queryString ? `${pathname}?${queryString}` : pathname); }, [debouncedValue]); return ( Loading components/marketplace/service-info-component.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ export default function ServiceInfoComponent({ ? new Date(serviceSpecificationDetails.lastUpdate) : new Date(); const formattedDate = new Intl.DateTimeFormat(undefined, { const formattedDate = new Intl.DateTimeFormat('en-US', { day: 'numeric', month: 'short', year: 'numeric', Loading components/marketplace/service-specification-card.tsx +63 −72 Original line number Diff line number Diff line 'use client'; import { addCartItem } from '@/services/cart-api'; import { ServiceSpecification } from '@/services/tmf633'; import { Button, Image, Tooltip } from '@nextui-org/react'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; // import { revalidatePath } from 'next/cache'; import { useSearchParams } from 'next/navigation'; import React from 'react'; import { FaCartPlus } from 'react-icons/fa'; import ServiceInfoComponent from './service-info-component'; import Link from 'next/link'; export function ServiceSpecificationCard({ userId, currentPath, serviceSpecification, }: Readonly<{ userId: string; currentPath: string; serviceSpecification: ServiceSpecification }>) { const searchParams = useSearchParams(); const isOffline = (serviceSpecification as ServiceSpecification & { domainGatewayStatus?: string }) ?.domainGatewayStatus === 'Offline'; const [prefix, ...suffixParts] = (serviceSpecification.name || '').split(':'); const suffix = suffixParts.join(':').trim(); const params = new URLSearchParams(searchParams?.toString() || ''); params.set('specId', serviceSpecification?.id || ''); const targetUrl = `/marketplace?${params.toString()}`; return ( <div className="group/item relative w-full max-w-[384px] justify-center"> <form action={async () => { 'use server'; const searchParams = new URLSearchParams(global.window?.location.search || ''); searchParams.set('specId', serviceSpecification?.id || ''); const newPath = `/marketplace?${searchParams.toString()}`; redirect(newPath); }} <Link href={targetUrl} className="flex w-full flex-col rounded-sm bg-white text-left text-maestro-black shadow-md" > <button className="flex w-full flex-col rounded-sm bg-white text-maestro-black shadow-md"> <div className={`flex h-[163px] w-full flex-col justify-between gap-7 px-[30px] ${ isOffline Loading Loading @@ -59,9 +59,7 @@ export function ServiceSpecificationCard({ className={`flex h-[163px] w-full justify-between gap-3.5 p-[30px] ${isOffline ? 'opacity-[0.85]' : ''}`} > <div className="flex flex-col items-start gap-2"> <h3 className="text-[11px] font-light leading-tight text-maestro-black"> DESCRIPTION </h3> <h3 className="text-[11px] font-light leading-tight text-maestro-black">DESCRIPTION</h3> <div className="text-left text-sm font-light leading-5 text-maestro-grey"> <Tooltip className="max-w-64" Loading @@ -84,24 +82,17 @@ export function ServiceSpecificationCard({ <Image src={serviceSpecification.attachment?.[0]?.url} alt="logo" width={55} /> </div> </div> </button> </form> <form action={async () => { 'use server'; </Link> await addCartItem(userId, serviceSpecification); revalidatePath(currentPath); }} > <Button className={`invisible absolute right-9 top-10 h-8 min-w-8 rounded bg-[#4AA5AF] px-0 ${isOffline ? '' : 'group-hover/item:visible'}`} type="submit" onClick={async (e) => { e.preventDefault(); e.stopPropagation(); await addCartItem(userId, serviceSpecification); }} startContent={<FaCartPlus size="1.125rem" />} ></Button> </form> </div> ); } components/marketplace/service-specification-detail-card.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ export default function ServiceSpecificationDetailCard({ const pathname = url.pathname; const searchParams = url.searchParams; searchParams.delete('specId'); const newPath = pathname + '?' + searchParams.toString(); const newPath = searchParams.toString() ? `${pathname}?${searchParams.toString()}` : pathname; redirect(newPath); }; Loading Loading
components/marketplace/navigation-tree.tsx +53 −15 Original line number Diff line number Diff line Loading @@ -25,19 +25,43 @@ export default function NavigationTree({ const treeRef = useRef<HTMLDivElement>(null); const [isOpen, setIsOpen] = useState(false); const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined); // const [selectedPath, setSelectedPath] = useState<string | undefined>(undefined); const selectedPath = searchParams?.get('categoryPath') || undefined; useEffect(() => { if (level == 0) { if (level === 0) { setIsOpen(true); } else if (selectedPath && hierarchy?.children?.some((child) => child.path === selectedPath)) { setIsOpen(true); } }, [level]); }, [level, selectedPath, hierarchy?.children]); useEffect(() => { if (level !== 0) return; // Attach listener only at root tree level const handleClickOutside = (event: MouseEvent) => { if (treeRef.current && !treeRef.current.contains(event.target as Node)) { setSelectedPath(undefined); router.replace(pathname || ''); const target = event.target as HTMLElement; // Ignore if click is inside the tree itself if (treeRef.current && treeRef.current.contains(target)) { return; } // Ignore if click is on a spec card, modal, search input, or sorting dropdown if ( target.closest('.group\\/item') || // Spec card target.closest('[role="dialog"]') || // Modal detail view target.closest('input') || // Search box target.closest('button') // Sorting / load buttons ) { return; } // Clear category filter when clicking empty background space const params = new URLSearchParams(searchParams?.toString() || ''); if (params.has('categoryPath')) { params.delete('categoryPath'); const queryString = params.toString(); router.replace(queryString ? `${pathname}?${queryString}` : pathname); } }; Loading @@ -45,18 +69,26 @@ export default function NavigationTree({ return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [treeRef]); }, [level, pathname, searchParams, router]); const onSelected = (path: string | undefined) => { const params = new URLSearchParams(searchParams?.toString() || ''); params.set('searchText', ''); params.set('categoryPath', path || ''); router.replace(pathname + '?' + params.toString()); setSelectedPath(path); if (path) { params.set('categoryPath', path); } else { params.delete('categoryPath'); } // Retains all other search params (like searchText, status, etc.) const queryString = params.toString(); const newUrl = queryString ? `${pathname}?${queryString}` : pathname; router.replace(newUrl); }; const isCurrentPath = (path: string | undefined) => { return selectedPath === path; return Boolean(path && selectedPath === path); }; const levelPaddings = ['pl-0', 'pl-0', 'pl-6', 'pl-10']; Loading @@ -74,13 +106,19 @@ export default function NavigationTree({ <IoMdArrowDropdown className="min-w-4" color="#98A2B3" onClick={() => setIsOpen(!isOpen)} onClick={(e) => { e.stopPropagation(); setIsOpen(!isOpen); }} /> ) : ( <IoMdArrowDropright className="min-w-4" color="#98A2B3" onClick={() => setIsOpen(!isOpen)} onClick={(e) => { e.stopPropagation(); setIsOpen(!isOpen); }} /> ) ) : ( Loading
components/marketplace/search-box.tsx +7 −3 Original line number Diff line number Diff line Loading @@ -26,11 +26,15 @@ export function SearchBox({ useEffect(() => { const params = new URLSearchParams(searchParams?.toString() || ''); if (debouncedValue) { params.set('categoryPath', ''); } params.set('searchText', debouncedValue); router.replace(pathname + '?' + params.toString()); } else { params.delete('searchText'); } const queryString = params.toString(); router.replace(queryString ? `${pathname}?${queryString}` : pathname); }, [debouncedValue]); return ( Loading
components/marketplace/service-info-component.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -10,7 +10,7 @@ export default function ServiceInfoComponent({ ? new Date(serviceSpecificationDetails.lastUpdate) : new Date(); const formattedDate = new Intl.DateTimeFormat(undefined, { const formattedDate = new Intl.DateTimeFormat('en-US', { day: 'numeric', month: 'short', year: 'numeric', Loading
components/marketplace/service-specification-card.tsx +63 −72 Original line number Diff line number Diff line 'use client'; import { addCartItem } from '@/services/cart-api'; import { ServiceSpecification } from '@/services/tmf633'; import { Button, Image, Tooltip } from '@nextui-org/react'; import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; // import { revalidatePath } from 'next/cache'; import { useSearchParams } from 'next/navigation'; import React from 'react'; import { FaCartPlus } from 'react-icons/fa'; import ServiceInfoComponent from './service-info-component'; import Link from 'next/link'; export function ServiceSpecificationCard({ userId, currentPath, serviceSpecification, }: Readonly<{ userId: string; currentPath: string; serviceSpecification: ServiceSpecification }>) { const searchParams = useSearchParams(); const isOffline = (serviceSpecification as ServiceSpecification & { domainGatewayStatus?: string }) ?.domainGatewayStatus === 'Offline'; const [prefix, ...suffixParts] = (serviceSpecification.name || '').split(':'); const suffix = suffixParts.join(':').trim(); const params = new URLSearchParams(searchParams?.toString() || ''); params.set('specId', serviceSpecification?.id || ''); const targetUrl = `/marketplace?${params.toString()}`; return ( <div className="group/item relative w-full max-w-[384px] justify-center"> <form action={async () => { 'use server'; const searchParams = new URLSearchParams(global.window?.location.search || ''); searchParams.set('specId', serviceSpecification?.id || ''); const newPath = `/marketplace?${searchParams.toString()}`; redirect(newPath); }} <Link href={targetUrl} className="flex w-full flex-col rounded-sm bg-white text-left text-maestro-black shadow-md" > <button className="flex w-full flex-col rounded-sm bg-white text-maestro-black shadow-md"> <div className={`flex h-[163px] w-full flex-col justify-between gap-7 px-[30px] ${ isOffline Loading Loading @@ -59,9 +59,7 @@ export function ServiceSpecificationCard({ className={`flex h-[163px] w-full justify-between gap-3.5 p-[30px] ${isOffline ? 'opacity-[0.85]' : ''}`} > <div className="flex flex-col items-start gap-2"> <h3 className="text-[11px] font-light leading-tight text-maestro-black"> DESCRIPTION </h3> <h3 className="text-[11px] font-light leading-tight text-maestro-black">DESCRIPTION</h3> <div className="text-left text-sm font-light leading-5 text-maestro-grey"> <Tooltip className="max-w-64" Loading @@ -84,24 +82,17 @@ export function ServiceSpecificationCard({ <Image src={serviceSpecification.attachment?.[0]?.url} alt="logo" width={55} /> </div> </div> </button> </form> <form action={async () => { 'use server'; </Link> await addCartItem(userId, serviceSpecification); revalidatePath(currentPath); }} > <Button className={`invisible absolute right-9 top-10 h-8 min-w-8 rounded bg-[#4AA5AF] px-0 ${isOffline ? '' : 'group-hover/item:visible'}`} type="submit" onClick={async (e) => { e.preventDefault(); e.stopPropagation(); await addCartItem(userId, serviceSpecification); }} startContent={<FaCartPlus size="1.125rem" />} ></Button> </form> </div> ); }
components/marketplace/service-specification-detail-card.tsx +1 −1 Original line number Diff line number Diff line Loading @@ -28,7 +28,7 @@ export default function ServiceSpecificationDetailCard({ const pathname = url.pathname; const searchParams = url.searchParams; searchParams.delete('specId'); const newPath = pathname + '?' + searchParams.toString(); const newPath = searchParams.toString() ? `${pathname}?${searchParams.toString()}` : pathname; redirect(newPath); }; Loading