Loading actions/geocode.ts 0 → 100644 +31 −0 Original line number Diff line number Diff line 'use server'; import { geocode } from 'opencage-api-client'; export interface GeoCoordinates { lat: number; lng: number; } export async function geocodeAddress(address: string): Promise<GeoCoordinates> { const geocodeApiKey = process.env.GEOCODE_API_KEY; if (!geocodeApiKey) { throw new Error('GEOCODE_API_KEY is not set on the server'); } const geocodeResponse = await geocode({ key: geocodeApiKey, q: address, language: 'en', limit: 1, no_annotations: 1, }); if (!geocodeResponse || geocodeResponse.results.length === 0) { throw new Error('No geocoding results found for the provided address.'); } const { lat, lng } = geocodeResponse.results[0].geometry; return { lat, lng }; } app/marketplace/page.tsx +6 −7 Original line number Diff line number Diff line Loading @@ -14,12 +14,12 @@ import { hasAccessToView } from '@/actions/roles-viewAccess'; export const dynamic = 'force-dynamic'; function getCurrentPath() { if (typeof window !== 'undefined') { return window.location.pathname; } return ''; } // function getCurrentPath() { // if (typeof window !== 'undefined') { // return window.location.pathname; // } // return ''; // } export default async function Marketplace({ searchParams, Loading Loading @@ -78,7 +78,6 @@ export default async function Marketplace({ {data.serviceSpecifications?.data?.map((serviceSpecification) => ( <ServiceSpecificationCard userId={data.session?.user?.name || ''} currentPath={getCurrentPath()} key={serviceSpecification.id} serviceSpecification={serviceSpecification} /> 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 Loading
actions/geocode.ts 0 → 100644 +31 −0 Original line number Diff line number Diff line 'use server'; import { geocode } from 'opencage-api-client'; export interface GeoCoordinates { lat: number; lng: number; } export async function geocodeAddress(address: string): Promise<GeoCoordinates> { const geocodeApiKey = process.env.GEOCODE_API_KEY; if (!geocodeApiKey) { throw new Error('GEOCODE_API_KEY is not set on the server'); } const geocodeResponse = await geocode({ key: geocodeApiKey, q: address, language: 'en', limit: 1, no_annotations: 1, }); if (!geocodeResponse || geocodeResponse.results.length === 0) { throw new Error('No geocoding results found for the provided address.'); } const { lat, lng } = geocodeResponse.results[0].geometry; return { lat, lng }; }
app/marketplace/page.tsx +6 −7 Original line number Diff line number Diff line Loading @@ -14,12 +14,12 @@ import { hasAccessToView } from '@/actions/roles-viewAccess'; export const dynamic = 'force-dynamic'; function getCurrentPath() { if (typeof window !== 'undefined') { return window.location.pathname; } return ''; } // function getCurrentPath() { // if (typeof window !== 'undefined') { // return window.location.pathname; // } // return ''; // } export default async function Marketplace({ searchParams, Loading Loading @@ -78,7 +78,6 @@ export default async function Marketplace({ {data.serviceSpecifications?.data?.map((serviceSpecification) => ( <ServiceSpecificationCard userId={data.session?.user?.name || ''} currentPath={getCurrentPath()} key={serviceSpecification.id} serviceSpecification={serviceSpecification} /> 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