From c6f76bf5069740f565d05a3ca39c98630e23aa05 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Thu, 3 Oct 2024 16:38:00 -0300 Subject: [PATCH 1/9] feat: initializing WooCommerce app development --- deco.ts | 1 + decohub/apps/woocommerce.ts | 1 + decohub/manifest.gen.ts | 6 +- woocommerce/README.md | 44 +++++++ .../loaders/product/productDetailsPage.ts | 38 ++++++ woocommerce/loaders/proxy.ts | 55 ++++++++ woocommerce/logo.png | Bin 0 -> 15653 bytes woocommerce/manifest.gen.ts | 19 +++ woocommerce/mod.ts | 96 ++++++++++++++ woocommerce/preview/Preview.tsx | 28 +++++ woocommerce/runtime.ts | 3 + woocommerce/utils/client.ts | 46 +++++++ woocommerce/utils/getAuthValue.ts | 4 + woocommerce/utils/transform.ts | 56 +++++++++ woocommerce/utils/types.ts | 117 ++++++++++++++++++ 15 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 decohub/apps/woocommerce.ts create mode 100644 woocommerce/README.md create mode 100644 woocommerce/loaders/product/productDetailsPage.ts create mode 100644 woocommerce/loaders/proxy.ts create mode 100644 woocommerce/logo.png create mode 100644 woocommerce/manifest.gen.ts create mode 100644 woocommerce/mod.ts create mode 100644 woocommerce/preview/Preview.tsx create mode 100644 woocommerce/runtime.ts create mode 100644 woocommerce/utils/client.ts create mode 100644 woocommerce/utils/getAuthValue.ts create mode 100644 woocommerce/utils/transform.ts create mode 100644 woocommerce/utils/types.ts diff --git a/deco.ts b/deco.ts index 48947b7b6..88e97e207 100644 --- a/deco.ts +++ b/deco.ts @@ -47,6 +47,7 @@ const config = { app("decohub"), app("htmx"), app("sap"), + app("woocommerce"), ...compatibilityApps, ], }; diff --git a/decohub/apps/woocommerce.ts b/decohub/apps/woocommerce.ts new file mode 100644 index 000000000..d1df539ba --- /dev/null +++ b/decohub/apps/woocommerce.ts @@ -0,0 +1 @@ +export { default, preview } from "../../woocommerce/mod.ts"; diff --git a/decohub/manifest.gen.ts b/decohub/manifest.gen.ts index 4d3ddbd57..81d9556bb 100644 --- a/decohub/manifest.gen.ts +++ b/decohub/manifest.gen.ts @@ -31,7 +31,8 @@ import * as $$$$$$$$$$$25 from "./apps/vtex.ts"; import * as $$$$$$$$$$$26 from "./apps/wake.ts"; import * as $$$$$$$$$$$27 from "./apps/wap.ts"; import * as $$$$$$$$$$$28 from "./apps/weather.ts"; -import * as $$$$$$$$$$$29 from "./apps/workflows.ts"; +import * as $$$$$$$$$$$29 from "./apps/woocommerce.ts"; +import * as $$$$$$$$$$$30 from "./apps/workflows.ts"; const manifest = { "apps": { @@ -64,7 +65,8 @@ const manifest = { "decohub/apps/wake.ts": $$$$$$$$$$$26, "decohub/apps/wap.ts": $$$$$$$$$$$27, "decohub/apps/weather.ts": $$$$$$$$$$$28, - "decohub/apps/workflows.ts": $$$$$$$$$$$29, + "decohub/apps/woocommerce.ts": $$$$$$$$$$$29, + "decohub/apps/workflows.ts": $$$$$$$$$$$30, }, "name": "decohub", "baseUrl": import.meta.url, diff --git a/woocommerce/README.md b/woocommerce/README.md new file mode 100644 index 000000000..50346a872 --- /dev/null +++ b/woocommerce/README.md @@ -0,0 +1,44 @@ +

+ + WooCommerce Integration for Your E-Commerce Solution + +

+

+ Loaders, actions, and workflows for integrating WooCommerce into your deco.cx website. +

+ +

+WooCommerce is a powerful, customizable e-commerce platform built on WordPress. It provides a robust set of tools and services for businesses to establish and manage their online stores with ease. + +This app wraps the WooCommerce API into a comprehensive set of loaders/actions/workflows, +enabling non-technical users to interact with and manage their headless commerce solution efficiently. +

+ +# Installation + +1. Install via DecoHub. +2. Complete the required fields: + + 1. **Store URL**: Enter the URL of your WooCommerce store. For example, if your store is accessible at www.store.com, make sure to use this URL. + 2. **Consumer Key**: Obtain this from the WooCommerce settings. [Follow this guide](https://woocommerce.com/document/woocommerce-rest-api/) for instructions on generating your API keys. + 3. **Consumer Secret**: Obtain this alongside the Consumer Key from WooCommerce. + +Optional Step: To use a custom search engine (such as Algolia or Typesense), you will need to provide the API Key and API Token. [Refer to this guide](https://woocommerce.com/document/woocommerce-rest-api/#authentication) for generating these credentials. + +Configure WooCommerce to send updates to this app by adding Deco as an affiliate. To do this, [follow this guide](https://woocommerce.com/document/woocommerce-rest-api/#authentication) and use the following endpoint as the notification URL: +- `https://{account}.deco.site/live/invoke/woocommerce/actions/trigger.ts` + +Configure the event listener in Deco. To do this: +- Open Blocks > Workflows +- Create a new instance of `events.ts` by clicking on `+` +- Name the block `woocommerce-trigger`. Note that this name is crucial and should not be altered. + +🎉 Your WooCommerce setup is complete! You should now see WooCommerce loaders/actions/workflows available for your sections. + +If you wish to index WooCommerce's product data into Deco, click the button below. Please be aware that this is a resource-intensive operation and may impact your page views quota. +
+
+ + +
+
diff --git a/woocommerce/loaders/product/productDetailsPage.ts b/woocommerce/loaders/product/productDetailsPage.ts new file mode 100644 index 000000000..1ede68c98 --- /dev/null +++ b/woocommerce/loaders/product/productDetailsPage.ts @@ -0,0 +1,38 @@ +import { RequestURLParam } from "../../../website/functions/requestToParam.ts"; +import type { ProductDetailsPage } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toBreadcrumbList, toProduct } from "../../utils/transform.ts"; + +export interface Props { + slug: RequestURLParam; +} + +async function loader( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + const { slug } = props; + const { api } = ctx; + + if (!slug) return null; + + const [product] = await api["GET /wc/v3/products"]({ + slug, + }).then((res) => res.json()); + + if (!product) return null; + + return { + "@type": "ProductDetailsPage", + product: toProduct(product), + breadcrumbList: toBreadcrumbList(product.categories), + seo: { + title: product.name, + description: product.short_description || product.description, + canonical: product.slug, + }, + }; +} + +export default loader; diff --git a/woocommerce/loaders/proxy.ts b/woocommerce/loaders/proxy.ts new file mode 100644 index 000000000..950ad80b9 --- /dev/null +++ b/woocommerce/loaders/proxy.ts @@ -0,0 +1,55 @@ +import { Route } from "../../website/flags/audience.ts"; +import { AppContext } from "../mod.ts"; + +const DEFAULT_PROXY_PATHS = [ + "/checkout", + "/checkout*", + "/cart", + "/cart*", + "/my-account", + "/my-account/*", + "/?wc-ajax*", + "/wp-content", + "/wp-content/*", + "/wp-includes", + "/wp-includes/*", + "/wc*", + "/wp*", +]; + +export interface Props { + /** @description ex: /p/fale-conosco */ + pagesToProxy?: string[]; +} + +/** + * @title WooCommerce Proxy Routes + */ +function loader( + { pagesToProxy = [] }: Props, + _req: Request, + ctx: AppContext, +): Route[] { + const { publicUrl } = ctx; + + const proxy = [ + ...DEFAULT_PROXY_PATHS, + ...pagesToProxy, + ].map((pathTemplate) => ({ + pathTemplate, + handler: { + value: { + __resolveType: "website/handlers/proxy.ts", + url: publicUrl, + redirect: "follow", + customHeaders: [{ + Host: publicUrl, + }], + }, + }, + })); + + return proxy; +} + +export default loader; diff --git a/woocommerce/logo.png b/woocommerce/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a4be0dbe267a64966a9aace0f681d6dfeeb94368 GIT binary patch literal 15653 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 literal 0 HcmV?d00001 diff --git a/woocommerce/manifest.gen.ts b/woocommerce/manifest.gen.ts new file mode 100644 index 000000000..b5d3f62f4 --- /dev/null +++ b/woocommerce/manifest.gen.ts @@ -0,0 +1,19 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$0 from "./loaders/product/productDetailsPage.ts"; +import * as $$$1 from "./loaders/proxy.ts"; + +const manifest = { + "loaders": { + "woocommerce/loaders/product/productDetailsPage.ts": $$$0, + "woocommerce/loaders/proxy.ts": $$$1, + }, + "name": "woocommerce", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/woocommerce/mod.ts b/woocommerce/mod.ts new file mode 100644 index 000000000..cfb6fbae6 --- /dev/null +++ b/woocommerce/mod.ts @@ -0,0 +1,96 @@ +import type { + App as A, + AppContext as AC, + AppRuntime as AR +} from "@deco/deco"; +import workflow from "../workflows/mod.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { WooCommerceAPI } from "./utils/client.ts"; +import type { Secret } from "../website/loaders/secret.ts"; + +import { Markdown } from "../decohub/components/Markdown.tsx"; +import { createHttpClient } from "../utils/http.ts"; +import { fetchSafe } from "../utils/fetch.ts"; +import { PreviewWooCommerce } from "./preview/Preview.tsx"; +import { getAuthValue } from "./utils/getAuthValue.ts"; + +export type App = ReturnType; +export type AppContext = AC; + +/** @title WooCommerce */ +export interface Props { + /** + * @title Public store URL + * @description Domain that is registered on License Manager (e.g: www.mystore.com.br) + */ + publicUrl: string; + /** + * @title Customer Key + */ + customer_key?: Secret; + /** + * @title Customer Secret + */ + customer_secret?: Secret; + /** + * @description Use WooCommerce as backend platform + */ + platform: "WooCommerce"; +} + +export const color = 0x800080; + +/** + * @title WooCommerce + * @description Loaders, actions and workflows for adding WooCommerce Commerce Platform to your website. + * @category Ecommmerce + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/woocommerce/logo.png + */ +export default function WooCommerce( + { customer_key, customer_secret, publicUrl }: Props, +) { + const ck = getAuthValue(customer_key); + const cs = getAuthValue(customer_secret); + + const createBasicAuth = (key: string, secret: string) => + btoa(`${encodeURIComponent(key)}:${encodeURIComponent(secret)}`); + + const auth = createBasicAuth(ck, cs); + + const headers = new Headers(); + headers.set("accept", "application/json"); + headers.set("Authorization", `Basic ${auth}`); + headers.set("content-type", "application/json"); + + const api = createHttpClient({ + base: `${publicUrl}/wp-json/`, + fetcher: fetchSafe, + headers, + }); + + const state = { + api, + publicUrl, + }; + + const app: A]> = { + state, + manifest, + dependencies: [workflow({})], + }; + + return app; +} + +export const preview = async (props: AR) => { + const markdownContent = await Markdown( + new URL("./README.md", import.meta.url).href, + ); + return { + Component: PreviewWooCommerce, + props: { + ...props, + markdownContent, + }, + }; +}; diff --git a/woocommerce/preview/Preview.tsx b/woocommerce/preview/Preview.tsx new file mode 100644 index 000000000..98a3cb4b0 --- /dev/null +++ b/woocommerce/preview/Preview.tsx @@ -0,0 +1,28 @@ +import type { JSX } from "preact"; +import { PreviewContainer } from "../../utils/preview.tsx"; +import { App } from "../mod.ts"; +import { type AppRuntime, type BaseContext, Context } from "@deco/deco"; + +export interface Props { + publicUrl: string; +} + +export const PreviewWooCommerce = ( + app: AppRuntime & { + markdownContent: () => JSX.Element; + }, +) => { + const context = Context.active(); + const _decoSite = context.site; + const _publicUrl = app.state?.publicUrl || ""; + + return ( + + ); +}; diff --git a/woocommerce/runtime.ts b/woocommerce/runtime.ts new file mode 100644 index 000000000..da42a2435 --- /dev/null +++ b/woocommerce/runtime.ts @@ -0,0 +1,3 @@ +import { Manifest } from "./manifest.gen.ts"; +import { proxy } from "@deco/deco/web"; +export const invoke = proxy(); diff --git a/woocommerce/utils/client.ts b/woocommerce/utils/client.ts new file mode 100644 index 000000000..77f739ad7 --- /dev/null +++ b/woocommerce/utils/client.ts @@ -0,0 +1,46 @@ +import { BaseProduct, Category, OrderBy } from "./types.ts"; + +export interface WooCommerceAPI { + "GET /wc/v3/products": { + response: BaseProduct[]; + searchParams: { + search?: string; + order?: "asc" | "desc"; + orderby?: OrderBy; + per_page?: number; + page?: number; + slug?: string; + parent?: string; + parent_exclude?: string[]; + status?: "any" | "draft" | "pending" | "private" | "publish"; + stock_status?: "instock" | "outofstock" | "onbackorder"; + type?: "simple" | "grouped" | "external" | "variable"; + featured?: boolean; + tag?: string; + sku?: string; + category?: string; + attribute?: string; + attribute_term?: string; + include?: string[]; + exclude?: string[]; + min_price?: string; + max_price?: string; + }; + }; + "GET /wc/v3/products/categories": { + response: Category[]; + searchParams: { + page?: number; + per_page?: number; + order?: "asc" | "desc"; + orderby?: OrderBy; + hide_empty?: boolean; + parent?: number; + search?: string; + include?: string[]; + exclude?: string[]; + product?: number; + slug?: string; + }; + }; +} diff --git a/woocommerce/utils/getAuthValue.ts b/woocommerce/utils/getAuthValue.ts new file mode 100644 index 000000000..623560890 --- /dev/null +++ b/woocommerce/utils/getAuthValue.ts @@ -0,0 +1,4 @@ +import { Secret } from "../../website/loaders/secret.ts"; + +export const getAuthValue = (key?: Secret) => + typeof key === "string" ? key : key?.get?.() ?? ""; \ No newline at end of file diff --git a/woocommerce/utils/transform.ts b/woocommerce/utils/transform.ts new file mode 100644 index 000000000..17a49fed7 --- /dev/null +++ b/woocommerce/utils/transform.ts @@ -0,0 +1,56 @@ +import { BreadcrumbList, ImageObject, Product } from "../../commerce/types.ts"; +import { BaseProduct, WCImage } from "./types.ts"; + +export const toProduct = ( + product: BaseProduct, +): Product => { + return { + "@type": "Product", + "@id": String(product.id), + productID: String(product.id), + sku: product.sku, + name: product.name, + url: product.slug, + description: product.description, + image: toImage(product.images), + offers: { + "@type": "AggregateOffer", + highPrice: Number(product.regular_price || product.price), + lowPrice: Number(product.sale_price), + offerCount: 0, + offers: [], + }, + additionalProperty: product.categories.map((category) => ({ + "@type": "PropertyValue", + name: category.name, + url: category.slug, + })), + }; +}; + +export const toBreadcrumbList = ( + categories: BaseProduct["categories"], +): BreadcrumbList => { + return { + "@type": "BreadcrumbList" as const, + numberOfItems: categories.length, + itemListElement: categories.map((category, index) => ({ + "@type": "ListItem" as const, + name: category.name, + position: index + 1, + item: new URL(category.slug).href, + })), + }; +}; + +export const toImage = ( + images: WCImage[] +): ImageObject[] => { + return images.map((image) => ({ + "@type": "ImageObject", + alternateName: image.alt, + url: image.src, + contentUrl: image.src, + description: image.alt, + })); +}; diff --git a/woocommerce/utils/types.ts b/woocommerce/utils/types.ts new file mode 100644 index 000000000..e31bc246c --- /dev/null +++ b/woocommerce/utils/types.ts @@ -0,0 +1,117 @@ +export type OrderBy = + | "price" + | "rating" + | "popularity" + | "date" + | "modified" + | "title" + | "slug" + | "date"; + +export interface BaseProduct { + id: number; + name: string; + slug: string; + permalink: string; + date_created: Date; + date_created_gmt: Date; + date_modified: Date; + date_modified_gmt: Date; + type: string; + status: string; + featured: boolean; + catalog_visibility: string; + description: string; + short_description: string; + sku: string; + price: string; + regular_price: string; + sale_price: string; + date_on_sale_from: null; + date_on_sale_from_gmt: null; + date_on_sale_to: null; + date_on_sale_to_gmt: null; + price_html: string; + on_sale: boolean; + purchasable: boolean; + total_sales: number; + virtual: boolean; + downloadable: boolean; + downloads: string[]; + download_limit: number; + download_expiry: number; + external_url: string; + button_text: string; + tax_status: string; + tax_class: string; + manage_stock: boolean; + stock_quantity: null; + stock_status: string; + backorders: string; + backorders_allowed: boolean; + backordered: boolean; + sold_individually: boolean; + weight: string; + dimensions: Dimensions; + shipping_required: boolean; + shipping_taxable: boolean; + shipping_class: string; + shipping_class_id: number; + reviews_allowed: boolean; + average_rating: string; + rating_count: number; + related_ids: number[]; + upsell_ids: string[]; + cross_sell_ids: string[]; + parent_id: number; + purchase_note: string; + categories: Pick[]; + tags: string[]; + images: WCImage[]; + attributes: string[]; + default_attributes: string[]; + variations: string[]; + grouped_products: unknown[]; + menu_order: number; + meta_data: unknown[]; + _links: Links; +} + +export interface Links { + self: Collection[]; + collection: Collection[]; +} + +export interface Collection { + href: string; +} + +export interface Category { + id: number; + name: string; + slug: string; + parent: number; + description: string; + display: string; + image: WCImage; + menu_order: number; + count: number; + _links: Links; +} + +export interface Dimensions { + length: string; + width: string; + height: string; +} + +export interface WCImage { + id: number; + date_created: Date; + date_created_gmt: Date; + date_modified: Date; + date_modified_gmt: Date; + src: string; + name: string; + alt: string; +} From aa5258485d6fa0b4d4c2b6a7c3353286ee2367dd Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Thu, 3 Oct 2024 16:38:37 -0300 Subject: [PATCH 2/9] chore(woocommerce): fmt --- woocommerce/mod.ts | 10 +++------- woocommerce/utils/getAuthValue.ts | 4 ++-- woocommerce/utils/transform.ts | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/woocommerce/mod.ts b/woocommerce/mod.ts index cfb6fbae6..4b339df02 100644 --- a/woocommerce/mod.ts +++ b/woocommerce/mod.ts @@ -1,8 +1,4 @@ -import type { - App as A, - AppContext as AC, - AppRuntime as AR -} from "@deco/deco"; +import type { App as A, AppContext as AC, AppRuntime as AR } from "@deco/deco"; import workflow from "../workflows/mod.ts"; import manifest, { Manifest } from "./manifest.gen.ts"; import { WooCommerceAPI } from "./utils/client.ts"; @@ -52,9 +48,9 @@ export default function WooCommerce( const ck = getAuthValue(customer_key); const cs = getAuthValue(customer_secret); - const createBasicAuth = (key: string, secret: string) => + const createBasicAuth = (key: string, secret: string) => btoa(`${encodeURIComponent(key)}:${encodeURIComponent(secret)}`); - + const auth = createBasicAuth(ck, cs); const headers = new Headers(); diff --git a/woocommerce/utils/getAuthValue.ts b/woocommerce/utils/getAuthValue.ts index 623560890..1273bca61 100644 --- a/woocommerce/utils/getAuthValue.ts +++ b/woocommerce/utils/getAuthValue.ts @@ -1,4 +1,4 @@ import { Secret } from "../../website/loaders/secret.ts"; -export const getAuthValue = (key?: Secret) => - typeof key === "string" ? key : key?.get?.() ?? ""; \ No newline at end of file +export const getAuthValue = (key?: Secret) => + typeof key === "string" ? key : key?.get?.() ?? ""; diff --git a/woocommerce/utils/transform.ts b/woocommerce/utils/transform.ts index 17a49fed7..eaa04b0d0 100644 --- a/woocommerce/utils/transform.ts +++ b/woocommerce/utils/transform.ts @@ -44,7 +44,7 @@ export const toBreadcrumbList = ( }; export const toImage = ( - images: WCImage[] + images: WCImage[], ): ImageObject[] => { return images.map((image) => ({ "@type": "ImageObject", From 91b3b7100297f70703f06bd3d9d88e67ae7a3133 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Sat, 5 Oct 2024 00:28:10 -0300 Subject: [PATCH 3/9] feat(woocomerce): adds productList loader --- .../loaders/product/productDetailsPage.ts | 4 + woocommerce/loaders/product/productList.ts | 88 +++++++++++++++++++ woocommerce/manifest.gen.ts | 6 +- woocommerce/utils/client.ts | 15 +++- woocommerce/utils/types.ts | 15 +++- 5 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 woocommerce/loaders/product/productList.ts diff --git a/woocommerce/loaders/product/productDetailsPage.ts b/woocommerce/loaders/product/productDetailsPage.ts index 1ede68c98..615bb1ee3 100644 --- a/woocommerce/loaders/product/productDetailsPage.ts +++ b/woocommerce/loaders/product/productDetailsPage.ts @@ -7,6 +7,10 @@ export interface Props { slug: RequestURLParam; } +/** + * @title WooCommerce Integration + * @description Product Details Page loader + */ async function loader( props: Props, _req: Request, diff --git a/woocommerce/loaders/product/productList.ts b/woocommerce/loaders/product/productList.ts new file mode 100644 index 000000000..b2f8fa367 --- /dev/null +++ b/woocommerce/loaders/product/productList.ts @@ -0,0 +1,88 @@ +import { Product } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toProduct } from "../../utils/transform.ts"; +import { Order, OrderBy, Status, StockStatus } from "../../utils/types.ts"; + +export interface SearchProps { + search: string; +} + +export interface ProductIDProps { + ids: string[]; +} + +export interface Props { + props: SearchProps | ProductIDProps; + /** + * @title Per Page + * @default 10 + * @description Maximum number of items to be returned in result set. Default is 10. + */ + per_page?: number; + /** + * @description Order sort attribute ascending or descending. Default is desc. + */ + order?: Order; + /** + * @title Order By + * @description Sort collection by object attribute. Default is date. + */ + orderby?: OrderBy; + /** + * @title Status + * @description Limit result set to products assigned a specific status. Options: any, draft, pending, private and publish. Default is any. + */ + status?: Status; + /** + * @title Stock Status + * @description Limit result set to products with specified stock status. Default: instock. + */ + stock_status?: StockStatus; + /** + * @title Exclude IDs + * @description Ensure result set excludes specific IDs. + */ + exclude?: string[]; +} + +async function loader( + p: Props, + _req: Request, + ctx: AppContext, +): Promise { + const { api } = ctx; + + const props = p.props ?? + (p as unknown as Props["props"]); + + let products; + + const queryParams: Omit = { + order: p.order ?? "desc", + orderby: p.orderby ?? "date", + status: p.status ?? "any", + stock_status: p.stock_status ?? "instock", + per_page: p.per_page, + exclude: p.exclude, + }; + + if ("search" in props) { + products = await api["GET /wc/v3/products"]({ + search: props.search, + ...queryParams, + }).then((res) => res.json()); + } + + if ("ids" in props) { + products = await api["GET /wc/v3/products"]({ + include: props.ids, + ...queryParams, + }).then((res) => res.json()); + } + + if (!products) return null; + + return products.map((product) => toProduct(product)); +} + +export default loader; diff --git a/woocommerce/manifest.gen.ts b/woocommerce/manifest.gen.ts index b5d3f62f4..e6462f273 100644 --- a/woocommerce/manifest.gen.ts +++ b/woocommerce/manifest.gen.ts @@ -3,12 +3,14 @@ // This file is automatically updated during development when running `dev.ts`. import * as $$$0 from "./loaders/product/productDetailsPage.ts"; -import * as $$$1 from "./loaders/proxy.ts"; +import * as $$$1 from "./loaders/product/productList.ts"; +import * as $$$2 from "./loaders/proxy.ts"; const manifest = { "loaders": { "woocommerce/loaders/product/productDetailsPage.ts": $$$0, - "woocommerce/loaders/proxy.ts": $$$1, + "woocommerce/loaders/product/productList.ts": $$$1, + "woocommerce/loaders/proxy.ts": $$$2, }, "name": "woocommerce", "baseUrl": import.meta.url, diff --git a/woocommerce/utils/client.ts b/woocommerce/utils/client.ts index 77f739ad7..8cdad0016 100644 --- a/woocommerce/utils/client.ts +++ b/woocommerce/utils/client.ts @@ -1,4 +1,11 @@ -import { BaseProduct, Category, OrderBy } from "./types.ts"; +import { + BaseProduct, + Category, + Order, + OrderBy, + Status, + StockStatus, +} from "./types.ts"; export interface WooCommerceAPI { "GET /wc/v3/products": { @@ -12,8 +19,8 @@ export interface WooCommerceAPI { slug?: string; parent?: string; parent_exclude?: string[]; - status?: "any" | "draft" | "pending" | "private" | "publish"; - stock_status?: "instock" | "outofstock" | "onbackorder"; + status?: Status; + stock_status?: StockStatus; type?: "simple" | "grouped" | "external" | "variable"; featured?: boolean; tag?: string; @@ -32,7 +39,7 @@ export interface WooCommerceAPI { searchParams: { page?: number; per_page?: number; - order?: "asc" | "desc"; + order?: Order; orderby?: OrderBy; hide_empty?: boolean; parent?: number; diff --git a/woocommerce/utils/types.ts b/woocommerce/utils/types.ts index e31bc246c..60bc240b4 100644 --- a/woocommerce/utils/types.ts +++ b/woocommerce/utils/types.ts @@ -6,7 +6,20 @@ export type OrderBy = | "modified" | "title" | "slug" - | "date"; + | "date" + | "id" + | "menu_order"; + +export type Status = + | "any" + | "draft" + | "pending" + | "private" + | "publish"; + +export type Order = "asc" | "desc"; + +export type StockStatus = "instock" | "outofstock" | "onbackorder"; export interface BaseProduct { id: number; From 407d22a56d597d1fa9ef52a932ee0e5f36041c13 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Thu, 3 Oct 2024 16:38:00 -0300 Subject: [PATCH 4/9] feat: initializing WooCommerce app development --- deco.ts | 1 + decohub/apps/woocommerce.ts | 1 + decohub/manifest.gen.ts | 6 +- woocommerce/README.md | 44 +++++++ .../loaders/product/productDetailsPage.ts | 38 ++++++ woocommerce/loaders/proxy.ts | 55 ++++++++ woocommerce/logo.png | Bin 0 -> 15653 bytes woocommerce/manifest.gen.ts | 19 +++ woocommerce/mod.ts | 96 ++++++++++++++ woocommerce/preview/Preview.tsx | 28 +++++ woocommerce/runtime.ts | 3 + woocommerce/utils/client.ts | 46 +++++++ woocommerce/utils/getAuthValue.ts | 4 + woocommerce/utils/transform.ts | 56 +++++++++ woocommerce/utils/types.ts | 117 ++++++++++++++++++ 15 files changed, 512 insertions(+), 2 deletions(-) create mode 100644 decohub/apps/woocommerce.ts create mode 100644 woocommerce/README.md create mode 100644 woocommerce/loaders/product/productDetailsPage.ts create mode 100644 woocommerce/loaders/proxy.ts create mode 100644 woocommerce/logo.png create mode 100644 woocommerce/manifest.gen.ts create mode 100644 woocommerce/mod.ts create mode 100644 woocommerce/preview/Preview.tsx create mode 100644 woocommerce/runtime.ts create mode 100644 woocommerce/utils/client.ts create mode 100644 woocommerce/utils/getAuthValue.ts create mode 100644 woocommerce/utils/transform.ts create mode 100644 woocommerce/utils/types.ts diff --git a/deco.ts b/deco.ts index 48947b7b6..88e97e207 100644 --- a/deco.ts +++ b/deco.ts @@ -47,6 +47,7 @@ const config = { app("decohub"), app("htmx"), app("sap"), + app("woocommerce"), ...compatibilityApps, ], }; diff --git a/decohub/apps/woocommerce.ts b/decohub/apps/woocommerce.ts new file mode 100644 index 000000000..d1df539ba --- /dev/null +++ b/decohub/apps/woocommerce.ts @@ -0,0 +1 @@ +export { default, preview } from "../../woocommerce/mod.ts"; diff --git a/decohub/manifest.gen.ts b/decohub/manifest.gen.ts index 4d3ddbd57..81d9556bb 100644 --- a/decohub/manifest.gen.ts +++ b/decohub/manifest.gen.ts @@ -31,7 +31,8 @@ import * as $$$$$$$$$$$25 from "./apps/vtex.ts"; import * as $$$$$$$$$$$26 from "./apps/wake.ts"; import * as $$$$$$$$$$$27 from "./apps/wap.ts"; import * as $$$$$$$$$$$28 from "./apps/weather.ts"; -import * as $$$$$$$$$$$29 from "./apps/workflows.ts"; +import * as $$$$$$$$$$$29 from "./apps/woocommerce.ts"; +import * as $$$$$$$$$$$30 from "./apps/workflows.ts"; const manifest = { "apps": { @@ -64,7 +65,8 @@ const manifest = { "decohub/apps/wake.ts": $$$$$$$$$$$26, "decohub/apps/wap.ts": $$$$$$$$$$$27, "decohub/apps/weather.ts": $$$$$$$$$$$28, - "decohub/apps/workflows.ts": $$$$$$$$$$$29, + "decohub/apps/woocommerce.ts": $$$$$$$$$$$29, + "decohub/apps/workflows.ts": $$$$$$$$$$$30, }, "name": "decohub", "baseUrl": import.meta.url, diff --git a/woocommerce/README.md b/woocommerce/README.md new file mode 100644 index 000000000..50346a872 --- /dev/null +++ b/woocommerce/README.md @@ -0,0 +1,44 @@ +

+ + WooCommerce Integration for Your E-Commerce Solution + +

+

+ Loaders, actions, and workflows for integrating WooCommerce into your deco.cx website. +

+ +

+WooCommerce is a powerful, customizable e-commerce platform built on WordPress. It provides a robust set of tools and services for businesses to establish and manage their online stores with ease. + +This app wraps the WooCommerce API into a comprehensive set of loaders/actions/workflows, +enabling non-technical users to interact with and manage their headless commerce solution efficiently. +

+ +# Installation + +1. Install via DecoHub. +2. Complete the required fields: + + 1. **Store URL**: Enter the URL of your WooCommerce store. For example, if your store is accessible at www.store.com, make sure to use this URL. + 2. **Consumer Key**: Obtain this from the WooCommerce settings. [Follow this guide](https://woocommerce.com/document/woocommerce-rest-api/) for instructions on generating your API keys. + 3. **Consumer Secret**: Obtain this alongside the Consumer Key from WooCommerce. + +Optional Step: To use a custom search engine (such as Algolia or Typesense), you will need to provide the API Key and API Token. [Refer to this guide](https://woocommerce.com/document/woocommerce-rest-api/#authentication) for generating these credentials. + +Configure WooCommerce to send updates to this app by adding Deco as an affiliate. To do this, [follow this guide](https://woocommerce.com/document/woocommerce-rest-api/#authentication) and use the following endpoint as the notification URL: +- `https://{account}.deco.site/live/invoke/woocommerce/actions/trigger.ts` + +Configure the event listener in Deco. To do this: +- Open Blocks > Workflows +- Create a new instance of `events.ts` by clicking on `+` +- Name the block `woocommerce-trigger`. Note that this name is crucial and should not be altered. + +🎉 Your WooCommerce setup is complete! You should now see WooCommerce loaders/actions/workflows available for your sections. + +If you wish to index WooCommerce's product data into Deco, click the button below. Please be aware that this is a resource-intensive operation and may impact your page views quota. +
+
+ + +
+
diff --git a/woocommerce/loaders/product/productDetailsPage.ts b/woocommerce/loaders/product/productDetailsPage.ts new file mode 100644 index 000000000..1ede68c98 --- /dev/null +++ b/woocommerce/loaders/product/productDetailsPage.ts @@ -0,0 +1,38 @@ +import { RequestURLParam } from "../../../website/functions/requestToParam.ts"; +import type { ProductDetailsPage } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toBreadcrumbList, toProduct } from "../../utils/transform.ts"; + +export interface Props { + slug: RequestURLParam; +} + +async function loader( + props: Props, + _req: Request, + ctx: AppContext, +): Promise { + const { slug } = props; + const { api } = ctx; + + if (!slug) return null; + + const [product] = await api["GET /wc/v3/products"]({ + slug, + }).then((res) => res.json()); + + if (!product) return null; + + return { + "@type": "ProductDetailsPage", + product: toProduct(product), + breadcrumbList: toBreadcrumbList(product.categories), + seo: { + title: product.name, + description: product.short_description || product.description, + canonical: product.slug, + }, + }; +} + +export default loader; diff --git a/woocommerce/loaders/proxy.ts b/woocommerce/loaders/proxy.ts new file mode 100644 index 000000000..950ad80b9 --- /dev/null +++ b/woocommerce/loaders/proxy.ts @@ -0,0 +1,55 @@ +import { Route } from "../../website/flags/audience.ts"; +import { AppContext } from "../mod.ts"; + +const DEFAULT_PROXY_PATHS = [ + "/checkout", + "/checkout*", + "/cart", + "/cart*", + "/my-account", + "/my-account/*", + "/?wc-ajax*", + "/wp-content", + "/wp-content/*", + "/wp-includes", + "/wp-includes/*", + "/wc*", + "/wp*", +]; + +export interface Props { + /** @description ex: /p/fale-conosco */ + pagesToProxy?: string[]; +} + +/** + * @title WooCommerce Proxy Routes + */ +function loader( + { pagesToProxy = [] }: Props, + _req: Request, + ctx: AppContext, +): Route[] { + const { publicUrl } = ctx; + + const proxy = [ + ...DEFAULT_PROXY_PATHS, + ...pagesToProxy, + ].map((pathTemplate) => ({ + pathTemplate, + handler: { + value: { + __resolveType: "website/handlers/proxy.ts", + url: publicUrl, + redirect: "follow", + customHeaders: [{ + Host: publicUrl, + }], + }, + }, + })); + + return proxy; +} + +export default loader; diff --git a/woocommerce/logo.png b/woocommerce/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a4be0dbe267a64966a9aace0f681d6dfeeb94368 GIT binary patch literal 15653 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 literal 0 HcmV?d00001 diff --git a/woocommerce/manifest.gen.ts b/woocommerce/manifest.gen.ts new file mode 100644 index 000000000..b5d3f62f4 --- /dev/null +++ b/woocommerce/manifest.gen.ts @@ -0,0 +1,19 @@ +// DO NOT EDIT. This file is generated by deco. +// This file SHOULD be checked into source version control. +// This file is automatically updated during development when running `dev.ts`. + +import * as $$$0 from "./loaders/product/productDetailsPage.ts"; +import * as $$$1 from "./loaders/proxy.ts"; + +const manifest = { + "loaders": { + "woocommerce/loaders/product/productDetailsPage.ts": $$$0, + "woocommerce/loaders/proxy.ts": $$$1, + }, + "name": "woocommerce", + "baseUrl": import.meta.url, +}; + +export type Manifest = typeof manifest; + +export default manifest; diff --git a/woocommerce/mod.ts b/woocommerce/mod.ts new file mode 100644 index 000000000..cfb6fbae6 --- /dev/null +++ b/woocommerce/mod.ts @@ -0,0 +1,96 @@ +import type { + App as A, + AppContext as AC, + AppRuntime as AR +} from "@deco/deco"; +import workflow from "../workflows/mod.ts"; +import manifest, { Manifest } from "./manifest.gen.ts"; +import { WooCommerceAPI } from "./utils/client.ts"; +import type { Secret } from "../website/loaders/secret.ts"; + +import { Markdown } from "../decohub/components/Markdown.tsx"; +import { createHttpClient } from "../utils/http.ts"; +import { fetchSafe } from "../utils/fetch.ts"; +import { PreviewWooCommerce } from "./preview/Preview.tsx"; +import { getAuthValue } from "./utils/getAuthValue.ts"; + +export type App = ReturnType; +export type AppContext = AC; + +/** @title WooCommerce */ +export interface Props { + /** + * @title Public store URL + * @description Domain that is registered on License Manager (e.g: www.mystore.com.br) + */ + publicUrl: string; + /** + * @title Customer Key + */ + customer_key?: Secret; + /** + * @title Customer Secret + */ + customer_secret?: Secret; + /** + * @description Use WooCommerce as backend platform + */ + platform: "WooCommerce"; +} + +export const color = 0x800080; + +/** + * @title WooCommerce + * @description Loaders, actions and workflows for adding WooCommerce Commerce Platform to your website. + * @category Ecommmerce + * @logo https://raw.githubusercontent.com/deco-cx/apps/main/woocommerce/logo.png + */ +export default function WooCommerce( + { customer_key, customer_secret, publicUrl }: Props, +) { + const ck = getAuthValue(customer_key); + const cs = getAuthValue(customer_secret); + + const createBasicAuth = (key: string, secret: string) => + btoa(`${encodeURIComponent(key)}:${encodeURIComponent(secret)}`); + + const auth = createBasicAuth(ck, cs); + + const headers = new Headers(); + headers.set("accept", "application/json"); + headers.set("Authorization", `Basic ${auth}`); + headers.set("content-type", "application/json"); + + const api = createHttpClient({ + base: `${publicUrl}/wp-json/`, + fetcher: fetchSafe, + headers, + }); + + const state = { + api, + publicUrl, + }; + + const app: A]> = { + state, + manifest, + dependencies: [workflow({})], + }; + + return app; +} + +export const preview = async (props: AR) => { + const markdownContent = await Markdown( + new URL("./README.md", import.meta.url).href, + ); + return { + Component: PreviewWooCommerce, + props: { + ...props, + markdownContent, + }, + }; +}; diff --git a/woocommerce/preview/Preview.tsx b/woocommerce/preview/Preview.tsx new file mode 100644 index 000000000..98a3cb4b0 --- /dev/null +++ b/woocommerce/preview/Preview.tsx @@ -0,0 +1,28 @@ +import type { JSX } from "preact"; +import { PreviewContainer } from "../../utils/preview.tsx"; +import { App } from "../mod.ts"; +import { type AppRuntime, type BaseContext, Context } from "@deco/deco"; + +export interface Props { + publicUrl: string; +} + +export const PreviewWooCommerce = ( + app: AppRuntime & { + markdownContent: () => JSX.Element; + }, +) => { + const context = Context.active(); + const _decoSite = context.site; + const _publicUrl = app.state?.publicUrl || ""; + + return ( + + ); +}; diff --git a/woocommerce/runtime.ts b/woocommerce/runtime.ts new file mode 100644 index 000000000..da42a2435 --- /dev/null +++ b/woocommerce/runtime.ts @@ -0,0 +1,3 @@ +import { Manifest } from "./manifest.gen.ts"; +import { proxy } from "@deco/deco/web"; +export const invoke = proxy(); diff --git a/woocommerce/utils/client.ts b/woocommerce/utils/client.ts new file mode 100644 index 000000000..77f739ad7 --- /dev/null +++ b/woocommerce/utils/client.ts @@ -0,0 +1,46 @@ +import { BaseProduct, Category, OrderBy } from "./types.ts"; + +export interface WooCommerceAPI { + "GET /wc/v3/products": { + response: BaseProduct[]; + searchParams: { + search?: string; + order?: "asc" | "desc"; + orderby?: OrderBy; + per_page?: number; + page?: number; + slug?: string; + parent?: string; + parent_exclude?: string[]; + status?: "any" | "draft" | "pending" | "private" | "publish"; + stock_status?: "instock" | "outofstock" | "onbackorder"; + type?: "simple" | "grouped" | "external" | "variable"; + featured?: boolean; + tag?: string; + sku?: string; + category?: string; + attribute?: string; + attribute_term?: string; + include?: string[]; + exclude?: string[]; + min_price?: string; + max_price?: string; + }; + }; + "GET /wc/v3/products/categories": { + response: Category[]; + searchParams: { + page?: number; + per_page?: number; + order?: "asc" | "desc"; + orderby?: OrderBy; + hide_empty?: boolean; + parent?: number; + search?: string; + include?: string[]; + exclude?: string[]; + product?: number; + slug?: string; + }; + }; +} diff --git a/woocommerce/utils/getAuthValue.ts b/woocommerce/utils/getAuthValue.ts new file mode 100644 index 000000000..623560890 --- /dev/null +++ b/woocommerce/utils/getAuthValue.ts @@ -0,0 +1,4 @@ +import { Secret } from "../../website/loaders/secret.ts"; + +export const getAuthValue = (key?: Secret) => + typeof key === "string" ? key : key?.get?.() ?? ""; \ No newline at end of file diff --git a/woocommerce/utils/transform.ts b/woocommerce/utils/transform.ts new file mode 100644 index 000000000..17a49fed7 --- /dev/null +++ b/woocommerce/utils/transform.ts @@ -0,0 +1,56 @@ +import { BreadcrumbList, ImageObject, Product } from "../../commerce/types.ts"; +import { BaseProduct, WCImage } from "./types.ts"; + +export const toProduct = ( + product: BaseProduct, +): Product => { + return { + "@type": "Product", + "@id": String(product.id), + productID: String(product.id), + sku: product.sku, + name: product.name, + url: product.slug, + description: product.description, + image: toImage(product.images), + offers: { + "@type": "AggregateOffer", + highPrice: Number(product.regular_price || product.price), + lowPrice: Number(product.sale_price), + offerCount: 0, + offers: [], + }, + additionalProperty: product.categories.map((category) => ({ + "@type": "PropertyValue", + name: category.name, + url: category.slug, + })), + }; +}; + +export const toBreadcrumbList = ( + categories: BaseProduct["categories"], +): BreadcrumbList => { + return { + "@type": "BreadcrumbList" as const, + numberOfItems: categories.length, + itemListElement: categories.map((category, index) => ({ + "@type": "ListItem" as const, + name: category.name, + position: index + 1, + item: new URL(category.slug).href, + })), + }; +}; + +export const toImage = ( + images: WCImage[] +): ImageObject[] => { + return images.map((image) => ({ + "@type": "ImageObject", + alternateName: image.alt, + url: image.src, + contentUrl: image.src, + description: image.alt, + })); +}; diff --git a/woocommerce/utils/types.ts b/woocommerce/utils/types.ts new file mode 100644 index 000000000..e31bc246c --- /dev/null +++ b/woocommerce/utils/types.ts @@ -0,0 +1,117 @@ +export type OrderBy = + | "price" + | "rating" + | "popularity" + | "date" + | "modified" + | "title" + | "slug" + | "date"; + +export interface BaseProduct { + id: number; + name: string; + slug: string; + permalink: string; + date_created: Date; + date_created_gmt: Date; + date_modified: Date; + date_modified_gmt: Date; + type: string; + status: string; + featured: boolean; + catalog_visibility: string; + description: string; + short_description: string; + sku: string; + price: string; + regular_price: string; + sale_price: string; + date_on_sale_from: null; + date_on_sale_from_gmt: null; + date_on_sale_to: null; + date_on_sale_to_gmt: null; + price_html: string; + on_sale: boolean; + purchasable: boolean; + total_sales: number; + virtual: boolean; + downloadable: boolean; + downloads: string[]; + download_limit: number; + download_expiry: number; + external_url: string; + button_text: string; + tax_status: string; + tax_class: string; + manage_stock: boolean; + stock_quantity: null; + stock_status: string; + backorders: string; + backorders_allowed: boolean; + backordered: boolean; + sold_individually: boolean; + weight: string; + dimensions: Dimensions; + shipping_required: boolean; + shipping_taxable: boolean; + shipping_class: string; + shipping_class_id: number; + reviews_allowed: boolean; + average_rating: string; + rating_count: number; + related_ids: number[]; + upsell_ids: string[]; + cross_sell_ids: string[]; + parent_id: number; + purchase_note: string; + categories: Pick[]; + tags: string[]; + images: WCImage[]; + attributes: string[]; + default_attributes: string[]; + variations: string[]; + grouped_products: unknown[]; + menu_order: number; + meta_data: unknown[]; + _links: Links; +} + +export interface Links { + self: Collection[]; + collection: Collection[]; +} + +export interface Collection { + href: string; +} + +export interface Category { + id: number; + name: string; + slug: string; + parent: number; + description: string; + display: string; + image: WCImage; + menu_order: number; + count: number; + _links: Links; +} + +export interface Dimensions { + length: string; + width: string; + height: string; +} + +export interface WCImage { + id: number; + date_created: Date; + date_created_gmt: Date; + date_modified: Date; + date_modified_gmt: Date; + src: string; + name: string; + alt: string; +} From 2001618851bfaf976213410f09fa0334a42c0e92 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Thu, 3 Oct 2024 16:38:37 -0300 Subject: [PATCH 5/9] chore(woocommerce): fmt --- woocommerce/mod.ts | 10 +++------- woocommerce/utils/getAuthValue.ts | 4 ++-- woocommerce/utils/transform.ts | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/woocommerce/mod.ts b/woocommerce/mod.ts index cfb6fbae6..4b339df02 100644 --- a/woocommerce/mod.ts +++ b/woocommerce/mod.ts @@ -1,8 +1,4 @@ -import type { - App as A, - AppContext as AC, - AppRuntime as AR -} from "@deco/deco"; +import type { App as A, AppContext as AC, AppRuntime as AR } from "@deco/deco"; import workflow from "../workflows/mod.ts"; import manifest, { Manifest } from "./manifest.gen.ts"; import { WooCommerceAPI } from "./utils/client.ts"; @@ -52,9 +48,9 @@ export default function WooCommerce( const ck = getAuthValue(customer_key); const cs = getAuthValue(customer_secret); - const createBasicAuth = (key: string, secret: string) => + const createBasicAuth = (key: string, secret: string) => btoa(`${encodeURIComponent(key)}:${encodeURIComponent(secret)}`); - + const auth = createBasicAuth(ck, cs); const headers = new Headers(); diff --git a/woocommerce/utils/getAuthValue.ts b/woocommerce/utils/getAuthValue.ts index 623560890..1273bca61 100644 --- a/woocommerce/utils/getAuthValue.ts +++ b/woocommerce/utils/getAuthValue.ts @@ -1,4 +1,4 @@ import { Secret } from "../../website/loaders/secret.ts"; -export const getAuthValue = (key?: Secret) => - typeof key === "string" ? key : key?.get?.() ?? ""; \ No newline at end of file +export const getAuthValue = (key?: Secret) => + typeof key === "string" ? key : key?.get?.() ?? ""; diff --git a/woocommerce/utils/transform.ts b/woocommerce/utils/transform.ts index 17a49fed7..eaa04b0d0 100644 --- a/woocommerce/utils/transform.ts +++ b/woocommerce/utils/transform.ts @@ -44,7 +44,7 @@ export const toBreadcrumbList = ( }; export const toImage = ( - images: WCImage[] + images: WCImage[], ): ImageObject[] => { return images.map((image) => ({ "@type": "ImageObject", From 2bf920b0a64df9621c854d857482dbc3261e6e3a Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Sat, 5 Oct 2024 00:28:10 -0300 Subject: [PATCH 6/9] feat(woocomerce): adds productList loader --- .../loaders/product/productDetailsPage.ts | 4 + woocommerce/loaders/product/productList.ts | 88 +++++++++++++++++++ woocommerce/manifest.gen.ts | 6 +- woocommerce/utils/client.ts | 15 +++- woocommerce/utils/types.ts | 15 +++- 5 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 woocommerce/loaders/product/productList.ts diff --git a/woocommerce/loaders/product/productDetailsPage.ts b/woocommerce/loaders/product/productDetailsPage.ts index 1ede68c98..615bb1ee3 100644 --- a/woocommerce/loaders/product/productDetailsPage.ts +++ b/woocommerce/loaders/product/productDetailsPage.ts @@ -7,6 +7,10 @@ export interface Props { slug: RequestURLParam; } +/** + * @title WooCommerce Integration + * @description Product Details Page loader + */ async function loader( props: Props, _req: Request, diff --git a/woocommerce/loaders/product/productList.ts b/woocommerce/loaders/product/productList.ts new file mode 100644 index 000000000..b2f8fa367 --- /dev/null +++ b/woocommerce/loaders/product/productList.ts @@ -0,0 +1,88 @@ +import { Product } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toProduct } from "../../utils/transform.ts"; +import { Order, OrderBy, Status, StockStatus } from "../../utils/types.ts"; + +export interface SearchProps { + search: string; +} + +export interface ProductIDProps { + ids: string[]; +} + +export interface Props { + props: SearchProps | ProductIDProps; + /** + * @title Per Page + * @default 10 + * @description Maximum number of items to be returned in result set. Default is 10. + */ + per_page?: number; + /** + * @description Order sort attribute ascending or descending. Default is desc. + */ + order?: Order; + /** + * @title Order By + * @description Sort collection by object attribute. Default is date. + */ + orderby?: OrderBy; + /** + * @title Status + * @description Limit result set to products assigned a specific status. Options: any, draft, pending, private and publish. Default is any. + */ + status?: Status; + /** + * @title Stock Status + * @description Limit result set to products with specified stock status. Default: instock. + */ + stock_status?: StockStatus; + /** + * @title Exclude IDs + * @description Ensure result set excludes specific IDs. + */ + exclude?: string[]; +} + +async function loader( + p: Props, + _req: Request, + ctx: AppContext, +): Promise { + const { api } = ctx; + + const props = p.props ?? + (p as unknown as Props["props"]); + + let products; + + const queryParams: Omit = { + order: p.order ?? "desc", + orderby: p.orderby ?? "date", + status: p.status ?? "any", + stock_status: p.stock_status ?? "instock", + per_page: p.per_page, + exclude: p.exclude, + }; + + if ("search" in props) { + products = await api["GET /wc/v3/products"]({ + search: props.search, + ...queryParams, + }).then((res) => res.json()); + } + + if ("ids" in props) { + products = await api["GET /wc/v3/products"]({ + include: props.ids, + ...queryParams, + }).then((res) => res.json()); + } + + if (!products) return null; + + return products.map((product) => toProduct(product)); +} + +export default loader; diff --git a/woocommerce/manifest.gen.ts b/woocommerce/manifest.gen.ts index b5d3f62f4..e6462f273 100644 --- a/woocommerce/manifest.gen.ts +++ b/woocommerce/manifest.gen.ts @@ -3,12 +3,14 @@ // This file is automatically updated during development when running `dev.ts`. import * as $$$0 from "./loaders/product/productDetailsPage.ts"; -import * as $$$1 from "./loaders/proxy.ts"; +import * as $$$1 from "./loaders/product/productList.ts"; +import * as $$$2 from "./loaders/proxy.ts"; const manifest = { "loaders": { "woocommerce/loaders/product/productDetailsPage.ts": $$$0, - "woocommerce/loaders/proxy.ts": $$$1, + "woocommerce/loaders/product/productList.ts": $$$1, + "woocommerce/loaders/proxy.ts": $$$2, }, "name": "woocommerce", "baseUrl": import.meta.url, diff --git a/woocommerce/utils/client.ts b/woocommerce/utils/client.ts index 77f739ad7..8cdad0016 100644 --- a/woocommerce/utils/client.ts +++ b/woocommerce/utils/client.ts @@ -1,4 +1,11 @@ -import { BaseProduct, Category, OrderBy } from "./types.ts"; +import { + BaseProduct, + Category, + Order, + OrderBy, + Status, + StockStatus, +} from "./types.ts"; export interface WooCommerceAPI { "GET /wc/v3/products": { @@ -12,8 +19,8 @@ export interface WooCommerceAPI { slug?: string; parent?: string; parent_exclude?: string[]; - status?: "any" | "draft" | "pending" | "private" | "publish"; - stock_status?: "instock" | "outofstock" | "onbackorder"; + status?: Status; + stock_status?: StockStatus; type?: "simple" | "grouped" | "external" | "variable"; featured?: boolean; tag?: string; @@ -32,7 +39,7 @@ export interface WooCommerceAPI { searchParams: { page?: number; per_page?: number; - order?: "asc" | "desc"; + order?: Order; orderby?: OrderBy; hide_empty?: boolean; parent?: number; diff --git a/woocommerce/utils/types.ts b/woocommerce/utils/types.ts index e31bc246c..60bc240b4 100644 --- a/woocommerce/utils/types.ts +++ b/woocommerce/utils/types.ts @@ -6,7 +6,20 @@ export type OrderBy = | "modified" | "title" | "slug" - | "date"; + | "date" + | "id" + | "menu_order"; + +export type Status = + | "any" + | "draft" + | "pending" + | "private" + | "publish"; + +export type Order = "asc" | "desc"; + +export type StockStatus = "instock" | "outofstock" | "onbackorder"; export interface BaseProduct { id: number; From 37243586d3c3404534f38863db8986af300fde7d Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Fri, 1 Nov 2024 13:56:43 -0300 Subject: [PATCH 7/9] feat: add productCategory and productListingPage loader --- .../intelligentSearch/productListingPage.ts | 4 +- .../loaders/product/productCategory.ts | 38 ++++++++ .../loaders/product/productListingPage.ts | 92 +++++++++++++++++++ woocommerce/manifest.gen.ts | 16 ++-- woocommerce/utils/utils.ts | 8 ++ 5 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 woocommerce/loaders/product/productCategory.ts create mode 100644 woocommerce/loaders/product/productListingPage.ts create mode 100644 woocommerce/utils/utils.ts diff --git a/vtex/loaders/intelligentSearch/productListingPage.ts b/vtex/loaders/intelligentSearch/productListingPage.ts index fcf1f311c..0c78a7cb5 100644 --- a/vtex/loaders/intelligentSearch/productListingPage.ts +++ b/vtex/loaders/intelligentSearch/productListingPage.ts @@ -264,7 +264,9 @@ const loader = async ( ? filtersFromPathname(pageTypes) : baseSelectedFacets; const selected = withDefaultFacets(selectedFacets, ctx); - const fselected = selected.filter((f) => f.key !== "price"); + const fselected = props.priceFacets + ? selected + : selected.filter((f) => f.key !== "price"); const isInSeachFormat = Boolean(selected.length) || Boolean(args.query); const pathQuery = queryFromPathname(isInSeachFormat, pageTypes, url.pathname); const searchArgs = { ...args, query: args.query || pathQuery }; diff --git a/woocommerce/loaders/product/productCategory.ts b/woocommerce/loaders/product/productCategory.ts new file mode 100644 index 000000000..d1d6a3ec6 --- /dev/null +++ b/woocommerce/loaders/product/productCategory.ts @@ -0,0 +1,38 @@ +import { RequestURLParam } from "../../../website/functions/requestToParam.ts"; +import { AppContext } from "../../mod.ts"; +import { Category } from "../../utils/types.ts"; + +export interface Props { + slug?: RequestURLParam; +} + +/** + * @title WooCommerce Integration + * @description Product Category loader + */ +async function loader( + props: Props, + req: Request, + ctx: AppContext, +): Promise { + const { slug } = props; + const { api } = ctx; + + const urlPathname = new URL(req.url).pathname; + + const pathname = (slug || urlPathname).split("/").filter(Boolean).pop(); + + if (!pathname) return null; + + const categories = await api["GET /wc/v3/products/categories"]({ + slug, + }).then((res) => res.json()); + + const category = categories.find((item) => item.slug === pathname); + + if (!category) return null; + + return category; +} + +export default loader; diff --git a/woocommerce/loaders/product/productListingPage.ts b/woocommerce/loaders/product/productListingPage.ts new file mode 100644 index 000000000..c91a5ccd3 --- /dev/null +++ b/woocommerce/loaders/product/productListingPage.ts @@ -0,0 +1,92 @@ +import type { ProductListingPage } from "../../../commerce/types.ts"; +import { AppContext } from "../../mod.ts"; +import { toProduct } from "../../utils/transform.ts"; +import { Order, OrderBy, Status, StockStatus } from "../../utils/types.ts"; +import { WOOCOMMERCE_SORT_OPTIONS } from "../../utils/utils.ts"; + +export interface Props { + /** + * @description overrides the query term at url + */ + query?: string; + /** + * @default 1 + */ + page: number; + /** + * @title Per Page + * @default 12 + * @description Maximum number of items to be returned in result set. Default is 12. + */ + per_page: number; + order?: Order; + order_by?: OrderBy; + status?: Status; + stock_status?: StockStatus; +} + +/** + * @title WooCommerce Integration + * @description Product Listing Page loader + */ +async function loader( + props: Props, + req: Request, + ctx: AppContext, +): Promise { + const url = new URL(req.url); + const pathname = url.pathname.split("/").filter(Boolean).pop(); + + const { page = 1, per_page = 12, query } = props; + const { api } = ctx; + + const category = await ctx.invoke.woocommerce.loaders.product.productCategory( + { + slug: pathname, + }, + ); + + const products = await api["GET /wc/v3/products"]({ + ...props, + page, + per_page, + category: !query ? category?.id?.toString() : undefined, + search: query, + }).then((res) => res.json()); + + if (!products) return null; + + const totalPages = Math.ceil((category?.count ?? 0) / props.per_page); + const notHasNextPage = totalPages == page; + + return { + "@type": "ProductListingPage", + products: products.map((product) => toProduct(product)), + sortOptions: WOOCOMMERCE_SORT_OPTIONS, + filters: [], + pageInfo: { + previousPage: page == 1 ? undefined : Number(page + 1).toString(), + currentPage: page, + nextPage: notHasNextPage ? undefined : Number(page + 1).toString(), + }, + breadcrumb: { + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem" as const, + name: category?.name, + position: 1, + item: new URL(category?.slug ?? "").href, + }, + ], + numberOfItems: category?.count ?? 0, + }, + seo: { + title: query || category?.name || pathname?.replaceAll("-", " ") || "", + description: category?.description ?? "", + canonical: pathname || req.url, + }, + }; +} + +export default loader; diff --git a/woocommerce/manifest.gen.ts b/woocommerce/manifest.gen.ts index e6462f273..5d5d2b83d 100644 --- a/woocommerce/manifest.gen.ts +++ b/woocommerce/manifest.gen.ts @@ -2,15 +2,19 @@ // This file SHOULD be checked into source version control. // This file is automatically updated during development when running `dev.ts`. -import * as $$$0 from "./loaders/product/productDetailsPage.ts"; -import * as $$$1 from "./loaders/product/productList.ts"; -import * as $$$2 from "./loaders/proxy.ts"; +import * as $$$0 from "./loaders/product/productCategory.ts"; +import * as $$$1 from "./loaders/product/productDetailsPage.ts"; +import * as $$$2 from "./loaders/product/productList.ts"; +import * as $$$3 from "./loaders/product/productListingPage.ts"; +import * as $$$4 from "./loaders/proxy.ts"; const manifest = { "loaders": { - "woocommerce/loaders/product/productDetailsPage.ts": $$$0, - "woocommerce/loaders/product/productList.ts": $$$1, - "woocommerce/loaders/proxy.ts": $$$2, + "woocommerce/loaders/product/productCategory.ts": $$$0, + "woocommerce/loaders/product/productDetailsPage.ts": $$$1, + "woocommerce/loaders/product/productList.ts": $$$2, + "woocommerce/loaders/product/productListingPage.ts": $$$3, + "woocommerce/loaders/proxy.ts": $$$4, }, "name": "woocommerce", "baseUrl": import.meta.url, diff --git a/woocommerce/utils/utils.ts b/woocommerce/utils/utils.ts new file mode 100644 index 000000000..8d509a616 --- /dev/null +++ b/woocommerce/utils/utils.ts @@ -0,0 +1,8 @@ +import { SortOption } from "../../commerce/types.ts"; + +export const WOOCOMMERCE_SORT_OPTIONS: SortOption[] = [ + { value: "date", label: "Data" }, + { value: "price", label: "Preço" }, + { value: "popularity", label: "Popularidade" }, + { value: "rating", label: "Média de Classificação" }, +]; From c798ab2172c4a8da03ca1470e87220414aa2dcb7 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Fri, 1 Nov 2024 16:47:35 -0300 Subject: [PATCH 8/9] fix: preview name fixed --- woocommerce/preview/Preview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/woocommerce/preview/Preview.tsx b/woocommerce/preview/Preview.tsx index 98a3cb4b0..5a51cf9ae 100644 --- a/woocommerce/preview/Preview.tsx +++ b/woocommerce/preview/Preview.tsx @@ -20,7 +20,7 @@ export const PreviewWooCommerce = ( From aeeef22bd874fa144a3d903ccef50b97fb794274 Mon Sep 17 00:00:00 2001 From: "@yuri_assuncx" Date: Fri, 1 Nov 2024 17:08:49 -0300 Subject: [PATCH 9/9] feat: get page info by searchParams --- woocommerce/loaders/product/productListingPage.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/woocommerce/loaders/product/productListingPage.ts b/woocommerce/loaders/product/productListingPage.ts index c91a5ccd3..f92c1e730 100644 --- a/woocommerce/loaders/product/productListingPage.ts +++ b/woocommerce/loaders/product/productListingPage.ts @@ -37,9 +37,11 @@ async function loader( const url = new URL(req.url); const pathname = url.pathname.split("/").filter(Boolean).pop(); - const { page = 1, per_page = 12, query } = props; + const { per_page = 12, query } = props; const { api } = ctx; + const page = Number(url.searchParams.get("page")) || props.page || 1; + const category = await ctx.invoke.woocommerce.loaders.product.productCategory( { slug: pathname,