diff --git a/commerce/faire/client.ts b/commerce/faire/client.ts new file mode 100644 index 00000000..fdfd66c2 --- /dev/null +++ b/commerce/faire/client.ts @@ -0,0 +1,18 @@ +import { fetchAPI } from "../../utils/fetch.ts"; +import { FaireSearchResult } from "./types.ts"; + +export default { + searchProducts: async (query: string) => { + const headers = new Headers(); + headers.set("content-type", "application/json"); + headers.set("Origin", "https://www.faire.com/"); + headers.set("user-agent", "deco.cx"); + + // TODO: Create Account and use it + const result = (await fetchAPI( + `https://proxy-faire.deco-cx.workers.dev?query=${query}`, + ).catch(console.log)) as FaireSearchResult; + + return result; + }, +}; diff --git a/commerce/faire/transform.ts b/commerce/faire/transform.ts new file mode 100644 index 00000000..c43cbc60 --- /dev/null +++ b/commerce/faire/transform.ts @@ -0,0 +1,61 @@ +import type { Product, UnitPriceSpecification } from "../types.ts"; + +import { FaireSearchResult } from "./types.ts"; + +export const toProduct = ( + productTile: FaireSearchResult["product_tiles"][number], +): Product => { + const product = productTile.product; + const bestImage = productTile.best_image; + const minOptionRetailPrice = productTile.min_option_retail_price; + + const priceSpec: UnitPriceSpecification[] = [{ + "@type": "UnitPriceSpecification", + priceType: "https://schema.org/SalePrice", + price: Number(minOptionRetailPrice.amount_cents / 100), + }]; + const url = + `https://www.faire.com/search?brand=${product.brand_token}&product=${product.token}&q=from+deco&signUp=product`; + + const price = minOptionRetailPrice.amount_cents / 100; + + return { + "@type": "Product", + productID: product.token, + "name": product.name, + "brand": product.brand_token, + "description": product.description, + "sku": product.taxonomy_type.token, + "category": product.taxonomy_type.name, + url, + isVariantOf: { + "@type": "ProductGroup", + productGroupID: product.token, + hasVariant: [], + url, + name: product.name, + additionalProperty: [], + }, + image: [{ + "@type": "ImageObject", + alternateName: product.name, + url: bestImage.url, + }], + offers: { + "@type": "AggregateOffer", + priceCurrency: "USD", + highPrice: price, + lowPrice: Number(price), + offerCount: 1, + offers: [{ + "@type": "Offer", + price: Number(price), + availability: true + ? "https://schema.org/InStock" + : "https://schema.org/OutOfStock", + inventoryLevel: { value: 1000 }, + priceSpecification: priceSpec, + }], + }, + }; +}; diff --git a/commerce/faire/types.ts b/commerce/faire/types.ts new file mode 100644 index 00000000..278a0bae --- /dev/null +++ b/commerce/faire/types.ts @@ -0,0 +1,269 @@ +export interface FaireSearchResult { + pagination_data: PaginationData; + request_id: string; + total_primary_results: number; + show_brand_off_faire_toggle: boolean; + formatted_total_primary_results_count: string; + product_tiles: ProductTile[]; + brands_by_token: { [key: string]: BrandsByToken }; + brand_tiles_related_to_search: BrandTilesRelatedToSearch[]; + filter_sections: FilterSection[]; + related_search_terms: any[]; + empty_suggestion_brands: any[]; + visual_filter_options: any[]; + badges: AvailableFilters; + brand_related_product_tiles: any[]; + fallback_product_tiles: any[]; + suggested_categories_broad_query: any[]; + retrieval_queries: any[]; + brand_related_products: any[]; + ranking_debug_info_for_products: any[]; + products: any[]; + fallback_products: any[]; + brand_tokens: any[]; + available_filters: AvailableFilters; + brand_off_faire_related_brands_info: any[]; +} + +export interface AvailableFilters { +} + +export interface BrandTilesRelatedToSearch { + brand: Brand; + lead_time_text: string; + images: Image[]; +} + +export interface Brand { + token: string; + name: string; + description: string; + ships_from_country: BasedIn; + is_first_order: boolean; + has_new_products: boolean; + profile_image: Image; + squared_image: Image; + minimum_order_amount: Min; + is_highlighted_brand_for_promotional_event: boolean; + is_top_shop?: boolean; + zip_code_protection_enabled: boolean; + based_in: BasedIn; + hide_chat: boolean; + show_location_badging: ShowLocationBadging; + active_products_count: number; + based_in_country_message: BasedInCountryMessage; + highlighted_apparel_seasons_for_promotional_event: any[]; +} + +export enum BasedIn { + Chn = "CHN", + Usa = "USA", +} + +export interface BasedInCountryMessage { + desktop_message: Message; + mobile_message: Message; +} + +export enum Message { + UnitedStates = "United States", +} + +export interface Min { + amount_cents: number; + currency: Currency; +} + +export enum Currency { + Usd = "USD", +} + +export interface Image { + token: string; + width: number; + height: number; + sequence?: number; + url: string; + tags: string[]; + type?: ProfileImageType; +} + +export enum ProfileImageType { + Hero = "HERO", + Product = "PRODUCT", +} + +export enum ShowLocationBadging { + DoNotShow = "DO_NOT_SHOW", +} + +export interface BrandsByToken { + token: string; + name: string; + forbids_online_only_retailers: boolean; + minimum_order_amount: Min; + ships_from_country: BasedIn; + based_in: BasedIn; + is_highlighted_brand_for_promotional_event: boolean; + is_top_shop?: boolean; + active_products_count: number; + based_in_country_message: BasedInCountryMessage; + profile_image: Image; +} + +export interface FilterSection { + selection_type: string; + field_name: string; + display_name: string; + formatted_display_name: string; + short_display_name: string; + searchable: boolean; + field_function: string; + pill_text: string; + search_label_text?: string; + options: Option[]; + range_filters: any[]; + option_groups: any[]; +} + +export interface Option { + key: string; + display_name: string; + global_display_name: string; + is_selected: boolean; + selection_state: SelectionState; + results_count: number; + sub_options: Option[]; + customizations: any[]; +} + +export enum SelectionState { + Unselected = "UNSELECTED", +} + +export interface PaginationData { + page_number: number; + page_size: number; + page_count: number; + total_results: number; +} + +export interface ProductTile { + product: Product; + badge_list: BadgeList; + best_image: Image; + quick_add: QuickAdd; + min_option_retail_price: Min; + has_active_brand_promo: boolean; + is_highlighted_product_for_promotional_event: boolean; + case_size_text?: string; + based_in_country: BasedIn; + show_location_badging: ShowLocationBadging; + images: Image[]; + min_option_brand_code?: string; + expected_shipping_date?: string; +} + +export interface BadgeList { + badges: Badge[]; +} + +export interface Badge { + priority: number; + type: BadgeType; + display_to_user: boolean; + style: Style; +} + +export interface Style { + position: Position; + badge_message: BadgeMessage; + badge_message_key: BadgeMessageKey; +} + +export enum BadgeMessage { + HighSellThrough = "High sell-through", +} + +export enum BadgeMessageKey { + BadgesHighSellThrough = "badges.high_sell_through", +} + +export enum Position { + Middle = "MIDDLE", +} + +export enum BadgeType { + HighSellThrough = "HIGH_SELL_THROUGH", +} + +export interface Product { + token: string; + name: string; + brand_token: string; + is_new: boolean; + maker_best_seller: boolean; + per_style_min_order_quantity: number; + state: State; + description?: string; + taxonomy_type: TaxonomyType; + preorderable: boolean; + is_promoted: boolean; + board_tokens: any[]; + promoted_by: any[]; + expected_ship_date?: number; + expected_ship_window_days?: number; +} + +export enum State { + ForSale = "FOR_SALE", +} + +export interface TaxonomyType { + token: string; + name: string; + group_name: string; + clean_name: string; + target_customer: TargetCustomer; + target_customer_display_name: TargetCustomerDisplayName; +} + +export enum TargetCustomer { + AdultMen = "ADULT_MEN", + AdultUnisex = "ADULT_UNISEX", + AdultWomen = "ADULT_WOMEN", + KidsUnisex = "KIDS_UNISEX", + PetsDogs = "PETS_DOGS", +} + +export enum TargetCustomerDisplayName { + AdultsMen = "Adults - Men", + AdultsUnisex = "Adults - Unisex", + AdultsWomen = "Adults - Women", + KidsUnisex = "Kids - Unisex", + PetsDogs = "Pets - Dogs", +} + +export interface QuickAdd { + quick_add_text: QuickAddText; + mobile_quick_add_text: MobileQuickAddText; + quick_add_option?: QuickAddOption; +} + +export enum MobileQuickAddText { + CaseOf1 = "Case of 1", + MoreOptions = "More Options", + PrePacksAvailable = "Pre-Packs Available", +} + +export interface QuickAddOption { + option_token: string; + option_unit_multiplier: number; + option_min_order_quantity: number; + option_available_units?: number; +} + +export enum QuickAddText { + ChooseOptions = "Choose Options", + QuickAddPack = "+ Quick Add Pack", +} diff --git a/live.gen.ts b/live.gen.ts index 93142bc1..98a331d0 100644 --- a/live.gen.ts +++ b/live.gen.ts @@ -36,19 +36,21 @@ import * as $$1 from "./accounts/yourViews.ts"; import * as $$2 from "./accounts/vtex.ts"; import * as $$3 from "./accounts/shopify.ts"; import * as $$4 from "./accounts/occ.ts"; -import * as $$$0 from "./loaders/vtex/legacy/productList.ts"; -import * as $$$1 from "./loaders/vtex/legacy/productDetailsPage.ts"; -import * as $$$2 from "./loaders/vtex/legacy/productListingPage.ts"; -import * as $$$3 from "./loaders/vtex/legacy/relatedProductsLoader.ts"; -import * as $$$4 from "./loaders/vtex/wishlist.ts"; -import * as $$$5 from "./loaders/vtex/navbar.ts"; -import * as $$$6 from "./loaders/vtex/proxy.ts"; -import * as $$$7 from "./loaders/vtex/intelligentSearch/productList.ts"; -import * as $$$8 from "./loaders/vtex/intelligentSearch/productDetailsPage.ts"; -import * as $$$9 from "./loaders/vtex/intelligentSearch/productListingPage.ts"; -import * as $$$10 from "./loaders/vtex/intelligentSearch/suggestions.ts"; -import * as $$$11 from "./loaders/vtex/cart.ts"; -import * as $$$12 from "./loaders/vtex/user.ts"; +import * as $$$0 from "./loaders/faireSearch.ts"; +import * as $$$1 from "./loaders/vtex/legacy/productList.ts"; +import * as $$$2 from "./loaders/vtex/legacy/productDetailsPage.ts"; +import * as $$$3 from "./loaders/vtex/legacy/productListingPage.ts"; +import * as $$$4 from "./loaders/vtex/legacy/relatedProductsLoader.ts"; +import * as $$$5 from "./loaders/vtex/wishlist.ts"; +import * as $$$6 from "./loaders/vtex/navbar.ts"; +import * as $$$7 from "./loaders/vtex/proxy.ts"; +import * as $$$8 from "./loaders/vtex/intelligentSearch/productList.ts"; +import * as $$$9 from "./loaders/vtex/intelligentSearch/productDetailsPage.ts"; +import * as $$$10 from "./loaders/vtex/intelligentSearch/productListingPage.ts"; +import * as $$$11 from "./loaders/vtex/intelligentSearch/suggestions.ts"; +import * as $$$12 from "./loaders/vtex/cart.ts"; +import * as $$$13 from "./loaders/vtex/user.ts"; +import * as $$$14 from "./loaders/faireSearchPage.ts"; import * as $$$$0 from "./routes/404.tsx"; import * as $$$$1 from "./routes/styles.css.ts"; import * as $$$$2 from "./routes/_app.tsx"; @@ -160,19 +162,22 @@ const manifest = { "$live/loaders/state.ts": i1$0, "$live/loaders/workflows/events.ts": i1$1, "$live/loaders/workflows/get.ts": i1$2, - "deco-sites/std/loaders/vtex/cart.ts": $$$11, - "deco-sites/std/loaders/vtex/intelligentSearch/productDetailsPage.ts": $$$8, - "deco-sites/std/loaders/vtex/intelligentSearch/productList.ts": $$$7, - "deco-sites/std/loaders/vtex/intelligentSearch/productListingPage.ts": $$$9, - "deco-sites/std/loaders/vtex/intelligentSearch/suggestions.ts": $$$10, - "deco-sites/std/loaders/vtex/legacy/productDetailsPage.ts": $$$1, - "deco-sites/std/loaders/vtex/legacy/productList.ts": $$$0, - "deco-sites/std/loaders/vtex/legacy/productListingPage.ts": $$$2, - "deco-sites/std/loaders/vtex/legacy/relatedProductsLoader.ts": $$$3, - "deco-sites/std/loaders/vtex/navbar.ts": $$$5, - "deco-sites/std/loaders/vtex/proxy.ts": $$$6, - "deco-sites/std/loaders/vtex/user.ts": $$$12, - "deco-sites/std/loaders/vtex/wishlist.ts": $$$4, + "deco-sites/std/loaders/faireSearch.ts": $$$0, + "deco-sites/std/loaders/faireSearchPage.ts": $$$14, + "deco-sites/std/loaders/vtex/cart.ts": $$$12, + "deco-sites/std/loaders/vtex/intelligentSearch/productDetailsPage.ts": $$$9, + "deco-sites/std/loaders/vtex/intelligentSearch/productList.ts": $$$8, + "deco-sites/std/loaders/vtex/intelligentSearch/productListingPage.ts": + $$$10, + "deco-sites/std/loaders/vtex/intelligentSearch/suggestions.ts": $$$11, + "deco-sites/std/loaders/vtex/legacy/productDetailsPage.ts": $$$2, + "deco-sites/std/loaders/vtex/legacy/productList.ts": $$$1, + "deco-sites/std/loaders/vtex/legacy/productListingPage.ts": $$$3, + "deco-sites/std/loaders/vtex/legacy/relatedProductsLoader.ts": $$$4, + "deco-sites/std/loaders/vtex/navbar.ts": $$$6, + "deco-sites/std/loaders/vtex/proxy.ts": $$$7, + "deco-sites/std/loaders/vtex/user.ts": $$$13, + "deco-sites/std/loaders/vtex/wishlist.ts": $$$5, }, "routes": { "./routes/_app.tsx": $$$$2, diff --git a/loaders/faireSearch.ts b/loaders/faireSearch.ts new file mode 100644 index 00000000..11008e4f --- /dev/null +++ b/loaders/faireSearch.ts @@ -0,0 +1,27 @@ +import type { Product } from "../commerce/types.ts"; +import client from "../commerce/faire/client.ts"; +import { toProduct } from "../commerce/faire/transform.ts"; + +export interface Props { + /** @description query to use on search */ + query: string; + /** @description total number of items to display */ + count: number; +} + +/** + * @title Faire - Search products + * @description Usefull for shelves and static galleries. + */ +const productListLoader = async ( + { query, count }: Props, +): Promise => { + console.log({ query }); + const rawProducts = await client.searchProducts(query); + + const products = rawProducts.product_tiles.slice(0, count).map(toProduct); + + return products; +}; + +export default productListLoader; diff --git a/loaders/faireSearchPage.ts b/loaders/faireSearchPage.ts new file mode 100644 index 00000000..32515863 --- /dev/null +++ b/loaders/faireSearchPage.ts @@ -0,0 +1,43 @@ +import type { ProductListingPage } from "../commerce/types.ts"; +import client from "../commerce/faire/client.ts"; +import { toProduct } from "../commerce/faire/transform.ts"; + +export interface Props { + /** @description total number of items to display */ + items: number; +} + +/** + * @title Faire - Search Page + * @description Uses ?s + */ +const productListLoader = async ( + { items }: Props, + req: Request, +): Promise => { + const url = new URL(req.url); + const query = url.searchParams.get("s"); + const rawProducts = await client.searchProducts(query as string); + + const products = rawProducts.product_tiles.slice(0, items).map(toProduct); + + return { + "@type": "ProductListingPage", + breadcrumb: { + "@type": "BreadcrumbList", + itemListElement: [], + numberOfItems: 0, + }, + filters: [], + products, + // TODO: Pagination + pageInfo: { + nextPage: undefined, + previousPage: undefined, + currentPage: 1, + }, + sortOptions: [], + }; +}; + +export default productListLoader; diff --git a/schemas.gen.json b/schemas.gen.json index 4f2cacfc..00ab9f6d 100644 --- a/schemas.gen.json +++ b/schemas.gen.json @@ -47,6 +47,8 @@ "$live/loaders/state.ts", "$live/loaders/workflows/events.ts", "$live/loaders/workflows/get.ts", + "deco-sites/std/loaders/faireSearch.ts", + "deco-sites/std/loaders/faireSearchPage.ts", "deco-sites/std/loaders/vtex/cart.ts", "deco-sites/std/loaders/vtex/intelligentSearch/productDetailsPage.ts", "deco-sites/std/loaders/vtex/intelligentSearch/productList.ts", @@ -1910,6 +1912,28 @@ } } }, + { + "title": "Faire - Search products", + "description": "Usefull for shelves and static galleries.", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaC50cw==@Props" + } + ], + "required": [ + "__resolveType" + ], + "properties": { + "__resolveType": { + "type": "string", + "enum": [ + "deco-sites/std/loaders/faireSearch.ts" + ], + "default": "deco-sites/std/loaders/faireSearch.ts" + } + } + }, { "title": "VTEX product list - Intelligent Search", "description": "Useful for shelves and galleries.", @@ -2392,6 +2416,28 @@ } } }, + { + "title": "Faire - Search Page", + "description": "Uses ?s", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaFBhZ2UudHM=@Props" + } + ], + "required": [ + "__resolveType" + ], + "properties": { + "__resolveType": { + "type": "string", + "enum": [ + "deco-sites/std/loaders/faireSearchPage.ts" + ], + "default": "deco-sites/std/loaders/faireSearchPage.ts" + } + } + }, { "title": "VTEX Intelligent Search - Product Listing page", "description": "Returns data ready for search pages like category,brand pages", @@ -3687,6 +3733,40 @@ ], "title": "deco-cx/live/loaders/workflows/get.ts@Props" }, + "ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaC50cw==@Props": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "query to use on search", + "title": "Query" + }, + "count": { + "type": "number", + "description": "total number of items to display", + "title": "Count" + } + }, + "required": [ + "query", + "count" + ], + "title": "deco-sites/std/loaders/faireSearch.ts@Props" + }, + "ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaFBhZ2UudHM=@Props": { + "type": "object", + "properties": { + "items": { + "type": "number", + "description": "total number of items to display", + "title": "Items" + } + }, + "required": [ + "items" + ], + "title": "deco-sites/std/loaders/faireSearchPage.ts@Props" + }, "ZGVjby1zaXRlcy9zdGQvcGFja3MvdnRleC90eXBlcy50cw==@Fields": { "type": "object", "properties": { @@ -7300,6 +7380,14 @@ ], "title": "Canonical URL" }, + "noIndexNoFollow": { + "type": [ + "boolean", + "null" + ], + "title": "Disable indexing", + "description": "In testing, you can use this to prevent search engines from indexing your site" + }, "context": { "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvY29tbWVyY2UvdHlwZXMudHM=@ProductDetailsPage|null|ProductListingPage|null", "title": "Context" @@ -8917,6 +9005,50 @@ } } }, + "ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaC50cw==": { + "title": "Faire - Search products", + "description": "Usefull for shelves and static galleries.", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaC50cw==@Props" + } + ], + "required": [ + "__resolveType" + ], + "properties": { + "__resolveType": { + "type": "string", + "enum": [ + "deco-sites/std/loaders/faireSearch.ts" + ], + "default": "deco-sites/std/loaders/faireSearch.ts" + } + } + }, + "ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaFBhZ2UudHM=": { + "title": "Faire - Search Page", + "description": "Uses ?s", + "type": "object", + "allOf": [ + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaFBhZ2UudHM=@Props" + } + ], + "required": [ + "__resolveType" + ], + "properties": { + "__resolveType": { + "type": "string", + "enum": [ + "deco-sites/std/loaders/faireSearchPage.ts" + ], + "default": "deco-sites/std/loaders/faireSearchPage.ts" + } + } + }, "ZGVjby1zaXRlcy9zdGQvbG9hZGVycy92dGV4L2NhcnQudHM=": { "title": "deco-sites/std/loaders/vtex/cart.ts", "docs": "https://developers.vtex.com/docs/api-reference/checkout-api#get-/api/checkout/pub/orderForm", @@ -10432,6 +10564,12 @@ { "$ref": "#/definitions/JGxpdmUvbG9hZGVycy93b3JrZmxvd3MvZ2V0LnRz" }, + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaC50cw==" + }, + { + "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy9mYWlyZVNlYXJjaFBhZ2UudHM=" + }, { "$ref": "#/definitions/ZGVjby1zaXRlcy9zdGQvbG9hZGVycy92dGV4L2NhcnQudHM=" },