From a97333f4b0d6a4097319ce2350933518dda11c34 Mon Sep 17 00:00:00 2001 From: karolinakuzniewicz Date: Wed, 30 Oct 2024 12:27:33 +0100 Subject: [PATCH] feat: Adjust storefront for fashion products --- apps/storefront/messages/en-GB.json | 15 +- .../(main)/_components/navigation.tsx | 21 +- .../(main)/_components/products-grid.tsx | 2 +- .../(main)/search/_filters/color-swatch.tsx | 73 ++++++ .../(main)/search/_filters/filter-text.tsx | 52 ++++ .../search/_filters/filters-container.tsx | 74 ++++-- .../(main)/search/_listing/products-list.tsx | 4 +- .../src/app/[locale]/(main)/search/actions.ts | 6 +- .../src/app/[locale]/(main)/search/page.tsx | 3 +- .../src/components/header/search-form.tsx | 2 +- .../src/components/search-product-card.tsx | 7 +- apps/storefront/src/envs/client.ts | 11 + apps/storefront/src/services/search.ts | 76 +++++- packages/infrastructure/package.json | 2 + .../src/private/algolia/search/helpers.ts | 37 +++ .../search/infrastructure/get-facets-infra.ts | 71 +++++ .../get-sort-by-options-infra.ts | 26 ++ .../search/infrastructure/search-infra.ts | 99 +++++++ .../src/private/algolia/search/providers.ts | 22 ++ .../src/private/algolia/search/serializers.ts | 27 ++ .../src/private/algolia/search/types.ts | 94 +++++++ pnpm-lock.yaml | 246 +++++++++++++++++- turbo.json | 2 + 23 files changed, 921 insertions(+), 51 deletions(-) create mode 100644 apps/storefront/src/app/[locale]/(main)/search/_filters/color-swatch.tsx create mode 100644 apps/storefront/src/app/[locale]/(main)/search/_filters/filter-text.tsx create mode 100644 packages/infrastructure/src/private/algolia/search/helpers.ts create mode 100644 packages/infrastructure/src/private/algolia/search/infrastructure/get-facets-infra.ts create mode 100644 packages/infrastructure/src/private/algolia/search/infrastructure/get-sort-by-options-infra.ts create mode 100644 packages/infrastructure/src/private/algolia/search/infrastructure/search-infra.ts create mode 100644 packages/infrastructure/src/private/algolia/search/providers.ts create mode 100644 packages/infrastructure/src/private/algolia/search/serializers.ts create mode 100644 packages/infrastructure/src/private/algolia/search/types.ts diff --git a/apps/storefront/messages/en-GB.json b/apps/storefront/messages/en-GB.json index f8883b9..c5afe50 100644 --- a/apps/storefront/messages/en-GB.json +++ b/apps/storefront/messages/en-GB.json @@ -17,7 +17,9 @@ "artists": "Artists", "in-stock": "In stock", "is-exclusive": "Is exclusive", - "is-digital": "Is digital" + "is-digital": "Is digital", + "size": "Size", + "color": "Color" }, "search": { "go-to-product": "Go to product {name}", @@ -344,5 +346,16 @@ "faq": "FAQ", "privacy-policy": "Privacy Policy", "terms-of-use": "Terms of Use" + }, + "colors": { + "yellow": "Yellow", + "black": "Black", + "white": "White", + "beige": "Beige", + "grey": "Grey", + "khaki": "Khaki", + "pink": "Pink", + "red": "Red", + "green": "Green" } } diff --git a/apps/storefront/src/app/[locale]/(main)/_components/navigation.tsx b/apps/storefront/src/app/[locale]/(main)/_components/navigation.tsx index 8fc1344..bc68159 100644 --- a/apps/storefront/src/app/[locale]/(main)/_components/navigation.tsx +++ b/apps/storefront/src/app/[locale]/(main)/_components/navigation.tsx @@ -10,14 +10,12 @@ import { NavigationMenuTrigger, } from "@nimara/ui/components/navigation-menu"; -import { Link, useRouter } from "@/i18n/routing"; +import { Link } from "@/i18n/routing"; import { generateLinkUrl } from "@/lib/helpers"; import { paths } from "@/lib/paths"; import type { Maybe } from "@/lib/types"; export const Navigation = ({ menu }: { menu: Maybe }) => { - const router = useRouter(); - if (!menu || menu?.items?.length === 0) { return null; } @@ -27,21 +25,8 @@ export const Navigation = ({ menu }: { menu: Maybe }) => { {menu.items.map((item) => ( - - item.category?.slug - ? router.replace( - paths.search.asPath({ - query: { - category: item.category.slug, - }, - }), - ) - : undefined - } - > - {item.name} + + {item?.url && {item.name}} {item.children?.length ? ( diff --git a/apps/storefront/src/app/[locale]/(main)/_components/products-grid.tsx b/apps/storefront/src/app/[locale]/(main)/_components/products-grid.tsx index c1779de..be5fe95 100644 --- a/apps/storefront/src/app/[locale]/(main)/_components/products-grid.tsx +++ b/apps/storefront/src/app/[locale]/(main)/_components/products-grid.tsx @@ -73,7 +73,7 @@ export const ProductsGrid = async ({ const { results: products } = await searchService.search( { - productIds: gridProductsIds.length ? [...gridProductsIds] : [], + productIds: gridProductsIds?.length ? [...gridProductsIds] : [], limit: 7, }, searchContext, diff --git a/apps/storefront/src/app/[locale]/(main)/search/_filters/color-swatch.tsx b/apps/storefront/src/app/[locale]/(main)/search/_filters/color-swatch.tsx new file mode 100644 index 0000000..6f90880 --- /dev/null +++ b/apps/storefront/src/app/[locale]/(main)/search/_filters/color-swatch.tsx @@ -0,0 +1,73 @@ +import { getTranslations } from "next-intl/server"; + +import type { Facet } from "@nimara/infrastructure/use-cases/search/types"; +import { Toggle } from "@nimara/ui/components/toggle"; + +import { cn } from "@/lib/utils"; +import type { TranslationMessage } from "@/types"; + +import type { ColorValue } from "./filters-container"; + +const colors: Record = { + yellow: "bg-[#fffa4B]", + black: "bg-black", + white: "bg-white", + beige: "bg-[#dbc1a3]", + grey: "bg-[#808080]", + khaki: "bg-[#c3b091]", + pink: "bg-[#ff9ac6]", + red: "bg-[#e50101]", + green: "bg-[#339f2b]", +}; + +export const ColorSwatch = async ({ + facet: { choices, name, slug, messageKey }, + searchParams, +}: { + facet: Facet; + searchParams: Record; +}) => { + const t = await getTranslations(); + const label = name ?? t(messageKey as TranslationMessage); + const defaultValue = searchParams[slug]?.split(".") ?? []; + + return ( +
+ {label && ( +

{label}

+ )} +
+ {choices?.map((choice) => ( +
+ + +
+
+ ); +}; diff --git a/apps/storefront/src/app/[locale]/(main)/search/_filters/filter-text.tsx b/apps/storefront/src/app/[locale]/(main)/search/_filters/filter-text.tsx new file mode 100644 index 0000000..032d067 --- /dev/null +++ b/apps/storefront/src/app/[locale]/(main)/search/_filters/filter-text.tsx @@ -0,0 +1,52 @@ +import { getTranslations } from "next-intl/server"; + +import type { Facet } from "@nimara/infrastructure/use-cases/search/types"; +import { Toggle } from "@nimara/ui/components/toggle"; + +import { type TranslationMessage } from "@/types"; + +export const FilterText = async ({ + facet: { choices, name, slug, messageKey }, + searchParams, +}: { + facet: Facet; + searchParams: Record; +}) => { + const t = await getTranslations(); + const defaultValue = searchParams[slug]?.split(".") ?? []; + const label = name ?? t(messageKey as TranslationMessage); + + return ( +
+ {label && ( +

{label}

+ )} +
+ {choices?.map((choice) => ( +
+ + + + +
+ ))} +
+
+ ); +}; diff --git a/apps/storefront/src/app/[locale]/(main)/search/_filters/filters-container.tsx b/apps/storefront/src/app/[locale]/(main)/search/_filters/filters-container.tsx index 69539b3..a016e50 100644 --- a/apps/storefront/src/app/[locale]/(main)/search/_filters/filters-container.tsx +++ b/apps/storefront/src/app/[locale]/(main)/search/_filters/filters-container.tsx @@ -1,7 +1,7 @@ import { Filter } from "lucide-react"; import { getTranslations } from "next-intl/server"; -import { type SortByOption } from "@nimara/domain/objects/Search"; +import type { SortByOption } from "@nimara/domain/objects/Search"; import type { Facet } from "@nimara/infrastructure/use-cases/search/types"; import { Button } from "@nimara/ui/components/button"; import { Label } from "@nimara/ui/components/label"; @@ -22,8 +22,10 @@ import { DEFAULT_SORT_BY } from "@/config"; import { type TranslationMessage } from "@/types"; import { handleFiltersFormSubmit } from "../actions"; +import { ColorSwatch } from "./color-swatch"; import { FilterBoolean } from "./filter-boolean"; import { FilterDropdown } from "./filter-dropdown"; +import { FilterText } from "./filter-text"; type Props = { facets: Facet[]; @@ -36,8 +38,23 @@ const renderFilterComponent = ( searchParams: Record, ) => { // TODO: Extend this function for other, more adequate Filter components - switch (facet.type) { + switch (facet?.type) { + case "PLAIN_TEXT": + return ( + + ); case "SWATCH": + return ( + + ); case "MULTISELECT": case "DROPDOWN": return ( @@ -58,12 +75,37 @@ const renderFilterComponent = ( } }; +const colors = [ + "yellow", + "black", + "white", + "beige", + "grey", + "khaki", + "pink", + "red", + "green", +] as const; + +export type ColorValue = (typeof colors)[number]; + export const FiltersContainer = async ({ facets, searchParams, sortByOptions, }: Props) => { const t = await getTranslations(); + const genderFacet = facets.filter((facet) => facet.slug === "gender")[0]; + const sizeFacet = facets.filter((facet) => facet.slug === "size")[0]; + const colorFacet = facets + .filter((facet) => facet.slug === "color") + .map((facet) => ({ + ...facet, + choices: colors.map((color) => ({ + label: t(`colors.${color}`), + value: color as string, + })), + }))[0]; const updateFiltersWithSearchParams = handleFiltersFormSubmit.bind( null, @@ -86,14 +128,17 @@ export const FiltersContainer = async ({
- {t("filters.filters")} + + {t("filters.filters")} + -
+
- {facets - .filter(({ type }) => type !== "BOOLEAN") - .map((facet) => renderFilterComponent(facet, searchParams))} + {renderFilterComponent(genderFacet, searchParams)}
- -
-

- {t("filters.options")} -

-
- {facets - .filter(({ type }) => type === "BOOLEAN") - .map((facet) => - renderFilterComponent(facet, searchParams), - )} -
+
+ {renderFilterComponent(sizeFacet, searchParams)} +
+
+ {renderFilterComponent(colorFacet, searchParams)}
diff --git a/apps/storefront/src/app/[locale]/(main)/search/_listing/products-list.tsx b/apps/storefront/src/app/[locale]/(main)/search/_listing/products-list.tsx index f099392..7e0f46f 100644 --- a/apps/storefront/src/app/[locale]/(main)/search/_listing/products-list.tsx +++ b/apps/storefront/src/app/[locale]/(main)/search/_listing/products-list.tsx @@ -9,8 +9,8 @@ type Props = { export const ProductsList = ({ products }: Props) => { return (
- {products.map((product) => ( - + {products.map((product, index) => ( + ))}
); diff --git a/apps/storefront/src/app/[locale]/(main)/search/actions.ts b/apps/storefront/src/app/[locale]/(main)/search/actions.ts index 90050ec..c766cec 100644 --- a/apps/storefront/src/app/[locale]/(main)/search/actions.ts +++ b/apps/storefront/src/app/[locale]/(main)/search/actions.ts @@ -15,7 +15,11 @@ export const handleFiltersFormSubmit = async ( const params = new URLSearchParams(); formData.forEach((value, key) => { - if (value && typeof value === "string" && !formClear) { + if (key.startsWith("group")) { + const [k, v] = key.replace("group", "").split("-"); + + params.set(k, params.getAll(k).concat(v).join(".")); + } else if (value && typeof value === "string" && !formClear) { params.set(key, value); } }); diff --git a/apps/storefront/src/app/[locale]/(main)/search/page.tsx b/apps/storefront/src/app/[locale]/(main)/search/page.tsx index 51510cb..be53bf1 100644 --- a/apps/storefront/src/app/[locale]/(main)/search/page.tsx +++ b/apps/storefront/src/app/[locale]/(main)/search/page.tsx @@ -66,6 +66,7 @@ export default async function Page({ searchParams }: PageProps) { limit, ...rest } = searchParams; + const { results, pageInfo } = await searchService.search( { query, @@ -94,7 +95,7 @@ export default async function Page({ searchParams }: PageProps) { if (searchParams.category) { return ( searchParams.category[0].toUpperCase() + searchParams.category.slice(1) - ); + ).replaceAll("-", " & "); } return null; diff --git a/apps/storefront/src/components/header/search-form.tsx b/apps/storefront/src/components/header/search-form.tsx index e9f10ac..bda31c8 100644 --- a/apps/storefront/src/components/header/search-form.tsx +++ b/apps/storefront/src/components/header/search-form.tsx @@ -39,7 +39,7 @@ type SearchState = { }; const minLetters = 3; -const maxSearchSuggestions = 15; +const maxSearchSuggestions = 10; const keyboardCodes = { ArrowDown: "ArrowDown", ArrowUp: "ArrowUp", diff --git a/apps/storefront/src/components/search-product-card.tsx b/apps/storefront/src/components/search-product-card.tsx index 00852d1..e94de6e 100644 --- a/apps/storefront/src/components/search-product-card.tsx +++ b/apps/storefront/src/components/search-product-card.tsx @@ -26,8 +26,8 @@ export const ProductPrice = ({ children }: PropsWithChildren) => { }; export const ProductThumbnail = ({ alt, ...props }: ImageProps) => ( -
- {alt} +
+ {alt}
); @@ -61,7 +61,8 @@ export const SearchProductCard = ({ src={thumbnail?.url ?? productPlaceholder} width={width ?? 256} sizes={ - sizes ?? "(max-width: 720px) 100vw, (max-width: 1024px) 50vw, 33vw" + sizes ?? + "(max-width: 720px) 100vw, (max-width: 1024px) 50vw, (max-width: 1294px) 33vw, 25vw" } />
diff --git a/apps/storefront/src/envs/client.ts b/apps/storefront/src/envs/client.ts index 4cbda2e..d29cd33 100644 --- a/apps/storefront/src/envs/client.ts +++ b/apps/storefront/src/envs/client.ts @@ -11,6 +11,11 @@ const schema = z.object({ NEXT_PUBLIC_DEFAULT_PAGE_TITLE: z.string().default(DEFAULT_PAGE_TITLE), NEXT_PUBLIC_DEFAULT_EMAIL: z.string().email().default("contact@mirumee.com"), + // Algolia envs - defaulted for now to avoid errors + NEXT_PUBLIC_ALGOLIA_APP_ID: z.string().default(""), + NEXT_PUBLIC_ALGOLIA_API_KEY: z.string().default(""), + NEXT_PUBLIC_ALGOLIA_INDEX_PREFIX: z.string().default("DEV"), + STRIPE_PUBLIC_KEY: z.string(), ENVIRONMENT: z .enum(["LOCAL", "DEVELOPMENT", "PRODUCTION", "STAGING"]) @@ -26,6 +31,12 @@ export const clientEnvs = schema.parse({ NEXT_PUBLIC_DEFAULT_PAGE_TITLE: process.env.NEXT_PUBLIC_DEFAULT_PAGE_TITLE, NEXT_PUBLIC_DEFAULT_EMAIL: process.env.NEXT_PUBLIC_DEFAULT_EMAIL, + // Algolia envs - defaulted for now to avoid errors + NEXT_PUBLIC_ALGOLIA_APP_ID: process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, + NEXT_PUBLIC_ALGOLIA_API_KEY: process.env.NEXT_PUBLIC_ALGOLIA_API_KEY, + NEXT_PUBLIC_ALGOLIA_INDEX_PREFIX: + process.env.NEXT_PUBLIC_ALGOLIA_INDEX_PREFIX, + STRIPE_PUBLIC_KEY: process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY, ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT, diff --git a/apps/storefront/src/services/search.ts b/apps/storefront/src/services/search.ts index ec55364..4caf8ed 100644 --- a/apps/storefront/src/services/search.ts +++ b/apps/storefront/src/services/search.ts @@ -1,8 +1,82 @@ +import { algoliaSearchService } from "@nimara/infrastructure/private/algolia/search/providers"; +import type { AvailableFacets } from "@nimara/infrastructure/private/algolia/search/types"; import { saleorSearchService } from "@nimara/infrastructure/public/saleor/search/providers"; import type { SearchService } from "@nimara/infrastructure/use-cases/search/types"; import { clientEnvs } from "@/envs/client"; +const facets = { + "attributes.Gender": { + slug: "gender", + type: "PLAIN_TEXT", + }, + "attributes.Size": { + messageKey: "filters.size", + slug: "size", + type: "PLAIN_TEXT", + }, + "attributes.Color": { + messageKey: "filters.color", + slug: "color", + type: "SWATCH", + }, +} satisfies AvailableFacets; + +export const searchServiceAlgolia = algoliaSearchService({ + credentials: { + apiKey: clientEnvs.NEXT_PUBLIC_ALGOLIA_API_KEY, + appId: clientEnvs.NEXT_PUBLIC_ALGOLIA_APP_ID, + }, + settings: { + indices: [ + { + availableFacets: facets, + channel: "channel-uk", + indexName: `channel-uk.GBP.products`, + virtualReplicas: [ + { + indexName: `channel-uk.GBP.products.name_asc`, + messageKey: "search.name-asc", + queryParamValue: "alpha-asc", + }, + { + indexName: `channel-uk.GBP.products.grossPrice_asc`, + messageKey: "search.price-asc", + queryParamValue: "price-asc", + }, + { + indexName: `channel-uk.GBP.products.grossPrice_desc`, + messageKey: "search.price-desc", + queryParamValue: "price-desc", + }, + ], + }, + { + availableFacets: facets, + channel: "channel-us", + indexName: `channel-us.USD.products`, + virtualReplicas: [ + { + indexName: `channel-us.USD.products.name_asc`, + messageKey: "search.name-asc", + queryParamValue: "alpha-asc", + }, + { + indexName: `channel-us.USD.products.grossPrice_asc`, + messageKey: "search.price-asc", + queryParamValue: "price-asc", + }, + { + indexName: `channel-us.USD.products.grossPrice_desc`, + messageKey: "search.price-desc", + queryParamValue: "price-desc", + }, + ], + }, + ], + }, +}); + export const searchServiceSaleor = saleorSearchService({ apiURL: clientEnvs.NEXT_PUBLIC_SALEOR_API_URL, settings: { @@ -35,4 +109,4 @@ export const searchServiceSaleor = saleorSearchService({ }, }); -export const searchService: SearchService = searchServiceSaleor; +export const searchService: SearchService = searchServiceAlgolia; diff --git a/packages/infrastructure/package.json b/packages/infrastructure/package.json index 50bcf4f..aedf0d1 100644 --- a/packages/infrastructure/package.json +++ b/packages/infrastructure/package.json @@ -18,6 +18,8 @@ "@types/react": "18.3.3", "@types/react-dom": "18.3.0", "@types/stripe": "8.0.417", + "algoliasearch": "4.22.1", + "instantsearch.js": "4.64.0", "libphonenumber-js": "1.11.3", "prettier": "3.3.3", "stripe": "15.10.0" diff --git a/packages/infrastructure/src/private/algolia/search/helpers.ts b/packages/infrastructure/src/private/algolia/search/helpers.ts new file mode 100644 index 0000000..4f46c3a --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/helpers.ts @@ -0,0 +1,37 @@ +import { invariant } from "graphql/jsutils/invariant"; + +import { loggingService } from "@nimara/infrastructure/logging/service"; + +import { type IndicesSettings } from "./types"; + +export const getIndexName = ( + indicesSettings: IndicesSettings, + channel: string, + sortByParam?: string, +): string => { + // This can be extended beyond just comparing a channel, e.g. by comparing language/currency/entity etc. + const channelIndex = indicesSettings.find( + (index) => index.channel === channel, + ); + + invariant(channelIndex, `Missing Algolia index for channel: ${channel}`); + + if (!sortByParam) { + return channelIndex.indexName; + } + + const replica = channelIndex.virtualReplicas.find( + (replica) => replica.queryParamValue === sortByParam, + ); + + if (!replica) { + loggingService.error( + "Missing virtual replica of given index for given sortBy parameter. Returning a main index.", + { indexName: channelIndex.indexName, sortByParam }, + ); + + return channelIndex.indexName; + } + + return replica.indexName; +}; diff --git a/packages/infrastructure/src/private/algolia/search/infrastructure/get-facets-infra.ts b/packages/infrastructure/src/private/algolia/search/infrastructure/get-facets-infra.ts new file mode 100644 index 0000000..f36c41d --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/infrastructure/get-facets-infra.ts @@ -0,0 +1,71 @@ +import algoliasearch from "algoliasearch"; + +import { loggingService } from "@nimara/infrastructure/logging/service"; +import type { + Facet, + GetFacetsInfra, +} from "@nimara/infrastructure/use-cases/search/types"; + +import { getIndexName } from "../helpers"; +import type { AlgoliaSearchServiceConfig } from "../types"; + +export const algoliaGetFacetsInfra = ({ + credentials, + settings, +}: AlgoliaSearchServiceConfig): GetFacetsInfra => { + const algoliaClient = algoliasearch(credentials.appId, credentials.apiKey); + + return async (params, context) => { + const indexName = getIndexName(settings.indices, context.channel); + const searchIndex = algoliaClient.initIndex(indexName); + + const response = await searchIndex.search(params?.query ?? "", { + facets: ["*"], + sortFacetValuesBy: "alpha", + responseFields: ["facets"], + }); + + if (!response.facets) { + return { + facets: [], + }; + } + + const index = settings.indices.find( + (index) => index.channel === context.channel, + ); + + if (!index) { + loggingService.info("Index not found for channel", { + channel: context.channel, + }); + + return { + facets: [], + }; + } + + const facets = Object.entries(response.facets).reduce( + (acc, [facetName, facetChoices]) => { + const indexFacetConfig = index.availableFacets[facetName]; + + if (!indexFacetConfig) { + return acc; + } + + acc.push({ + ...indexFacetConfig, + choices: Object.entries(facetChoices).map(([name, _count]) => ({ + label: name, + value: name, + })), + }); + + return acc; + }, + [], + ); + + return { facets }; + }; +}; diff --git a/packages/infrastructure/src/private/algolia/search/infrastructure/get-sort-by-options-infra.ts b/packages/infrastructure/src/private/algolia/search/infrastructure/get-sort-by-options-infra.ts new file mode 100644 index 0000000..31eb241 --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/infrastructure/get-sort-by-options-infra.ts @@ -0,0 +1,26 @@ +import type { GetSortByOptionsInfra } from "@nimara/infrastructure/use-cases/search/types"; + +import type { AlgoliaSearchServiceConfig } from "../types"; + +export const algoliaGetSortByOptionsInfra = ( + config: AlgoliaSearchServiceConfig, +): GetSortByOptionsInfra => { + return (context) => { + const index = config.settings.indices.find( + (index) => index.channel === context.channel, + ); + + if (!index) { + return { + options: [], + }; + } + + return { + options: index.virtualReplicas.map((replica) => ({ + messageKey: replica.messageKey, + value: replica.queryParamValue, + })), + }; + }; +}; diff --git a/packages/infrastructure/src/private/algolia/search/infrastructure/search-infra.ts b/packages/infrastructure/src/private/algolia/search/infrastructure/search-infra.ts new file mode 100644 index 0000000..5e8536a --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/infrastructure/search-infra.ts @@ -0,0 +1,99 @@ +import algoliasearch from "algoliasearch"; + +import { loggingService } from "@nimara/infrastructure/logging/service"; +import type { SearchInfra } from "@nimara/infrastructure/use-cases/search/types"; + +import { getIndexName } from "../helpers"; +import { searchProductSerializer } from "../serializers"; +import type { AlgoliaSearchServiceConfig } from "../types"; + +export const algoliaSearchInfra = ({ + credentials, + serializers, + settings, +}: AlgoliaSearchServiceConfig): SearchInfra => { + const algoliaClient = algoliasearch(credentials.appId, credentials.apiKey); + + return async ({ page, filters, sortBy, query, limit }, { channel }) => { + const indexName = getIndexName(settings.indices, channel, sortBy); + const searchIndex = algoliaClient.initIndex(indexName); + + const mainIndex = settings.indices.find( + (index) => index.channel === channel, + ); + + // Create a mapping between slugs and Algolia name + const facetsMapping = Object.entries( + mainIndex?.availableFacets ?? {}, + ).reduce>((acc, [key, value]) => { + acc[value.slug] = key; + + return acc; + }, {}); + + // Create a mapping between slugs and Algolia name + const parsedFilters = Object.entries(filters ?? {}) + .reduce((acc, [name, value]) => { + if (name === "category") { + const formattedValue = ( + String(value).charAt(0).toUpperCase() + String(value).slice(1) + ).replaceAll("-", " & "); + acc.push(`categories.lvl0:'${formattedValue}'`); + } + + if (name in facetsMapping) { + const values = value.split("."); + + if (values.length > 1) { + const multipleValuesFacet: string[] = []; + + values.forEach((v) => { + multipleValuesFacet.push(`${facetsMapping[name]}:${v}`); + }); + + acc.push(multipleValuesFacet.join(" OR ")); + } else { + acc.push(`${facetsMapping[name]}:${value}`); + } + } + + return acc; + }, []) + .join(" AND "); + + try { + const { + hits, + page: currentPage, + nbPages, + } = await searchIndex.search(query as string, { + page: page ? Number.parseInt(page) - 1 : 0, + hitsPerPage: limit, + filters: parsedFilters, + responseFields: ["hits", "page", "nbPages"], + }); + + const serializer = serializers?.search ?? searchProductSerializer; + + return { + pageInfo: { + type: "numeric", + currentPage: currentPage + 1, + hasNextPage: currentPage < nbPages - 1, + hasPreviousPage: currentPage > 0, + }, + results: hits.map(serializer), + error: null, + }; + } catch (e) { + loggingService.error("Failed to fetch the products from Algolia", { + error: e, + }); + + return { + results: [], + error: e, + }; + } + }; +}; diff --git a/packages/infrastructure/src/private/algolia/search/providers.ts b/packages/infrastructure/src/private/algolia/search/providers.ts new file mode 100644 index 0000000..4b26f47 --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/providers.ts @@ -0,0 +1,22 @@ +import { getFacetsUseCase } from "@nimara/infrastructure/use-cases/search/get-facets-use-case"; +import { getSortByOptionsUseCase } from "@nimara/infrastructure/use-cases/search/get-sort-by-options-use-case"; +import { searchUseCase } from "@nimara/infrastructure/use-cases/search/search-use-case"; +import type { SearchService } from "@nimara/infrastructure/use-cases/search/types"; + +import { algoliaGetFacetsInfra } from "./infrastructure/get-facets-infra"; +import { algoliaGetSortByOptionsInfra } from "./infrastructure/get-sort-by-options-infra"; +import { algoliaSearchInfra } from "./infrastructure/search-infra"; +import type { AlgoliaSearchServiceConfig } from "./types"; + +export const algoliaSearchService = (config: AlgoliaSearchServiceConfig) => + ({ + getFacets: getFacetsUseCase({ + facetsInfra: algoliaGetFacetsInfra(config), + }), + getSortByOptions: getSortByOptionsUseCase({ + getSortByOptionsInfra: algoliaGetSortByOptionsInfra(config), + }), + search: searchUseCase({ + searchInfra: algoliaSearchInfra(config), + }), + }) satisfies SearchService; diff --git a/packages/infrastructure/src/private/algolia/search/serializers.ts b/packages/infrastructure/src/private/algolia/search/serializers.ts new file mode 100644 index 0000000..1f3fdac --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/serializers.ts @@ -0,0 +1,27 @@ +import { type SearchProduct } from "@nimara/domain/objects/SearchProduct"; + +import { type RecordSerializer } from "./types"; + +export const searchProductSerializer: RecordSerializer = ( + data, +) => + Object.freeze({ + currency: data.currency, + id: data.productId, + name: data.productName, + slug: data.slug, + price: Number(data.grossPrice), + thumbnail: data.thumbnail + ? { + // INFO: just for demo purposes + url: data.thumbnail.replace("=/256/", "=/1024/"), + } + : null, + media: Array.isArray(data.media) + ? (data.media as { alt: string; url: string }[]).map((mediaItem) => ({ + url: mediaItem.url, + alt: mediaItem.alt, + })) + : null, + updatedAt: data.updatedAt, + }); diff --git a/packages/infrastructure/src/private/algolia/search/types.ts b/packages/infrastructure/src/private/algolia/search/types.ts new file mode 100644 index 0000000..13f30ba --- /dev/null +++ b/packages/infrastructure/src/private/algolia/search/types.ts @@ -0,0 +1,94 @@ +import type { BaseHit } from "instantsearch.js"; + +import type { SearchProduct } from "@nimara/domain/objects/SearchProduct"; +import type { + Facet, + SearchContext, +} from "@nimara/infrastructure/use-cases/search/types"; + +export type RecordSerializer> = { + (data: T): Readonly; +}; + +export type AlgoliaSearchServiceConfig = { + credentials: { + apiKey: string; + appId: string; + }; + serializers?: { + search?: RecordSerializer; + }; + settings: { + indices: IndicesSettings; + }; +}; + +export type IndicesSettings = Array< + { + /** + * An object with defined facets within given index. {@link AvailableFacets}. + */ + availableFacets: AvailableFacets; + /** + * A main index name. + * + * @example + * "DEV.channel-us.USD.products" + */ + indexName: string; + /** + * An array of virtual replicas used for sorting. {@link VirtualReplica}. + */ + virtualReplicas: Array; + } & SearchContext +>; + +/** + * A virtual replica config used to define a possible sorting options. + * + * @see https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas Understanding replicas - Algolia Documentation + * @example + * ```ts + * { + * indexName: "DEV.channel-us.USD.products.name_asc", + * messageKey: "search.name-asc", + * queryParamValue: "alpha-asc", + * } + * ``` + */ +export type VirtualReplica = { + /** + * Index name used for sorting (Virtual Replica). + * + * @example + * "DEV.channel-uk.GBP.products.name_asc" + */ + indexName: string; + /** + * A message key with namespace used for translations. + * + * @example + * "search.name-asc" + */ + messageKey: string; + /** + * A value that will be stored in search URL. - /search?sortBy={THIS VALUE}. + * + * @example + * "name-asc" + */ + queryParamValue: string; +}; + +/** + * An object with available facets for given index, where the key is name of the attribute in Algolia. + * + * @example + * ```ts + * "attributes.Artists": { + * messageKey: "filters.artists", + * type: "DROPDOWN", + * }, + * ``` + */ +export type AvailableFacets = Record>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 222cb1d..8de7726 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -524,6 +524,12 @@ importers: '@types/stripe': specifier: 8.0.417 version: 8.0.417 + algoliasearch: + specifier: 4.22.1 + version: 4.22.1 + instantsearch.js: + specifier: 4.64.0 + version: 4.64.0(algoliasearch@4.22.1) libphonenumber-js: specifier: 1.11.3 version: 1.11.3 @@ -698,6 +704,57 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} + '@algolia/cache-browser-local-storage@4.22.1': + resolution: {integrity: sha512-Sw6IAmOCvvP6QNgY9j+Hv09mvkvEIDKjYW8ow0UDDAxSXy664RBNQk3i/0nt7gvceOJ6jGmOTimaZoY1THmU7g==} + + '@algolia/cache-common@4.22.1': + resolution: {integrity: sha512-TJMBKqZNKYB9TptRRjSUtevJeQVXRmg6rk9qgFKWvOy8jhCPdyNZV1nB3SKGufzvTVbomAukFR8guu/8NRKBTA==} + + '@algolia/cache-in-memory@4.22.1': + resolution: {integrity: sha512-ve+6Ac2LhwpufuWavM/aHjLoNz/Z/sYSgNIXsinGofWOysPilQZPUetqLj8vbvi+DHZZaYSEP9H5SRVXnpsNNw==} + + '@algolia/client-account@4.22.1': + resolution: {integrity: sha512-k8m+oegM2zlns/TwZyi4YgCtyToackkOpE+xCaKCYfBfDtdGOaVZCM5YvGPtK+HGaJMIN/DoTL8asbM3NzHonw==} + + '@algolia/client-analytics@4.22.1': + resolution: {integrity: sha512-1ssi9pyxyQNN4a7Ji9R50nSdISIumMFDwKNuwZipB6TkauJ8J7ha/uO60sPJFqQyqvvI+px7RSNRQT3Zrvzieg==} + + '@algolia/client-common@4.22.1': + resolution: {integrity: sha512-IvaL5v9mZtm4k4QHbBGDmU3wa/mKokmqNBqPj0K7lcR8ZDKzUorhcGp/u8PkPC/e0zoHSTvRh7TRkGX3Lm7iOQ==} + + '@algolia/client-personalization@4.22.1': + resolution: {integrity: sha512-sl+/klQJ93+4yaqZ7ezOttMQ/nczly/3GmgZXJ1xmoewP5jmdP/X/nV5U7EHHH3hCUEHeN7X1nsIhGPVt9E1cQ==} + + '@algolia/client-search@4.22.1': + resolution: {integrity: sha512-yb05NA4tNaOgx3+rOxAmFztgMTtGBi97X7PC3jyNeGiwkAjOZc2QrdZBYyIdcDLoI09N0gjtpClcackoTN0gPA==} + + '@algolia/events@4.0.1': + resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==} + + '@algolia/logger-common@4.22.1': + resolution: {integrity: sha512-OnTFymd2odHSO39r4DSWRFETkBufnY2iGUZNrMXpIhF5cmFE8pGoINNPzwg02QLBlGSaLqdKy0bM8S0GyqPLBg==} + + '@algolia/logger-console@4.22.1': + resolution: {integrity: sha512-O99rcqpVPKN1RlpgD6H3khUWylU24OXlzkavUAMy6QZd1776QAcauE3oP8CmD43nbaTjBexZj2nGsBH9Tc0FVA==} + + '@algolia/requester-browser-xhr@4.22.1': + resolution: {integrity: sha512-dtQGYIg6MteqT1Uay3J/0NDqD+UciHy3QgRbk7bNddOJu+p3hzjTRYESqEnoX/DpEkaNYdRHUKNylsqMpgwaEw==} + + '@algolia/requester-common@4.22.1': + resolution: {integrity: sha512-dgvhSAtg2MJnR+BxrIFqlLtkLlVVhas9HgYKMk2Uxiy5m6/8HZBL40JVAMb2LovoPFs9I/EWIoFVjOrFwzn5Qg==} + + '@algolia/requester-node-http@4.22.1': + resolution: {integrity: sha512-JfmZ3MVFQkAU+zug8H3s8rZ6h0ahHZL/SpMaSasTCGYR5EEJsCc8SI5UZ6raPN2tjxa5bxS13BRpGSBUens7EA==} + + '@algolia/transporter@4.22.1': + resolution: {integrity: sha512-kzWgc2c9IdxMa3YqA6TN0NW5VrKYYW/BELIn7vnLyn+U/RFdZ4lxxt9/8yq3DKV5snvoDzzO4ClyejZRdV3lMQ==} + + '@algolia/ui-components-highlight-vdom@1.2.3': + resolution: {integrity: sha512-gNlPkCwX2M7LnNhCSAjvQkOhdeYDu+FP0ISc0IY297Kw9EmGeUxpce8lvKGByYglz7ifW3qTWRxrtCV3is5+Ag==} + + '@algolia/ui-components-shared@1.2.3': + resolution: {integrity: sha512-Nk4stv4FW9qIpvmdkJMf6oS49xA8Ns/IAAYNngpaFFQTErZupegXvyr8W+6NVvBSzHzeE4H8YO7spg3xWR8e8A==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -4006,6 +4063,9 @@ packages: '@types/doctrine@0.0.9': resolution: {integrity: sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==} + '@types/dom-speech-recognition@0.0.1': + resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==} + '@types/ejs@3.1.5': resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==} @@ -4042,6 +4102,9 @@ packages: '@types/glob@7.2.0': resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + '@types/google.maps@3.58.1': + resolution: {integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -4051,6 +4114,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hogan.js@3.0.5': + resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==} + '@types/http-assert@1.5.5': resolution: {integrity: sha512-4+tE/lwdAahgZT1g30Jkdm9PzFRde0xwxBNUyRsCitRvCQB90iuA2uJYdUnhnANRcqGXaWOGY4FEoxeElNAK2g==} @@ -4434,6 +4500,9 @@ packages: resolution: {integrity: sha512-6xm38yGVIa6mKm/DUCF2zFFJhERh/QWp1ufm4cNUvxsONBmfPg8uZ9pZBdOmF6qFGr/HlT6ABBkCSx/dlEtvWg==} engines: {node: '>=12 <14 || 14.2 - 14.9 || >14.10.0'} + abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -4511,6 +4580,14 @@ packages: ajv@8.12.0: resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==} + algoliasearch-helper@3.16.1: + resolution: {integrity: sha512-qxAHVjjmT7USVvrM8q6gZGaJlCK1fl4APfdAA7o8O6iXEc68G0xMNrzRkxoB/HmhhvyHnoteS/iMTiHiTcQQcg==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + + algoliasearch@4.22.1: + resolution: {integrity: sha512-jwydKFQJKIx9kIZ8Jm44SdpigFwRGPESaxZBaHSV0XWN2yBJAOT4mT7ppvlrpA4UGzz92pqFnVKr/kaZXrcreg==} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -5941,6 +6018,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@6.2.1: @@ -6564,6 +6642,10 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} + hogan.js@3.0.2: + resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==} + hasBin: true + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -6586,6 +6668,9 @@ packages: resolution: {integrity: sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==} engines: {node: ^18.17.0 || >=20.5.0} + htm@3.1.1: + resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-entities@2.5.2: resolution: {integrity: sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==} @@ -6717,6 +6802,11 @@ packages: resolution: {integrity: sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==} engines: {node: '>=12.0.0'} + instantsearch.js@4.64.0: + resolution: {integrity: sha512-JQjq8jF98JktNvRxCYL7DM3eLFUVURxwnaPRbqa0Tw0S/u1rypCQBymvx7ROzfqGXJpmSmnvZzMYmAW0DWzGNw==} + peerDependencies: + algoliasearch: '>= 3.1 < 6' + internal-slot@1.0.6: resolution: {integrity: sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==} engines: {node: '>= 0.4'} @@ -7782,6 +7872,10 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + mkdirp@0.3.0: + resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==} + deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.) + mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -7970,6 +8064,10 @@ packages: non-layered-tidy-tree-layout@2.0.2: resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + nopt@1.0.10: + resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -8706,6 +8804,10 @@ packages: resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==} engines: {node: '>=0.6'} + qs@6.9.7: + resolution: {integrity: sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -9116,6 +9218,9 @@ packages: scuid@1.1.0: resolution: {integrity: sha512-MuCAyrGZcTLfQoH2XoBlQ8C6bzwN88XT/0slOGz0pn8+gIP85BOAfYa44ZXQUTOwRwPU0QvgU+V+OSajl/59Xg==} + search-insights@2.17.2: + resolution: {integrity: sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==} + section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -10471,6 +10576,77 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} + '@algolia/cache-browser-local-storage@4.22.1': + dependencies: + '@algolia/cache-common': 4.22.1 + + '@algolia/cache-common@4.22.1': {} + + '@algolia/cache-in-memory@4.22.1': + dependencies: + '@algolia/cache-common': 4.22.1 + + '@algolia/client-account@4.22.1': + dependencies: + '@algolia/client-common': 4.22.1 + '@algolia/client-search': 4.22.1 + '@algolia/transporter': 4.22.1 + + '@algolia/client-analytics@4.22.1': + dependencies: + '@algolia/client-common': 4.22.1 + '@algolia/client-search': 4.22.1 + '@algolia/requester-common': 4.22.1 + '@algolia/transporter': 4.22.1 + + '@algolia/client-common@4.22.1': + dependencies: + '@algolia/requester-common': 4.22.1 + '@algolia/transporter': 4.22.1 + + '@algolia/client-personalization@4.22.1': + dependencies: + '@algolia/client-common': 4.22.1 + '@algolia/requester-common': 4.22.1 + '@algolia/transporter': 4.22.1 + + '@algolia/client-search@4.22.1': + dependencies: + '@algolia/client-common': 4.22.1 + '@algolia/requester-common': 4.22.1 + '@algolia/transporter': 4.22.1 + + '@algolia/events@4.0.1': {} + + '@algolia/logger-common@4.22.1': {} + + '@algolia/logger-console@4.22.1': + dependencies: + '@algolia/logger-common': 4.22.1 + + '@algolia/requester-browser-xhr@4.22.1': + dependencies: + '@algolia/requester-common': 4.22.1 + + '@algolia/requester-common@4.22.1': {} + + '@algolia/requester-node-http@4.22.1': + dependencies: + '@algolia/requester-common': 4.22.1 + + '@algolia/transporter@4.22.1': + dependencies: + '@algolia/cache-common': 4.22.1 + '@algolia/logger-common': 4.22.1 + '@algolia/requester-common': 4.22.1 + + '@algolia/ui-components-highlight-vdom@1.2.3': + dependencies: + '@algolia/ui-components-shared': 1.2.3 + '@babel/runtime': 7.23.9 + + '@algolia/ui-components-shared@1.2.3': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.2.1': @@ -15374,6 +15550,8 @@ snapshots: '@types/doctrine@0.0.9': {} + '@types/dom-speech-recognition@0.0.1': {} + '@types/ejs@3.1.5': {} '@types/emscripten@1.39.10': {} @@ -15419,6 +15597,8 @@ snapshots: '@types/minimatch': 5.1.2 '@types/node': 20.14.12 + '@types/google.maps@3.58.1': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 20.14.12 @@ -15431,6 +15611,8 @@ snapshots: dependencies: '@types/unist': 3.0.2 + '@types/hogan.js@3.0.5': {} + '@types/http-assert@1.5.5': {} '@types/http-errors@2.0.4': {} @@ -15921,6 +16103,8 @@ snapshots: '@types/emscripten': 1.39.10 tslib: 1.14.1 + abbrev@1.1.1: {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -15999,6 +16183,28 @@ snapshots: uri-js: 4.4.1 optional: true + algoliasearch-helper@3.16.1(algoliasearch@4.22.1): + dependencies: + '@algolia/events': 4.0.1 + algoliasearch: 4.22.1 + + algoliasearch@4.22.1: + dependencies: + '@algolia/cache-browser-local-storage': 4.22.1 + '@algolia/cache-common': 4.22.1 + '@algolia/cache-in-memory': 4.22.1 + '@algolia/client-account': 4.22.1 + '@algolia/client-analytics': 4.22.1 + '@algolia/client-common': 4.22.1 + '@algolia/client-personalization': 4.22.1 + '@algolia/client-search': 4.22.1 + '@algolia/logger-common': 4.22.1 + '@algolia/logger-console': 4.22.1 + '@algolia/requester-browser-xhr': 4.22.1 + '@algolia/requester-common': 4.22.1 + '@algolia/requester-node-http': 4.22.1 + '@algolia/transporter': 4.22.1 + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -17562,7 +17768,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.15.0 eslint: 8.57.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -17574,7 +17780,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -17595,7 +17801,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.13.1(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -18580,6 +18786,11 @@ snapshots: highlight.js@10.7.3: {} + hogan.js@3.0.2: + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -18600,6 +18811,8 @@ snapshots: dependencies: lru-cache: 10.3.0 + htm@3.1.1: {} + html-entities@2.5.2: {} html-tags@3.3.1: {} @@ -18755,6 +18968,23 @@ snapshots: through: 2.3.8 wrap-ansi: 6.2.0 + instantsearch.js@4.64.0(algoliasearch@4.22.1): + dependencies: + '@algolia/events': 4.0.1 + '@algolia/ui-components-highlight-vdom': 1.2.3 + '@algolia/ui-components-shared': 1.2.3 + '@types/dom-speech-recognition': 0.0.1 + '@types/google.maps': 3.58.1 + '@types/hogan.js': 3.0.5 + '@types/qs': 6.9.10 + algoliasearch: 4.22.1 + algoliasearch-helper: 3.16.1(algoliasearch@4.22.1) + hogan.js: 3.0.2 + htm: 3.1.1 + preact: 10.11.3 + qs: 6.9.7 + search-insights: 2.17.2 + internal-slot@1.0.6: dependencies: get-intrinsic: 1.2.2 @@ -20104,6 +20334,8 @@ snapshots: mkdirp-classic@0.5.3: {} + mkdirp@0.3.0: {} + mkdirp@0.5.6: dependencies: minimist: 1.2.8 @@ -20321,6 +20553,10 @@ snapshots: non-layered-tidy-tree-layout@2.0.2: {} + nopt@1.0.10: + dependencies: + abbrev: 1.1.1 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -20933,6 +21169,8 @@ snapshots: dependencies: side-channel: 1.0.4 + qs@6.9.7: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} @@ -21443,6 +21681,8 @@ snapshots: scuid@1.1.0: {} + search-insights@2.17.2: {} + section-matter@1.0.0: dependencies: extend-shallow: 2.0.1 diff --git a/turbo.json b/turbo.json index 0063dc2..e6ada46 100644 --- a/turbo.json +++ b/turbo.json @@ -4,6 +4,8 @@ "globalPassThroughEnv": [ "AUTH_SECRET", "BASE_URL", + "NEXT_PUBLIC_ALGOLIA_API_KEY", + "NEXT_PUBLIC_ALGOLIA_APP_ID", "SALEOR_APP_TOKEN", "SENTRY_AUTH_TOKEN", "SENTRY_ORG",