diff --git a/vnda/loaders/productListingPage.ts b/vnda/loaders/productListingPage.ts index 508d1f45c..da7f1370f 100644 --- a/vnda/loaders/productListingPage.ts +++ b/vnda/loaders/productListingPage.ts @@ -13,7 +13,6 @@ import { getSEOFromTag, toFilters, toProduct, - typeTagExtractor, } from "../utils/transform.ts"; export const VNDA_SORT_OPTIONS: SortOption[] = [ @@ -87,6 +86,42 @@ const handleOperator = ( [`${key}_operator`]: filterOperators?.[key] ?? defaultValue ?? "and", }); +const fetchTag = ( + api: AppContext["api"], + name: string, +): Promise => + api["GET /api/v2/tags/:name"]({ name }, STALE) + .then((res) => res.json() as Promise) + .catch((): undefined => undefined); + +interface TypeTag { + key: string; + value: string; + isProperty: boolean; +} + +const parseTypeTagsFromUrl = (url: URL): { typeTags: TypeTag[]; cleanUrl: URL } => { + const TYPE_TAG_PATTERN = /^type_tags\[(.+)\]\[\]$/; + + const typeTags = [...url.searchParams.entries()] + .filter(([key]) => TYPE_TAG_PATTERN.test(key)) + .map(([key, value]) => { + const keyName = key.match(TYPE_TAG_PATTERN)?.[1] ?? ""; + return { + key, + value, + isProperty: /^property\d+$/.test(keyName), + }; + }); + + const cleanUrl = new URL(url.href); + [...cleanUrl.searchParams.keys()] + .filter((k) => k.startsWith("type_tags")) + .forEach((k) => cleanUrl.searchParams.delete(k)); + + return { typeTags, cleanUrl }; +}; + /** * @title VNDA Integration * @description Product Listing Page loader @@ -108,44 +143,40 @@ const searchLoader = async ( const isSearchPage = ctx.searchPagePath ? ctx.searchPagePath === url.pathname : url.pathname === "/busca" || url.pathname === "/s"; + const qQueryString = url.searchParams.get("q"); - const term = props.term || props.slug || qQueryString || - undefined; + const term = props.term || props.slug || qQueryString || undefined; const priceFilterRegex = /de-(\d+)-a-(\d+)/; const filterMatch = url.href.match(priceFilterRegex) ?? []; - const categoryTagName = (props.term || url.pathname.slice(1) || "").split( - "/", - ); + const categoryTagName = (props.term || url.pathname.slice(1) || "").split("/"); const properties1 = url.searchParams.getAll("type_tags[property1][]"); const properties2 = url.searchParams.getAll("type_tags[property2][]"); const properties3 = url.searchParams.getAll("type_tags[property3][]"); - const categoryTagNames = Array.from(url.searchParams.values()); + const uniquePathNames = [ + ...new Set( + categoryTagName.filter((item): item is string => typeof item === "string"), + ), + ]; - const tags = await Promise.all([ - ...categoryTagNames, - ...categoryTagName.filter((item): item is string => - typeof item === "string" + const tagByName = new Map( + await Promise.all( + uniquePathNames.map( + async (name) => [name, await fetchTag(api, name)] as const, + ), ), - ].map((name) => - api["GET /api/v2/tags/:name"]({ name }, STALE) - .then((res) => res.json()) - .catch(() => undefined) - )); - - const categories = tags - .slice(-categoryTagName.length) + ); + + const categories = categoryTagName + .map((name) => tagByName.get(name)) .filter((tag): tag is Tag => typeof tag !== "undefined" && typeof tag.name !== "undefined" ); - const filteredTags = tags - .filter((tag): tag is Tag => typeof tag !== "undefined"); - - const { cleanUrl, typeTags } = typeTagExtractor(url, filteredTags); + const { typeTags, cleanUrl } = parseTypeTagsFromUrl(url); const initialTags = props.tags && props.tags?.length > 0 ? props.tags @@ -165,7 +196,7 @@ const searchLoader = async ( const tag = categories.at(-1); const [response, seo = []] = await Promise.all([ - await api["GET /api/v2/products/search"]({ + api["GET /api/v2/products/search"]({ term: term ?? preference, sort, page, @@ -211,19 +242,15 @@ const searchLoader = async ( ) as ProductSearchResult["pagination"] | null; const search = await response.json(); - const { results: searchResults = [] } = search; - const validProducts = searchResults.filter(({ variants }) => { - return variants.length !== 0; - }); + const validProducts = searchResults.filter(({ variants }) => + variants.length !== 0 + ); - const products = validProducts.map((product) => { - return toProduct(product, null, { - url, - priceCurrency: "BRL", - }); - }); + const products = validProducts.map((product) => + toProduct(product, null, { url, priceCurrency: "BRL" }) + ); const nextPage = new URLSearchParams(url.searchParams); const previousPage = new URLSearchParams(url.searchParams); @@ -247,11 +274,7 @@ const searchLoader = async ( "@type": "ProductListingPage", seo: getSEOFromTag(categories, url, seo.at(-1), hasTypeTags, isSearchPage), breadcrumb: isSearchPage - ? { - "@type": "BreadcrumbList", - itemListElement: [], - numberOfItems: 0, - } + ? { "@type": "BreadcrumbList", itemListElement: [], numberOfItems: 0 } : getBreadcrumbList(categories, url), filters: toFilters(search.aggregations, typeTags, cleanUrl), products, @@ -270,12 +293,9 @@ export const cache = "stale-while-revalidate"; export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => { const url = new URL(props.pageHref || req.url); const qQueryString = url.searchParams.get("q"); - const term = props.term || qQueryString || - undefined; + const term = props.term || qQueryString || undefined; - if (term) { - return null; - } + if (term) return null; const typeTags = [...url.searchParams.entries()] .filter(([key]) => key.includes("type_tags")) @@ -299,19 +319,15 @@ export const cacheKey = (props: Props, req: Request, _ctx: AppContext) => { ["sort", url.searchParams.get("sort") ?? props.sort ?? ""], ["type_tags", typeTags], ["tags", props?.tags?.join("\\") ?? ""], - [ - "price", - filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : "", - ], + ["price", filterMatch ? `min:${filterMatch[1]}_max:${filterMatch[2]}` : ""], ["filterByTags", props.filterByTags ? "true" : "false"], ["filterOperator", filterOperators.join("\\")], ["page", (url.searchParams.get("page") ?? 1).toString()], ]); params.sort(); - url.search = params.toString(); return url.href; }; -export default searchLoader; +export default searchLoader; \ No newline at end of file