From e648f267aed89957c8430fc684159dc9e0385e35 Mon Sep 17 00:00:00 2001 From: Robert Field Date: Thu, 16 Nov 2023 17:29:48 +0100 Subject: [PATCH 1/4] feat: latest updates --- examples/algolia/README.md | 160 ++++++++++++++++++++----------------- examples/basic/README.md | 160 ++++++++++++++++++++----------------- 2 files changed, 174 insertions(+), 146 deletions(-) diff --git a/examples/algolia/README.md b/examples/algolia/README.md index a8b3ceff..e0a45c5a 100644 --- a/examples/algolia/README.md +++ b/examples/algolia/README.md @@ -1,52 +1,56 @@ -# `BETA` Elastic Path D2C Starter Kit - algolia +# `BETA` Elastic Path D2C Starter Kit - mystorefront678 This project was generated with [Elastic Path Commerce Cloud CLI](https://www.elasticpath.com/). -The Elastic Path D2C Starter Kit is an opinionated tool box aimed at accelerating the development of direct-to-consumer ecommerce storefronts using [Elastic Path PXM APIs](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/get-started-pcm.html#__docusaurus). Some of the aims of this project are: +The Elastic Path D2C Starter Kit is an opinionated tool box aimed at accelerating the development of direct-to-consumer +ecommerce storefronts +using [Elastic Path PXM APIs](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/get-started-pcm.html#__docusaurus). +Some of the aims of this project are: - **"Not Another Demo Store"** :yawning_face:: provide useful tooling rather than a rigid API showcase -- **Configurability** :construction:: components and building blocks that can be selected and customized to specific use cases -- **Composable Commerce** :handshake:: the starter kit should integrate with best-in-class services to enable modern ecommerce workflows +- **Configurability** :construction:: components and building blocks that can be selected and customized to specific use + cases +- **Composable Commerce** :handshake:: the starter kit should integrate with best-in-class services to enable modern + ecommerce workflows - **Extensibility** :rocket:: can be expanded to include new integrations over time -- **Performance** :racing_car:: extensive use of Next.js static generation for speed +- **Performance** :racing_car:: Elastic Path and Next.js framework working together to provide a fast, scalable + storefront ## Tech Stack -- Next.js +- [Elastic Path PXM](https://www.elasticpath.com/products/product-experience-manager): our next generation product and + catalog management APIs -- EPCC PXM: our next generation product and catalog management APIs +- [Next.js](https://nextjs.org/): a React framework for building static and server-side rendered applications -- Chakra UI: enabling you to get started with a range of out the box components that are easy to customize +- [Tailwind CSS](https://tailwindcss.com/): enabling you to get started with a range of out the box components that are + easy to customize -- Algolia: our current search solution +- [Headless UI](https://headlessui.com/): completely unstyled, fully accessible UI components, designed to integrate + beautifully with Tailwind CSS. -- Netlify (currently) - -## Roadmap - -A list of planned enhancements for this project - -- `create-elasticpath-app`: we aim to provide a CLI interface for the app similar to `create-react-app` and other tools you may have used - This stands to enable a key goal which is to allow you to ‘scaffold’ out your app at create-time, specifying the app structure, integrations and behaviour you require - -- Additional integrations: we have plans to support additional search providers alongside CMS and site builder integrations +- [Typescript](https://www.typescriptlang.org/): JavaScript with syntax for types ## Current feature set reference -| **Feature** | **Notes** | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Static PDP | Product Display Pages | -| Static PLP | Product Listing Pages. Currently driven via Algolia | -| EPCC PXM product variations | [Learn more](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/generate-pcm-variations.html) | -| EPCC PXM static bundles | [Learn more](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/pcm-products/bundle-configuration.html#__docusaurus) | -| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hiearchy and node structure | -| Prebuilt helper components | Some basic building blocks for typical ecommerce store features | +| **Feature** | **Notes** | +|------------------------------------------|-----------------------------------------------------------------------------------------------| +| PDP | Product Display Pages | +| PLP | Product Listing Pages. | +| EPCC PXM product variations | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-product-variations/pxm-variations) | +| EPCC PXM bundles | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-bundles/pxm-bundles) | +| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hierarchy and node structure | +| Prebuilt helper components | Some basic building blocks for typical ecommerce store features | +| Checkout | [Learn more](https://elasticpath.dev/docs/commerce-cloud/checkout/checkout-workflow) | +| Cart | [Learn more](https://elasticpath.dev/docs/commerce-cloud/carts/carts) | ## Helper components: ### Navigation -The store navigation component is node/hierarchy driven and built statically. The ‘top level’ is created directly by the base hierarchies in your EPCC store. This is currently limited to 5 items. 5 ‘direct child’ nodes of each hierarchy, and the nodes attached to them, are supported. +The store navigation component is node/hierarchy driven and built statically. The ‘top level’ is created directly by the +base hierarchies in your EPCC store. This is currently limited to 5 items. 5 ‘direct child’ nodes of each hierarchy, and +the nodes attached to them, are supported. ### Footer @@ -54,23 +58,49 @@ A simple static component with links to placeholder pages provided ### Featured products -Helper display component that will show basic information about products in a given hierarchy or node. Can be passed either a hierarchy/node id from which products can be fetched dynamically, or statically provided as a populated object via a`getStaticProps` call. - -### Featured hierarchies/nodes - -Helper display component that will show basic information about a hierarchy or node. Can be passed either a hierarchy/node id which can be fetched dynamically, or statically provided as a populated object via a`getStaticProps` call. +Helper display component that will show basic information about products in a given hierarchy or node. ### Promotion banner -Helper display component that will show a basic banner with info (title, description) about a promotion. Must be passed populated object via a`getStaticProps` call because fetching promotions required a `client_credentials` token. You can optionally add a background image to a promotion via a custom flow field named `epcc-reference-promotion-image` (add a string URL of where the image can be fetched from) +Helper display component that will show a basic banner with info (title, description) about a promotion. ### Cart and checkout -Currently supporting Braintree checkout (Elastic Path Payments coming soon) +Currently supporting Elastic Path Payments ## Setup -> :warning: **Requires Algolia account and index**: the current beta release of this project requires a properly configured Algolia index. +> If you have already configured your integrations at generation time then you're good to go and can skip this section. + +> :warning: **Requires Algolia account and index**: the current early release of this project requires a properly +> configured Algolia index. + +You can configure your site via composable cli or manually. + +### Composable CLI Configuration + +The easiest way to get started is to use the [composable cli](https://www.npmjs.com/package/composable-cli) to configure +the project. + +#### Algolia Configuration + +From inside your project directory run: + +```bash +composable-cli init algolia +``` + +#### Elastic Path Payments + +From inside your project directory run: + +```bash +composable-cli payments ep-payments +``` + +### Manual Configuration + +#### Algolia Configuration There are a couple of setup steps that need to be done to get started: @@ -83,29 +113,34 @@ First, make a copy of the `.env.example` and rename it to `.env.local.` Set at l ### Setup Currency -Add `NEXT_PUBLIC_DEFAULT_CURRENCY_CODE` value in your environment file. Make sure you use ISO currency code in uppercase e.g. USD, GBP, EUR, CAD etc. +Add `NEXT_PUBLIC_DEFAULT_CURRENCY_CODE` value in your environment file. Make sure you use ISO currency code in uppercase +e.g. USD, GBP, EUR, CAD etc. ### Setup Algolia index -> :tired_face: We recognise manually configuring Algolia in this way is a pain. We are working on tools to streamline this process. - #### Initial setup Make sure you have an Algolia account. Free accounts can be created [on their website](https://www.algolia.com/). -Once you have your api keys from Algolia you need to configure the Algolia integration from Commerce Manager e.g. https://euwest.cm.elasticpath.com/integrations-hub +Once you have your api keys from Algolia you need to configure the Algolia integration from Commerce Manager +e.g. https://euwest.cm.elasticpath.com/integrations-hub -Follow the [Integrating with Algolia](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/integrations/algolia-integration.html#__docusaurus) instructions as outlined in our docs. +Follow +the [Integrating with Algolia](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/integrations/algolia-integration.html#__docusaurus) +instructions as outlined in our docs. You're looking for the **"Algolia Integration - Full / Delta / Large Catalog"** integration. #### Supporting category pages -Our category pages depend on Algolia at the moment and more specially make use of the Aloglia instantsearch widgets. These widgets make use of Facets which have to be configured manually currently. +Our category pages depend on Algolia at the moment and more specially make use of the Aloglia instantsearch widgets. +These widgets make use of Facets which have to be configured currently. ##### Configuring facets -Use the instructions [in the Algolia docs](https://www.algolia.com/doc/guides/solutions/ecommerce/business-users/initial-configuration/faceting/#step-1-declare-attributes-for-faceting) to configure the following attribute for faceting: +Use the +instructions [in the Algolia docs](https://www.algolia.com/doc/guides/solutions/ecommerce/business-users/initial-configuration/faceting/#step-1-declare-attributes-for-faceting) +to configure the following attribute for faceting: ``` ep_categories.lvl0 @@ -130,11 +165,14 @@ my_catalog_index_price_asc my_catalog_index_price_desc ``` -Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia integration. Make sure to create a **standard** replica. +Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) +in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia +integration. Make sure to create a **standard** replica. #### Finally -Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your production environment. +Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your +production environment. ``` NEXT_PUBLIC_ALGOLIA_APP_ID= @@ -154,34 +192,10 @@ npm run dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Git Commits - -### Depends on - -- [lint-staged](https://github.com/okonet/lint-staged) -- [husky](https://github.com/typicode/husky) - -### Pre-commit hooks details - -The project has a pre-commit hook that will run four stages independently for .ts, .tsx, .js and .jsx files - -- runs `prettier` formating fix -- typecheck by running `tsc` -- validate code using `eslint` with the `next lint` command -- runs a final `prettier` format check to make sure nothing slipped through. - -This is configured in the .lintstagedrc.js file in the root project directory. +You can start editing the page by modifying `app/page.tsx`. The page will hot reload as you edit the file. ## Deployment -Deployment is typical for a Next.js site. We recommend using a provider like Netlify or Vercel to get full Next.js feature support. - -You can use an EPCC Webhook created via Commerce Manager to trigger rebuild of your static pages with the ‘catalog updated’ event - -On demand incremental static regeneration is supported and encouraged, however currently this is only supported via Vercel. +Deployment is typical for a Next.js site. We recommend using a provider +like [Netlify](https://www.netlify.com/blog/2020/11/30/how-to-deploy-next.js-sites-to-netlify/) +or [Vercel](https://vercel.com/docs/frameworks/nextjs) to get full Next.js feature support. diff --git a/examples/basic/README.md b/examples/basic/README.md index 17169e38..e0a45c5a 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,52 +1,56 @@ -# `BETA` Elastic Path D2C Starter Kit - basic +# `BETA` Elastic Path D2C Starter Kit - mystorefront678 This project was generated with [Elastic Path Commerce Cloud CLI](https://www.elasticpath.com/). -The Elastic Path D2C Starter Kit is an opinionated tool box aimed at accelerating the development of direct-to-consumer ecommerce storefronts using [Elastic Path PXM APIs](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/get-started-pcm.html#__docusaurus). Some of the aims of this project are: +The Elastic Path D2C Starter Kit is an opinionated tool box aimed at accelerating the development of direct-to-consumer +ecommerce storefronts +using [Elastic Path PXM APIs](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/get-started-pcm.html#__docusaurus). +Some of the aims of this project are: - **"Not Another Demo Store"** :yawning_face:: provide useful tooling rather than a rigid API showcase -- **Configurability** :construction:: components and building blocks that can be selected and customized to specific use cases -- **Composable Commerce** :handshake:: the starter kit should integrate with best-in-class services to enable modern ecommerce workflows +- **Configurability** :construction:: components and building blocks that can be selected and customized to specific use + cases +- **Composable Commerce** :handshake:: the starter kit should integrate with best-in-class services to enable modern + ecommerce workflows - **Extensibility** :rocket:: can be expanded to include new integrations over time -- **Performance** :racing_car:: extensive use of Next.js static generation for speed +- **Performance** :racing_car:: Elastic Path and Next.js framework working together to provide a fast, scalable + storefront ## Tech Stack -- Next.js +- [Elastic Path PXM](https://www.elasticpath.com/products/product-experience-manager): our next generation product and + catalog management APIs -- EPCC PXM: our next generation product and catalog management APIs +- [Next.js](https://nextjs.org/): a React framework for building static and server-side rendered applications -- Chakra UI: enabling you to get started with a range of out the box components that are easy to customize +- [Tailwind CSS](https://tailwindcss.com/): enabling you to get started with a range of out the box components that are + easy to customize -- Algolia: our current search solution +- [Headless UI](https://headlessui.com/): completely unstyled, fully accessible UI components, designed to integrate + beautifully with Tailwind CSS. -- Netlify (currently) - -## Roadmap - -A list of planned enhancements for this project - -- `create-elasticpath-app`: we aim to provide a CLI interface for the app similar to `create-react-app` and other tools you may have used - This stands to enable a key goal which is to allow you to ‘scaffold’ out your app at create-time, specifying the app structure, integrations and behaviour you require - -- Additional integrations: we have plans to support additional search providers alongside CMS and site builder integrations +- [Typescript](https://www.typescriptlang.org/): JavaScript with syntax for types ## Current feature set reference -| **Feature** | **Notes** | -| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -| Static PDP | Product Display Pages | -| Static PLP | Product Listing Pages. Currently driven via Algolia | -| EPCC PXM product variations | [Learn more](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/generate-pcm-variations.html) | -| EPCC PXM static bundles | [Learn more](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/pcm-products/bundle-configuration.html#__docusaurus) | -| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hiearchy and node structure | -| Prebuilt helper components | Some basic building blocks for typical ecommerce store features | +| **Feature** | **Notes** | +|------------------------------------------|-----------------------------------------------------------------------------------------------| +| PDP | Product Display Pages | +| PLP | Product Listing Pages. | +| EPCC PXM product variations | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-product-variations/pxm-variations) | +| EPCC PXM bundles | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-bundles/pxm-bundles) | +| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hierarchy and node structure | +| Prebuilt helper components | Some basic building blocks for typical ecommerce store features | +| Checkout | [Learn more](https://elasticpath.dev/docs/commerce-cloud/checkout/checkout-workflow) | +| Cart | [Learn more](https://elasticpath.dev/docs/commerce-cloud/carts/carts) | ## Helper components: ### Navigation -The store navigation component is node/hierarchy driven and built statically. The ‘top level’ is created directly by the base hierarchies in your EPCC store. This is currently limited to 5 items. 5 ‘direct child’ nodes of each hierarchy, and the nodes attached to them, are supported. +The store navigation component is node/hierarchy driven and built statically. The ‘top level’ is created directly by the +base hierarchies in your EPCC store. This is currently limited to 5 items. 5 ‘direct child’ nodes of each hierarchy, and +the nodes attached to them, are supported. ### Footer @@ -54,23 +58,49 @@ A simple static component with links to placeholder pages provided ### Featured products -Helper display component that will show basic information about products in a given hierarchy or node. Can be passed either a hierarchy/node id from which products can be fetched dynamically, or statically provided as a populated object via a`getStaticProps` call. - -### Featured hierarchies/nodes - -Helper display component that will show basic information about a hierarchy or node. Can be passed either a hierarchy/node id which can be fetched dynamically, or statically provided as a populated object via a`getStaticProps` call. +Helper display component that will show basic information about products in a given hierarchy or node. ### Promotion banner -Helper display component that will show a basic banner with info (title, description) about a promotion. Must be passed populated object via a`getStaticProps` call because fetching promotions required a `client_credentials` token. You can optionally add a background image to a promotion via a custom flow field named `epcc-reference-promotion-image` (add a string URL of where the image can be fetched from) +Helper display component that will show a basic banner with info (title, description) about a promotion. ### Cart and checkout -Currently supporting Braintree checkout (Elastic Path Payments coming soon) +Currently supporting Elastic Path Payments ## Setup -> :warning: **Requires Algolia account and index**: the current beta release of this project requires a properly configured Algolia index. +> If you have already configured your integrations at generation time then you're good to go and can skip this section. + +> :warning: **Requires Algolia account and index**: the current early release of this project requires a properly +> configured Algolia index. + +You can configure your site via composable cli or manually. + +### Composable CLI Configuration + +The easiest way to get started is to use the [composable cli](https://www.npmjs.com/package/composable-cli) to configure +the project. + +#### Algolia Configuration + +From inside your project directory run: + +```bash +composable-cli init algolia +``` + +#### Elastic Path Payments + +From inside your project directory run: + +```bash +composable-cli payments ep-payments +``` + +### Manual Configuration + +#### Algolia Configuration There are a couple of setup steps that need to be done to get started: @@ -83,29 +113,34 @@ First, make a copy of the `.env.example` and rename it to `.env.local.` Set at l ### Setup Currency -Add `NEXT_PUBLIC_DEFAULT_CURRENCY_CODE` value in your environment file. Make sure you use ISO currency code in uppercase e.g. USD, GBP, EUR, CAD etc. +Add `NEXT_PUBLIC_DEFAULT_CURRENCY_CODE` value in your environment file. Make sure you use ISO currency code in uppercase +e.g. USD, GBP, EUR, CAD etc. ### Setup Algolia index -> :tired_face: We recognise manually configuring Algolia in this way is a pain. We are working on tools to streamline this process. - #### Initial setup Make sure you have an Algolia account. Free accounts can be created [on their website](https://www.algolia.com/). -Once you have your api keys from Algolia you need to configure the Algolia integration from Commerce Manager e.g. https://euwest.cm.elasticpath.com/integrations-hub +Once you have your api keys from Algolia you need to configure the Algolia integration from Commerce Manager +e.g. https://euwest.cm.elasticpath.com/integrations-hub -Follow the [Integrating with Algolia](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/integrations/algolia-integration.html#__docusaurus) instructions as outlined in our docs. +Follow +the [Integrating with Algolia](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/integrations/algolia-integration.html#__docusaurus) +instructions as outlined in our docs. You're looking for the **"Algolia Integration - Full / Delta / Large Catalog"** integration. #### Supporting category pages -Our category pages depend on Algolia at the moment and more specially make use of the Aloglia instantsearch widgets. These widgets make use of Facets which have to be configured manually currently. +Our category pages depend on Algolia at the moment and more specially make use of the Aloglia instantsearch widgets. +These widgets make use of Facets which have to be configured currently. ##### Configuring facets -Use the instructions [in the Algolia docs](https://www.algolia.com/doc/guides/solutions/ecommerce/business-users/initial-configuration/faceting/#step-1-declare-attributes-for-faceting) to configure the following attribute for faceting: +Use the +instructions [in the Algolia docs](https://www.algolia.com/doc/guides/solutions/ecommerce/business-users/initial-configuration/faceting/#step-1-declare-attributes-for-faceting) +to configure the following attribute for faceting: ``` ep_categories.lvl0 @@ -130,11 +165,14 @@ my_catalog_index_price_asc my_catalog_index_price_desc ``` -Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia integration. Make sure to create a **standard** replica. +Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) +in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia +integration. Make sure to create a **standard** replica. #### Finally -Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your production environment. +Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your +production environment. ``` NEXT_PUBLIC_ALGOLIA_APP_ID= @@ -154,34 +192,10 @@ npm run dev Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. - -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. - -## Git Commits - -### Depends on - -- [lint-staged](https://github.com/okonet/lint-staged) -- [husky](https://github.com/typicode/husky) - -### Pre-commit hooks details - -The project has a pre-commit hook that will run four stages independently for .ts, .tsx, .js and .jsx files - -- runs `prettier` formating fix -- typecheck by running `tsc` -- validate code using `eslint` with the `next lint` command -- runs a final `prettier` format check to make sure nothing slipped through. - -This is configured in the .lintstagedrc.js file in the root project directory. +You can start editing the page by modifying `app/page.tsx`. The page will hot reload as you edit the file. ## Deployment -Deployment is typical for a Next.js site. We recommend using a provider like Netlify or Vercel to get full Next.js feature support. - -You can use an EPCC Webhook created via Commerce Manager to trigger rebuild of your static pages with the ‘catalog updated’ event - -On demand incremental static regeneration is supported and encouraged, however currently this is only supported via Vercel. +Deployment is typical for a Next.js site. We recommend using a provider +like [Netlify](https://www.netlify.com/blog/2020/11/30/how-to-deploy-next.js-sites-to-netlify/) +or [Vercel](https://vercel.com/docs/frameworks/nextjs) to get full Next.js feature support. From b9855b32cf8b0a57f73071b641ce97bcf751b2a4 Mon Sep 17 00:00:00 2001 From: Robert Field Date: Thu, 16 Nov 2023 17:35:13 +0100 Subject: [PATCH 2/4] feat: added a new simple payment example --- examples/algolia/package.json | 4 +- examples/basic/.env.example | 17 - examples/{basic => payments}/.composablerc | 0 examples/{basic => payments}/.eslintrc.json | 0 examples/{basic => payments}/.gitignore | 0 examples/{basic => payments}/.lintstagedrc.js | 0 examples/{basic => payments}/.prettierignore | 0 examples/{basic => payments}/.prettierrc | 0 examples/{basic => payments}/README.md | 0 .../e2e/checkout-flow.spec.ts | 0 .../{basic => payments}/e2e/home-page.spec.ts | 0 .../e2e/models/d2c-cart-page.ts | 0 .../e2e/models/d2c-checkout-page.ts | 0 .../e2e/models/d2c-home-page.ts | 0 .../e2e/models/d2c-product-detail-page.ts | 0 .../e2e/product-details-page.spec.ts | 0 .../e2e/util/enter-payment-information.ts | 0 .../e2e/util/epcc-admin-client.ts | 0 .../e2e/util/epcc-client.ts | 0 .../e2e/util/fill-form-field.ts | 0 .../e2e/util/gateway-check.ts | 0 .../e2e/util/gateway-is-enabled.ts | 0 .../e2e/util/get-cart-id.ts | 0 .../e2e/util/has-published-catalog.ts | 0 .../e2e/util/missing-published-catalog.ts | 0 .../e2e/util/resolver-product-from-store.ts | 0 .../e2e/util/skip-ci-env.ts | 0 examples/{basic => payments}/license.md | 0 examples/{basic => payments}/next-env.d.ts | 0 examples/{basic => payments}/next.config.js | 0 examples/{basic => payments}/package.json | 6 +- .../{basic => payments}/playwright.config.ts | 0 .../{basic => payments}/postcss.config.js | 0 .../{basic => payments}/public/favicon.ico | Bin .../src/app/about/page.tsx | 0 .../{basic => payments}/src/app/cart/page.tsx | 0 .../app/checkout/[cartId]/cart-display.tsx | 0 .../src/app/checkout/[cartId]/page.tsx | 0 .../src/app/configuration-error/page.tsx | 0 .../{basic => payments}/src/app/error.tsx | 0 .../{basic => payments}/src/app/faq/page.tsx | 0 .../{basic => payments}/src/app/layout.tsx | 0 .../{basic => payments}/src/app/not-found.tsx | 0 examples/{basic => payments}/src/app/page.tsx | 0 .../src/app/products/[productId]/page.tsx | 0 .../products/[productId]/product-display.tsx | 0 .../{basic => payments}/src/app/providers.tsx | 0 .../src/app/shipping/page.tsx | 0 .../src/app/terms/page.tsx | 0 .../src/components/NoImage.tsx | 0 .../src/components/Spinner.tsx | 0 .../src/components/cart/Cart.tsx | 0 .../src/components/cart/CartItemList.tsx | 0 .../src/components/cart/CartOrderSummary.tsx | 0 .../src/components/cart/Promotion.tsx | 0 .../src/components/checkout/BillingForm.tsx | 0 .../src/components/checkout/CountrySelect.tsx | 0 .../components/checkout/CustomFormControl.tsx | 0 .../components/checkout/CustomFormSelect.tsx | 0 .../src/components/checkout/OrderComplete.tsx | 0 .../src/components/checkout/OrderSummary.tsx | 0 .../src/components/checkout/ShippingForm.tsx | 0 .../form-schema/checkout-form-schema.ts | 0 .../checkout/payments/CheckoutForm.tsx | 0 .../payments/EpStripePayment.module.css | 0 .../checkout/payments/EpStripePayment.tsx | 0 .../checkout/payments/EpStripePaymentForm.tsx | 0 .../checkout/types/checkout-form.ts | 0 .../checkout/types/order-pending-state.ts | 0 .../featured-products/FeaturedProducts.tsx | 0 .../fetchFeaturedProducts.ts | 0 .../src/components/footer/Footer.tsx | 0 .../src/components/header/Header.tsx | 0 .../src/components/header/cart/CartMenu.tsx | 0 .../components/header/cart/ModalCartItem.tsx | 0 .../header/navigation/MobileNavBar.tsx | 0 .../header/navigation/MobileNavBarButton.tsx | 0 .../components/header/navigation/NavBar.tsx | 0 .../header/navigation/NavBarPopover.tsx | 0 .../header/navigation/NavItemContent.tsx | 0 .../components/header/navigation/NavMenu.tsx | 0 .../src/components/icons/cart.tsx | 0 .../src/components/icons/ep-icon.tsx | 0 .../src/components/icons/ep-logo.tsx | 0 .../src/components/icons/github-icon.tsx | 0 .../src/components/product/CartActions.tsx | 0 .../src/components/product/Price.tsx | 0 .../components/product/ProductContainer.tsx | 0 .../src/components/product/ProductDetails.tsx | 0 .../components/product/ProductExtensions.tsx | 0 .../src/components/product/ProductSummary.tsx | 0 .../src/components/product/SimpleProduct.tsx | 0 .../src/components/product/StrikePrice.tsx | 0 .../product/bundles/BundleProduct.tsx | 0 .../product/bundles/ProductComponent.tsx | 0 .../product/bundles/ProductComponents.tsx | 0 .../product/bundles/form-parsers.test.ts | 0 .../product/bundles/form-parsers.ts | 0 .../product/bundles/sort-by-order.ts | 0 .../product/bundles/validation-schema.test.ts | 0 .../product/bundles/validation-schema.ts | 0 .../product/carousel/CarouselListener.tsx | 0 .../product/carousel/HorizontalCarousel.tsx | 0 .../carousel/ProductCarousel.module.css | 0 .../product/carousel/ProductCarousel.tsx | 0 .../carousel/ProductHighlightCarousel.tsx | 0 .../variations/ProductVariationColor.tsx | 0 .../variations/ProductVariationStandard.tsx | 0 .../product/variations/ProductVariations.tsx | 0 .../product/variations/VariationProduct.tsx | 0 .../promotion-banner/PromotionBanner.tsx | 0 .../quantity-handler/QuantityHandler.tsx | 0 .../src/components/shared/blurb.tsx | 0 .../src/components/shimmer.tsx | 0 .../src/components/toast/toaster.tsx | 0 .../src/lib/build-site-navigation.ts | 0 .../src/lib/cart-cookie-client.ts | 0 .../src/lib/cart-cookie-server.ts | 0 .../src/lib/color-lookup.ts | 0 .../lib/connect-products-with-main-images.ts | 0 .../src/lib/cookie-constants.ts | 0 .../src/lib/custom-rule-headers.ts | 0 .../src/lib/ep-client-store.ts | 0 .../src/lib/epcc-errors.ts | 0 .../src/lib/epcc-implicit-client.ts | 0 .../src/lib/epcc-server-client.ts | 0 .../lib/epcc-server-side-implicit-client.ts | 0 .../src/lib/form-url-encode-body.ts | 0 .../src/lib/get-store-context.ts | 0 .../src/lib/is-empty-object.ts | 0 .../src/lib/is-supported-extension.ts | 0 .../src/lib/middleware/apply-set-cookie.ts | 0 .../lib/middleware/cart-cookie-middleware.ts | 0 ...create-missing-environment-variable-url.ts | 0 .../middleware/implicit-auth-middleware.ts | 0 .../src/lib/middleware/middleware-runner.ts | 0 .../src/lib/parse-cookie.ts | 0 .../src/lib/product-context.ts | 0 .../src/lib/product-helper.test.ts | 0 .../src/lib/product-helper.ts | 0 .../src/lib/product-util.test.ts | 0 .../src/lib/product-util.ts | 0 .../src/lib/providers/store-provider.tsx | 0 .../src/lib/resolve-cart-env.ts | 0 .../src/lib/resolve-ep-currency-code.ts | 0 .../src/lib/resolve-ep-stripe-env.ts | 0 .../src/lib/resolve-epcc-env.ts | 0 .../src/lib/resolve-shopping-cart-props.ts | 0 .../src/lib/sort-alphabetically.ts | 0 .../{basic => payments}/src/lib/to-base-64.ts | 0 .../src/lib/token-expired.ts | 0 .../src/lib/types/deep-partial.ts | 0 .../src/lib/types/matrix-object-entry.ts | 0 .../src/lib/types/non-empty-array.ts | 0 .../src/lib/types/product-types.ts | 0 .../lib/types/read-only-non-empty-array.ts | 0 .../src/lib/types/store-context.ts | 0 .../{basic => payments}/src/middleware.ts | 0 .../{basic => payments}/src/services/cart.ts | 0 .../src/services/checkout.ts | 0 .../src/services/hierarchy.ts | 0 .../src/services/products.ts | 0 .../src/styles/globals.css | 0 .../{basic => payments}/tailwind.config.ts | 0 examples/{basic => payments}/tsconfig.json | 0 examples/{basic => payments}/vite.config.ts | 0 examples/simple/.composablerc | 6 + examples/simple/.eslintrc.json | 12 + examples/simple/.gitignore | 45 +++ examples/simple/.lintstagedrc.js | 32 ++ examples/simple/.prettierignore | 1 + examples/simple/.prettierrc | 1 + examples/simple/README.md | 201 +++++++++++ examples/simple/e2e/checkout-flow.spec.ts | 68 ++++ examples/simple/e2e/home-page.spec.ts | 14 + examples/simple/e2e/models/d2c-cart-page.ts | 27 ++ .../simple/e2e/models/d2c-checkout-page.ts | 53 +++ examples/simple/e2e/models/d2c-home-page.ts | 15 + .../e2e/models/d2c-product-detail-page.ts | 132 ++++++++ .../simple/e2e/product-details-page.spec.ts | 33 ++ .../e2e/util/enter-payment-information.ts | 10 + examples/simple/e2e/util/epcc-admin-client.ts | 14 + examples/simple/e2e/util/epcc-client.ts | 12 + examples/simple/e2e/util/fill-form-field.ts | 34 ++ examples/simple/e2e/util/gateway-check.ts | 13 + .../simple/e2e/util/gateway-is-enabled.ts | 10 + examples/simple/e2e/util/get-cart-id.ts | 13 + .../simple/e2e/util/has-published-catalog.ts | 12 + .../e2e/util/missing-published-catalog.ts | 10 + .../e2e/util/resolver-product-from-store.ts | 84 +++++ examples/simple/e2e/util/skip-ci-env.ts | 8 + examples/simple/license.md | 21 ++ examples/simple/next-env.d.ts | 5 + examples/simple/next.config.js | 34 ++ examples/simple/package.json | 73 ++++ examples/simple/playwright.config.ts | 82 +++++ examples/simple/postcss.config.js | 6 + examples/simple/public/favicon.ico | Bin 0 -> 15086 bytes examples/simple/src/app/about/page.tsx | 5 + examples/simple/src/app/cart/page.tsx | 42 +++ .../app/checkout/[cartId]/cart-display.tsx | 75 +++++ .../simple/src/app/checkout/[cartId]/page.tsx | 28 ++ .../src/app/configuration-error/page.tsx | 69 ++++ examples/simple/src/app/error.tsx | 31 ++ examples/simple/src/app/faq/page.tsx | 5 + examples/simple/src/app/layout.tsx | 59 ++++ examples/simple/src/app/not-found.tsx | 13 + examples/simple/src/app/page.tsx | 38 +++ .../src/app/products/[productId]/page.tsx | 48 +++ .../products/[productId]/product-display.tsx | 39 +++ examples/simple/src/app/providers.tsx | 17 + examples/simple/src/app/shipping/page.tsx | 5 + examples/simple/src/app/terms/page.tsx | 5 + examples/simple/src/components/NoImage.tsx | 12 + examples/simple/src/components/Spinner.tsx | 30 ++ examples/simple/src/components/cart/Cart.tsx | 38 +++ .../src/components/cart/CartItemList.tsx | 65 ++++ .../src/components/cart/CartOrderSummary.tsx | 80 +++++ .../simple/src/components/cart/Promotion.tsx | 59 ++++ .../src/components/checkout/BillingForm.tsx | 93 ++++++ .../src/components/checkout/CountrySelect.tsx | 262 +++++++++++++++ .../components/checkout/CustomFormControl.tsx | 46 +++ .../components/checkout/CustomFormSelect.tsx | 50 +++ .../src/components/checkout/OrderComplete.tsx | 173 ++++++++++ .../src/components/checkout/OrderSummary.tsx | 106 ++++++ .../src/components/checkout/ShippingForm.tsx | 109 ++++++ .../form-schema/checkout-form-schema.ts | 50 +++ .../checkout/payments/CheckoutForm.tsx | 137 ++++++++ .../checkout/types/checkout-form.ts | 11 + .../checkout/types/order-pending-state.ts | 9 + .../featured-products/FeaturedProducts.tsx | 86 +++++ .../fetchFeaturedProducts.ts | 19 ++ .../simple/src/components/footer/Footer.tsx | 72 ++++ .../simple/src/components/header/Header.tsx | 35 ++ .../src/components/header/cart/CartMenu.tsx | 115 +++++++ .../components/header/cart/ModalCartItem.tsx | 168 ++++++++++ .../header/navigation/MobileNavBar.tsx | 32 ++ .../header/navigation/MobileNavBarButton.tsx | 33 ++ .../components/header/navigation/NavBar.tsx | 17 + .../header/navigation/NavBarPopover.tsx | 44 +++ .../header/navigation/NavItemContent.tsx | 59 ++++ .../components/header/navigation/NavMenu.tsx | 115 +++++++ examples/simple/src/components/icons/cart.tsx | 16 + .../simple/src/components/icons/ep-icon.tsx | 20 ++ .../simple/src/components/icons/ep-logo.tsx | 31 ++ .../src/components/icons/github-icon.tsx | 16 + .../src/components/product/CartActions.tsx | 35 ++ .../simple/src/components/product/Price.tsx | 17 + .../components/product/ProductContainer.tsx | 42 +++ .../src/components/product/ProductDetails.tsx | 28 ++ .../components/product/ProductExtensions.tsx | 69 ++++ .../src/components/product/ProductSummary.tsx | 46 +++ .../src/components/product/SimpleProduct.tsx | 45 +++ .../src/components/product/StrikePrice.tsx | 19 ++ .../product/bundles/BundleProduct.tsx | 68 ++++ .../product/bundles/ProductComponent.tsx | 165 +++++++++ .../product/bundles/ProductComponents.tsx | 33 ++ .../product/bundles/form-parsers.test.ts | 72 ++++ .../product/bundles/form-parsers.ts | 51 +++ .../product/bundles/sort-by-order.ts | 6 + .../product/bundles/validation-schema.test.ts | 136 ++++++++ .../product/bundles/validation-schema.ts | 38 +++ .../product/carousel/CarouselListener.tsx | 22 ++ .../product/carousel/HorizontalCarousel.tsx | 93 ++++++ .../carousel/ProductCarousel.module.css | 20 ++ .../product/carousel/ProductCarousel.tsx | 52 +++ .../carousel/ProductHighlightCarousel.tsx | 89 +++++ .../variations/ProductVariationColor.tsx | 55 +++ .../variations/ProductVariationStandard.tsx | 55 +++ .../product/variations/ProductVariations.tsx | 108 ++++++ .../product/variations/VariationProduct.tsx | 43 +++ .../promotion-banner/PromotionBanner.tsx | 62 ++++ .../quantity-handler/QuantityHandler.tsx | 44 +++ .../simple/src/components/shared/blurb.tsx | 76 +++++ examples/simple/src/components/shimmer.tsx | 13 + .../simple/src/components/toast/toaster.tsx | 25 ++ .../simple/src/lib/build-site-navigation.ts | 104 ++++++ examples/simple/src/lib/cart-cookie-client.ts | 17 + examples/simple/src/lib/cart-cookie-server.ts | 18 + examples/simple/src/lib/color-lookup.ts | 10 + .../lib/connect-products-with-main-images.ts | 29 ++ examples/simple/src/lib/cookie-constants.ts | 3 + .../simple/src/lib/custom-rule-headers.ts | 17 + examples/simple/src/lib/ep-client-store.ts | 27 ++ examples/simple/src/lib/epcc-errors.ts | 27 ++ .../simple/src/lib/epcc-implicit-client.ts | 37 ++ examples/simple/src/lib/epcc-server-client.ts | 29 ++ .../lib/epcc-server-side-implicit-client.ts | 48 +++ .../simple/src/lib/form-url-encode-body.ts | 10 + examples/simple/src/lib/get-store-context.ts | 19 ++ examples/simple/src/lib/is-empty-object.ts | 2 + .../simple/src/lib/is-supported-extension.ts | 9 + .../src/lib/middleware/apply-set-cookie.ts | 31 ++ .../lib/middleware/cart-cookie-middleware.ts | 121 +++++++ ...create-missing-environment-variable-url.ts | 36 ++ .../middleware/implicit-auth-middleware.ts | 102 ++++++ .../src/lib/middleware/middleware-runner.ts | 63 ++++ examples/simple/src/lib/parse-cookie.ts | 32 ++ examples/simple/src/lib/product-context.ts | 10 + .../simple/src/lib/product-helper.test.ts | 273 +++++++++++++++ examples/simple/src/lib/product-helper.ts | 81 +++++ examples/simple/src/lib/product-util.test.ts | 315 ++++++++++++++++++ examples/simple/src/lib/product-util.ts | 54 +++ .../src/lib/providers/store-provider.tsx | 19 ++ examples/simple/src/lib/resolve-cart-env.ts | 11 + .../src/lib/resolve-ep-currency-code.ts | 13 + examples/simple/src/lib/resolve-epcc-env.ts | 21 ++ .../src/lib/resolve-shopping-cart-props.ts | 29 ++ .../simple/src/lib/sort-alphabetically.ts | 4 + examples/simple/src/lib/to-base-64.ts | 4 + examples/simple/src/lib/token-expired.ts | 3 + examples/simple/src/lib/types/deep-partial.ts | 3 + .../src/lib/types/matrix-object-entry.ts | 5 + .../simple/src/lib/types/non-empty-array.ts | 3 + .../simple/src/lib/types/product-types.ts | 31 ++ .../lib/types/read-only-non-empty-array.ts | 7 + .../simple/src/lib/types/store-context.ts | 17 + examples/simple/src/middleware.ts | 21 ++ examples/simple/src/services/cart.ts | 22 ++ examples/simple/src/services/checkout.ts | 34 ++ examples/simple/src/services/hierarchy.ts | 28 ++ examples/simple/src/services/products.ts | 68 ++++ examples/simple/src/styles/globals.css | 64 ++++ examples/simple/tailwind.config.ts | 42 +++ examples/simple/tsconfig.json | 35 ++ examples/simple/vite.config.ts | 10 + scripts/example-specs/configuration.ts | 21 +- 327 files changed, 7437 insertions(+), 24 deletions(-) delete mode 100644 examples/basic/.env.example rename examples/{basic => payments}/.composablerc (100%) rename examples/{basic => payments}/.eslintrc.json (100%) rename examples/{basic => payments}/.gitignore (100%) rename examples/{basic => payments}/.lintstagedrc.js (100%) rename examples/{basic => payments}/.prettierignore (100%) rename examples/{basic => payments}/.prettierrc (100%) rename examples/{basic => payments}/README.md (100%) rename examples/{basic => payments}/e2e/checkout-flow.spec.ts (100%) rename examples/{basic => payments}/e2e/home-page.spec.ts (100%) rename examples/{basic => payments}/e2e/models/d2c-cart-page.ts (100%) rename examples/{basic => payments}/e2e/models/d2c-checkout-page.ts (100%) rename examples/{basic => payments}/e2e/models/d2c-home-page.ts (100%) rename examples/{basic => payments}/e2e/models/d2c-product-detail-page.ts (100%) rename examples/{basic => payments}/e2e/product-details-page.spec.ts (100%) rename examples/{basic => payments}/e2e/util/enter-payment-information.ts (100%) rename examples/{basic => payments}/e2e/util/epcc-admin-client.ts (100%) rename examples/{basic => payments}/e2e/util/epcc-client.ts (100%) rename examples/{basic => payments}/e2e/util/fill-form-field.ts (100%) rename examples/{basic => payments}/e2e/util/gateway-check.ts (100%) rename examples/{basic => payments}/e2e/util/gateway-is-enabled.ts (100%) rename examples/{basic => payments}/e2e/util/get-cart-id.ts (100%) rename examples/{basic => payments}/e2e/util/has-published-catalog.ts (100%) rename examples/{basic => payments}/e2e/util/missing-published-catalog.ts (100%) rename examples/{basic => payments}/e2e/util/resolver-product-from-store.ts (100%) rename examples/{basic => payments}/e2e/util/skip-ci-env.ts (100%) rename examples/{basic => payments}/license.md (100%) rename examples/{basic => payments}/next-env.d.ts (100%) rename examples/{basic => payments}/next.config.js (100%) rename examples/{basic => payments}/package.json (94%) rename examples/{basic => payments}/playwright.config.ts (100%) rename examples/{basic => payments}/postcss.config.js (100%) rename examples/{basic => payments}/public/favicon.ico (100%) rename examples/{basic => payments}/src/app/about/page.tsx (100%) rename examples/{basic => payments}/src/app/cart/page.tsx (100%) rename examples/{basic => payments}/src/app/checkout/[cartId]/cart-display.tsx (100%) rename examples/{basic => payments}/src/app/checkout/[cartId]/page.tsx (100%) rename examples/{basic => payments}/src/app/configuration-error/page.tsx (100%) rename examples/{basic => payments}/src/app/error.tsx (100%) rename examples/{basic => payments}/src/app/faq/page.tsx (100%) rename examples/{basic => payments}/src/app/layout.tsx (100%) rename examples/{basic => payments}/src/app/not-found.tsx (100%) rename examples/{basic => payments}/src/app/page.tsx (100%) rename examples/{basic => payments}/src/app/products/[productId]/page.tsx (100%) rename examples/{basic => payments}/src/app/products/[productId]/product-display.tsx (100%) rename examples/{basic => payments}/src/app/providers.tsx (100%) rename examples/{basic => payments}/src/app/shipping/page.tsx (100%) rename examples/{basic => payments}/src/app/terms/page.tsx (100%) rename examples/{basic => payments}/src/components/NoImage.tsx (100%) rename examples/{basic => payments}/src/components/Spinner.tsx (100%) rename examples/{basic => payments}/src/components/cart/Cart.tsx (100%) rename examples/{basic => payments}/src/components/cart/CartItemList.tsx (100%) rename examples/{basic => payments}/src/components/cart/CartOrderSummary.tsx (100%) rename examples/{basic => payments}/src/components/cart/Promotion.tsx (100%) rename examples/{basic => payments}/src/components/checkout/BillingForm.tsx (100%) rename examples/{basic => payments}/src/components/checkout/CountrySelect.tsx (100%) rename examples/{basic => payments}/src/components/checkout/CustomFormControl.tsx (100%) rename examples/{basic => payments}/src/components/checkout/CustomFormSelect.tsx (100%) rename examples/{basic => payments}/src/components/checkout/OrderComplete.tsx (100%) rename examples/{basic => payments}/src/components/checkout/OrderSummary.tsx (100%) rename examples/{basic => payments}/src/components/checkout/ShippingForm.tsx (100%) rename examples/{basic => payments}/src/components/checkout/form-schema/checkout-form-schema.ts (100%) rename examples/{basic => payments}/src/components/checkout/payments/CheckoutForm.tsx (100%) rename examples/{basic => payments}/src/components/checkout/payments/EpStripePayment.module.css (100%) rename examples/{basic => payments}/src/components/checkout/payments/EpStripePayment.tsx (100%) rename examples/{basic => payments}/src/components/checkout/payments/EpStripePaymentForm.tsx (100%) rename examples/{basic => payments}/src/components/checkout/types/checkout-form.ts (100%) rename examples/{basic => payments}/src/components/checkout/types/order-pending-state.ts (100%) rename examples/{basic => payments}/src/components/featured-products/FeaturedProducts.tsx (100%) rename examples/{basic => payments}/src/components/featured-products/fetchFeaturedProducts.ts (100%) rename examples/{basic => payments}/src/components/footer/Footer.tsx (100%) rename examples/{basic => payments}/src/components/header/Header.tsx (100%) rename examples/{basic => payments}/src/components/header/cart/CartMenu.tsx (100%) rename examples/{basic => payments}/src/components/header/cart/ModalCartItem.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/MobileNavBar.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/MobileNavBarButton.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/NavBar.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/NavBarPopover.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/NavItemContent.tsx (100%) rename examples/{basic => payments}/src/components/header/navigation/NavMenu.tsx (100%) rename examples/{basic => payments}/src/components/icons/cart.tsx (100%) rename examples/{basic => payments}/src/components/icons/ep-icon.tsx (100%) rename examples/{basic => payments}/src/components/icons/ep-logo.tsx (100%) rename examples/{basic => payments}/src/components/icons/github-icon.tsx (100%) rename examples/{basic => payments}/src/components/product/CartActions.tsx (100%) rename examples/{basic => payments}/src/components/product/Price.tsx (100%) rename examples/{basic => payments}/src/components/product/ProductContainer.tsx (100%) rename examples/{basic => payments}/src/components/product/ProductDetails.tsx (100%) rename examples/{basic => payments}/src/components/product/ProductExtensions.tsx (100%) rename examples/{basic => payments}/src/components/product/ProductSummary.tsx (100%) rename examples/{basic => payments}/src/components/product/SimpleProduct.tsx (100%) rename examples/{basic => payments}/src/components/product/StrikePrice.tsx (100%) rename examples/{basic => payments}/src/components/product/bundles/BundleProduct.tsx (100%) rename examples/{basic => payments}/src/components/product/bundles/ProductComponent.tsx (100%) rename examples/{basic => payments}/src/components/product/bundles/ProductComponents.tsx (100%) rename examples/{basic => payments}/src/components/product/bundles/form-parsers.test.ts (100%) rename examples/{basic => payments}/src/components/product/bundles/form-parsers.ts (100%) rename examples/{basic => payments}/src/components/product/bundles/sort-by-order.ts (100%) rename examples/{basic => payments}/src/components/product/bundles/validation-schema.test.ts (100%) rename examples/{basic => payments}/src/components/product/bundles/validation-schema.ts (100%) rename examples/{basic => payments}/src/components/product/carousel/CarouselListener.tsx (100%) rename examples/{basic => payments}/src/components/product/carousel/HorizontalCarousel.tsx (100%) rename examples/{basic => payments}/src/components/product/carousel/ProductCarousel.module.css (100%) rename examples/{basic => payments}/src/components/product/carousel/ProductCarousel.tsx (100%) rename examples/{basic => payments}/src/components/product/carousel/ProductHighlightCarousel.tsx (100%) rename examples/{basic => payments}/src/components/product/variations/ProductVariationColor.tsx (100%) rename examples/{basic => payments}/src/components/product/variations/ProductVariationStandard.tsx (100%) rename examples/{basic => payments}/src/components/product/variations/ProductVariations.tsx (100%) rename examples/{basic => payments}/src/components/product/variations/VariationProduct.tsx (100%) rename examples/{basic => payments}/src/components/promotion-banner/PromotionBanner.tsx (100%) rename examples/{basic => payments}/src/components/quantity-handler/QuantityHandler.tsx (100%) rename examples/{basic => payments}/src/components/shared/blurb.tsx (100%) rename examples/{basic => payments}/src/components/shimmer.tsx (100%) rename examples/{basic => payments}/src/components/toast/toaster.tsx (100%) rename examples/{basic => payments}/src/lib/build-site-navigation.ts (100%) rename examples/{basic => payments}/src/lib/cart-cookie-client.ts (100%) rename examples/{basic => payments}/src/lib/cart-cookie-server.ts (100%) rename examples/{basic => payments}/src/lib/color-lookup.ts (100%) rename examples/{basic => payments}/src/lib/connect-products-with-main-images.ts (100%) rename examples/{basic => payments}/src/lib/cookie-constants.ts (100%) rename examples/{basic => payments}/src/lib/custom-rule-headers.ts (100%) rename examples/{basic => payments}/src/lib/ep-client-store.ts (100%) rename examples/{basic => payments}/src/lib/epcc-errors.ts (100%) rename examples/{basic => payments}/src/lib/epcc-implicit-client.ts (100%) rename examples/{basic => payments}/src/lib/epcc-server-client.ts (100%) rename examples/{basic => payments}/src/lib/epcc-server-side-implicit-client.ts (100%) rename examples/{basic => payments}/src/lib/form-url-encode-body.ts (100%) rename examples/{basic => payments}/src/lib/get-store-context.ts (100%) rename examples/{basic => payments}/src/lib/is-empty-object.ts (100%) rename examples/{basic => payments}/src/lib/is-supported-extension.ts (100%) rename examples/{basic => payments}/src/lib/middleware/apply-set-cookie.ts (100%) rename examples/{basic => payments}/src/lib/middleware/cart-cookie-middleware.ts (100%) rename examples/{basic => payments}/src/lib/middleware/create-missing-environment-variable-url.ts (100%) rename examples/{basic => payments}/src/lib/middleware/implicit-auth-middleware.ts (100%) rename examples/{basic => payments}/src/lib/middleware/middleware-runner.ts (100%) rename examples/{basic => payments}/src/lib/parse-cookie.ts (100%) rename examples/{basic => payments}/src/lib/product-context.ts (100%) rename examples/{basic => payments}/src/lib/product-helper.test.ts (100%) rename examples/{basic => payments}/src/lib/product-helper.ts (100%) rename examples/{basic => payments}/src/lib/product-util.test.ts (100%) rename examples/{basic => payments}/src/lib/product-util.ts (100%) rename examples/{basic => payments}/src/lib/providers/store-provider.tsx (100%) rename examples/{basic => payments}/src/lib/resolve-cart-env.ts (100%) rename examples/{basic => payments}/src/lib/resolve-ep-currency-code.ts (100%) rename examples/{basic => payments}/src/lib/resolve-ep-stripe-env.ts (100%) rename examples/{basic => payments}/src/lib/resolve-epcc-env.ts (100%) rename examples/{basic => payments}/src/lib/resolve-shopping-cart-props.ts (100%) rename examples/{basic => payments}/src/lib/sort-alphabetically.ts (100%) rename examples/{basic => payments}/src/lib/to-base-64.ts (100%) rename examples/{basic => payments}/src/lib/token-expired.ts (100%) rename examples/{basic => payments}/src/lib/types/deep-partial.ts (100%) rename examples/{basic => payments}/src/lib/types/matrix-object-entry.ts (100%) rename examples/{basic => payments}/src/lib/types/non-empty-array.ts (100%) rename examples/{basic => payments}/src/lib/types/product-types.ts (100%) rename examples/{basic => payments}/src/lib/types/read-only-non-empty-array.ts (100%) rename examples/{basic => payments}/src/lib/types/store-context.ts (100%) rename examples/{basic => payments}/src/middleware.ts (100%) rename examples/{basic => payments}/src/services/cart.ts (100%) rename examples/{basic => payments}/src/services/checkout.ts (100%) rename examples/{basic => payments}/src/services/hierarchy.ts (100%) rename examples/{basic => payments}/src/services/products.ts (100%) rename examples/{basic => payments}/src/styles/globals.css (100%) rename examples/{basic => payments}/tailwind.config.ts (100%) rename examples/{basic => payments}/tsconfig.json (100%) rename examples/{basic => payments}/vite.config.ts (100%) create mode 100644 examples/simple/.composablerc create mode 100644 examples/simple/.eslintrc.json create mode 100644 examples/simple/.gitignore create mode 100644 examples/simple/.lintstagedrc.js create mode 100644 examples/simple/.prettierignore create mode 100644 examples/simple/.prettierrc create mode 100644 examples/simple/README.md create mode 100644 examples/simple/e2e/checkout-flow.spec.ts create mode 100644 examples/simple/e2e/home-page.spec.ts create mode 100644 examples/simple/e2e/models/d2c-cart-page.ts create mode 100644 examples/simple/e2e/models/d2c-checkout-page.ts create mode 100644 examples/simple/e2e/models/d2c-home-page.ts create mode 100644 examples/simple/e2e/models/d2c-product-detail-page.ts create mode 100644 examples/simple/e2e/product-details-page.spec.ts create mode 100644 examples/simple/e2e/util/enter-payment-information.ts create mode 100644 examples/simple/e2e/util/epcc-admin-client.ts create mode 100644 examples/simple/e2e/util/epcc-client.ts create mode 100644 examples/simple/e2e/util/fill-form-field.ts create mode 100644 examples/simple/e2e/util/gateway-check.ts create mode 100644 examples/simple/e2e/util/gateway-is-enabled.ts create mode 100644 examples/simple/e2e/util/get-cart-id.ts create mode 100644 examples/simple/e2e/util/has-published-catalog.ts create mode 100644 examples/simple/e2e/util/missing-published-catalog.ts create mode 100644 examples/simple/e2e/util/resolver-product-from-store.ts create mode 100644 examples/simple/e2e/util/skip-ci-env.ts create mode 100644 examples/simple/license.md create mode 100644 examples/simple/next-env.d.ts create mode 100644 examples/simple/next.config.js create mode 100644 examples/simple/package.json create mode 100644 examples/simple/playwright.config.ts create mode 100644 examples/simple/postcss.config.js create mode 100644 examples/simple/public/favicon.ico create mode 100644 examples/simple/src/app/about/page.tsx create mode 100644 examples/simple/src/app/cart/page.tsx create mode 100644 examples/simple/src/app/checkout/[cartId]/cart-display.tsx create mode 100644 examples/simple/src/app/checkout/[cartId]/page.tsx create mode 100644 examples/simple/src/app/configuration-error/page.tsx create mode 100644 examples/simple/src/app/error.tsx create mode 100644 examples/simple/src/app/faq/page.tsx create mode 100644 examples/simple/src/app/layout.tsx create mode 100644 examples/simple/src/app/not-found.tsx create mode 100644 examples/simple/src/app/page.tsx create mode 100644 examples/simple/src/app/products/[productId]/page.tsx create mode 100644 examples/simple/src/app/products/[productId]/product-display.tsx create mode 100644 examples/simple/src/app/providers.tsx create mode 100644 examples/simple/src/app/shipping/page.tsx create mode 100644 examples/simple/src/app/terms/page.tsx create mode 100644 examples/simple/src/components/NoImage.tsx create mode 100644 examples/simple/src/components/Spinner.tsx create mode 100644 examples/simple/src/components/cart/Cart.tsx create mode 100644 examples/simple/src/components/cart/CartItemList.tsx create mode 100644 examples/simple/src/components/cart/CartOrderSummary.tsx create mode 100644 examples/simple/src/components/cart/Promotion.tsx create mode 100644 examples/simple/src/components/checkout/BillingForm.tsx create mode 100644 examples/simple/src/components/checkout/CountrySelect.tsx create mode 100644 examples/simple/src/components/checkout/CustomFormControl.tsx create mode 100644 examples/simple/src/components/checkout/CustomFormSelect.tsx create mode 100644 examples/simple/src/components/checkout/OrderComplete.tsx create mode 100644 examples/simple/src/components/checkout/OrderSummary.tsx create mode 100644 examples/simple/src/components/checkout/ShippingForm.tsx create mode 100644 examples/simple/src/components/checkout/form-schema/checkout-form-schema.ts create mode 100644 examples/simple/src/components/checkout/payments/CheckoutForm.tsx create mode 100644 examples/simple/src/components/checkout/types/checkout-form.ts create mode 100644 examples/simple/src/components/checkout/types/order-pending-state.ts create mode 100644 examples/simple/src/components/featured-products/FeaturedProducts.tsx create mode 100644 examples/simple/src/components/featured-products/fetchFeaturedProducts.ts create mode 100644 examples/simple/src/components/footer/Footer.tsx create mode 100644 examples/simple/src/components/header/Header.tsx create mode 100644 examples/simple/src/components/header/cart/CartMenu.tsx create mode 100644 examples/simple/src/components/header/cart/ModalCartItem.tsx create mode 100644 examples/simple/src/components/header/navigation/MobileNavBar.tsx create mode 100644 examples/simple/src/components/header/navigation/MobileNavBarButton.tsx create mode 100644 examples/simple/src/components/header/navigation/NavBar.tsx create mode 100644 examples/simple/src/components/header/navigation/NavBarPopover.tsx create mode 100644 examples/simple/src/components/header/navigation/NavItemContent.tsx create mode 100644 examples/simple/src/components/header/navigation/NavMenu.tsx create mode 100644 examples/simple/src/components/icons/cart.tsx create mode 100644 examples/simple/src/components/icons/ep-icon.tsx create mode 100644 examples/simple/src/components/icons/ep-logo.tsx create mode 100644 examples/simple/src/components/icons/github-icon.tsx create mode 100644 examples/simple/src/components/product/CartActions.tsx create mode 100644 examples/simple/src/components/product/Price.tsx create mode 100644 examples/simple/src/components/product/ProductContainer.tsx create mode 100644 examples/simple/src/components/product/ProductDetails.tsx create mode 100644 examples/simple/src/components/product/ProductExtensions.tsx create mode 100644 examples/simple/src/components/product/ProductSummary.tsx create mode 100644 examples/simple/src/components/product/SimpleProduct.tsx create mode 100644 examples/simple/src/components/product/StrikePrice.tsx create mode 100644 examples/simple/src/components/product/bundles/BundleProduct.tsx create mode 100644 examples/simple/src/components/product/bundles/ProductComponent.tsx create mode 100644 examples/simple/src/components/product/bundles/ProductComponents.tsx create mode 100644 examples/simple/src/components/product/bundles/form-parsers.test.ts create mode 100644 examples/simple/src/components/product/bundles/form-parsers.ts create mode 100644 examples/simple/src/components/product/bundles/sort-by-order.ts create mode 100644 examples/simple/src/components/product/bundles/validation-schema.test.ts create mode 100644 examples/simple/src/components/product/bundles/validation-schema.ts create mode 100644 examples/simple/src/components/product/carousel/CarouselListener.tsx create mode 100644 examples/simple/src/components/product/carousel/HorizontalCarousel.tsx create mode 100644 examples/simple/src/components/product/carousel/ProductCarousel.module.css create mode 100644 examples/simple/src/components/product/carousel/ProductCarousel.tsx create mode 100644 examples/simple/src/components/product/carousel/ProductHighlightCarousel.tsx create mode 100644 examples/simple/src/components/product/variations/ProductVariationColor.tsx create mode 100644 examples/simple/src/components/product/variations/ProductVariationStandard.tsx create mode 100644 examples/simple/src/components/product/variations/ProductVariations.tsx create mode 100644 examples/simple/src/components/product/variations/VariationProduct.tsx create mode 100644 examples/simple/src/components/promotion-banner/PromotionBanner.tsx create mode 100644 examples/simple/src/components/quantity-handler/QuantityHandler.tsx create mode 100644 examples/simple/src/components/shared/blurb.tsx create mode 100644 examples/simple/src/components/shimmer.tsx create mode 100644 examples/simple/src/components/toast/toaster.tsx create mode 100644 examples/simple/src/lib/build-site-navigation.ts create mode 100644 examples/simple/src/lib/cart-cookie-client.ts create mode 100644 examples/simple/src/lib/cart-cookie-server.ts create mode 100644 examples/simple/src/lib/color-lookup.ts create mode 100644 examples/simple/src/lib/connect-products-with-main-images.ts create mode 100644 examples/simple/src/lib/cookie-constants.ts create mode 100644 examples/simple/src/lib/custom-rule-headers.ts create mode 100644 examples/simple/src/lib/ep-client-store.ts create mode 100644 examples/simple/src/lib/epcc-errors.ts create mode 100644 examples/simple/src/lib/epcc-implicit-client.ts create mode 100644 examples/simple/src/lib/epcc-server-client.ts create mode 100644 examples/simple/src/lib/epcc-server-side-implicit-client.ts create mode 100644 examples/simple/src/lib/form-url-encode-body.ts create mode 100644 examples/simple/src/lib/get-store-context.ts create mode 100644 examples/simple/src/lib/is-empty-object.ts create mode 100644 examples/simple/src/lib/is-supported-extension.ts create mode 100644 examples/simple/src/lib/middleware/apply-set-cookie.ts create mode 100644 examples/simple/src/lib/middleware/cart-cookie-middleware.ts create mode 100644 examples/simple/src/lib/middleware/create-missing-environment-variable-url.ts create mode 100644 examples/simple/src/lib/middleware/implicit-auth-middleware.ts create mode 100644 examples/simple/src/lib/middleware/middleware-runner.ts create mode 100644 examples/simple/src/lib/parse-cookie.ts create mode 100644 examples/simple/src/lib/product-context.ts create mode 100644 examples/simple/src/lib/product-helper.test.ts create mode 100644 examples/simple/src/lib/product-helper.ts create mode 100644 examples/simple/src/lib/product-util.test.ts create mode 100644 examples/simple/src/lib/product-util.ts create mode 100644 examples/simple/src/lib/providers/store-provider.tsx create mode 100644 examples/simple/src/lib/resolve-cart-env.ts create mode 100644 examples/simple/src/lib/resolve-ep-currency-code.ts create mode 100644 examples/simple/src/lib/resolve-epcc-env.ts create mode 100644 examples/simple/src/lib/resolve-shopping-cart-props.ts create mode 100644 examples/simple/src/lib/sort-alphabetically.ts create mode 100644 examples/simple/src/lib/to-base-64.ts create mode 100644 examples/simple/src/lib/token-expired.ts create mode 100644 examples/simple/src/lib/types/deep-partial.ts create mode 100644 examples/simple/src/lib/types/matrix-object-entry.ts create mode 100644 examples/simple/src/lib/types/non-empty-array.ts create mode 100644 examples/simple/src/lib/types/product-types.ts create mode 100644 examples/simple/src/lib/types/read-only-non-empty-array.ts create mode 100644 examples/simple/src/lib/types/store-context.ts create mode 100644 examples/simple/src/middleware.ts create mode 100644 examples/simple/src/services/cart.ts create mode 100644 examples/simple/src/services/checkout.ts create mode 100644 examples/simple/src/services/hierarchy.ts create mode 100644 examples/simple/src/services/products.ts create mode 100644 examples/simple/src/styles/globals.css create mode 100644 examples/simple/tailwind.config.ts create mode 100644 examples/simple/tsconfig.json create mode 100644 examples/simple/vite.config.ts diff --git a/examples/algolia/package.json b/examples/algolia/package.json index c7795c01..5482d8cb 100644 --- a/examples/algolia/package.json +++ b/examples/algolia/package.json @@ -21,8 +21,8 @@ }, "dependencies": { "@algolia/react-instantsearch-widget-color-refinement-list": "^1.4.7", - "@elasticpath/react-shopper-hooks": "workspace:^0.6.0", - "@elasticpath/shopper-common": "workspace:^0.2.0", + "@elasticpath/react-shopper-hooks": "^0.5.1", + "@elasticpath/shopper-common": "^0.1.1", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.0.18", "@moltin/sdk": "^25.0.2", diff --git a/examples/basic/.env.example b/examples/basic/.env.example deleted file mode 100644 index e9f71a4c..00000000 --- a/examples/basic/.env.example +++ /dev/null @@ -1,17 +0,0 @@ -NEXT_PUBLIC_EPCC_CLIENT_ID= -EPCC_CLIENT_SECRET= -STRIPE_CLIENT_SECRET= -REVALIDATE_SECRET_TOKEN -NEXT_PUBLIC_DEFAULT_CURRENCY_CODE= -NEXT_PUBLIC_STRIPE_KEY= -NEXT_PUBLIC_EPCC_ENDPOINT_URL= -NEXT_PUBLIC_ALGOLIA_APP_ID= -NEXT_PUBLIC_ALGOLIA_API_KEY= -NEXT_PUBLIC_ALGOLIA_INDEX_NAME= -NEXT_PUBLIC_COOKIE_PREFIX_KEY= -NEXT_PUBLIC_BRAINTREE_KEY= -NEXT_PUBLIC_CONTEXT_TAG= -NEXT_PUBLIC_CHANNEL= -NEXT_PUBLIC_DEMO_PROMO_ID= -NEXT_PUBLIC_DEMO_NODE_ID= -SITE_NAME=My Site \ No newline at end of file diff --git a/examples/basic/.composablerc b/examples/payments/.composablerc similarity index 100% rename from examples/basic/.composablerc rename to examples/payments/.composablerc diff --git a/examples/basic/.eslintrc.json b/examples/payments/.eslintrc.json similarity index 100% rename from examples/basic/.eslintrc.json rename to examples/payments/.eslintrc.json diff --git a/examples/basic/.gitignore b/examples/payments/.gitignore similarity index 100% rename from examples/basic/.gitignore rename to examples/payments/.gitignore diff --git a/examples/basic/.lintstagedrc.js b/examples/payments/.lintstagedrc.js similarity index 100% rename from examples/basic/.lintstagedrc.js rename to examples/payments/.lintstagedrc.js diff --git a/examples/basic/.prettierignore b/examples/payments/.prettierignore similarity index 100% rename from examples/basic/.prettierignore rename to examples/payments/.prettierignore diff --git a/examples/basic/.prettierrc b/examples/payments/.prettierrc similarity index 100% rename from examples/basic/.prettierrc rename to examples/payments/.prettierrc diff --git a/examples/basic/README.md b/examples/payments/README.md similarity index 100% rename from examples/basic/README.md rename to examples/payments/README.md diff --git a/examples/basic/e2e/checkout-flow.spec.ts b/examples/payments/e2e/checkout-flow.spec.ts similarity index 100% rename from examples/basic/e2e/checkout-flow.spec.ts rename to examples/payments/e2e/checkout-flow.spec.ts diff --git a/examples/basic/e2e/home-page.spec.ts b/examples/payments/e2e/home-page.spec.ts similarity index 100% rename from examples/basic/e2e/home-page.spec.ts rename to examples/payments/e2e/home-page.spec.ts diff --git a/examples/basic/e2e/models/d2c-cart-page.ts b/examples/payments/e2e/models/d2c-cart-page.ts similarity index 100% rename from examples/basic/e2e/models/d2c-cart-page.ts rename to examples/payments/e2e/models/d2c-cart-page.ts diff --git a/examples/basic/e2e/models/d2c-checkout-page.ts b/examples/payments/e2e/models/d2c-checkout-page.ts similarity index 100% rename from examples/basic/e2e/models/d2c-checkout-page.ts rename to examples/payments/e2e/models/d2c-checkout-page.ts diff --git a/examples/basic/e2e/models/d2c-home-page.ts b/examples/payments/e2e/models/d2c-home-page.ts similarity index 100% rename from examples/basic/e2e/models/d2c-home-page.ts rename to examples/payments/e2e/models/d2c-home-page.ts diff --git a/examples/basic/e2e/models/d2c-product-detail-page.ts b/examples/payments/e2e/models/d2c-product-detail-page.ts similarity index 100% rename from examples/basic/e2e/models/d2c-product-detail-page.ts rename to examples/payments/e2e/models/d2c-product-detail-page.ts diff --git a/examples/basic/e2e/product-details-page.spec.ts b/examples/payments/e2e/product-details-page.spec.ts similarity index 100% rename from examples/basic/e2e/product-details-page.spec.ts rename to examples/payments/e2e/product-details-page.spec.ts diff --git a/examples/basic/e2e/util/enter-payment-information.ts b/examples/payments/e2e/util/enter-payment-information.ts similarity index 100% rename from examples/basic/e2e/util/enter-payment-information.ts rename to examples/payments/e2e/util/enter-payment-information.ts diff --git a/examples/basic/e2e/util/epcc-admin-client.ts b/examples/payments/e2e/util/epcc-admin-client.ts similarity index 100% rename from examples/basic/e2e/util/epcc-admin-client.ts rename to examples/payments/e2e/util/epcc-admin-client.ts diff --git a/examples/basic/e2e/util/epcc-client.ts b/examples/payments/e2e/util/epcc-client.ts similarity index 100% rename from examples/basic/e2e/util/epcc-client.ts rename to examples/payments/e2e/util/epcc-client.ts diff --git a/examples/basic/e2e/util/fill-form-field.ts b/examples/payments/e2e/util/fill-form-field.ts similarity index 100% rename from examples/basic/e2e/util/fill-form-field.ts rename to examples/payments/e2e/util/fill-form-field.ts diff --git a/examples/basic/e2e/util/gateway-check.ts b/examples/payments/e2e/util/gateway-check.ts similarity index 100% rename from examples/basic/e2e/util/gateway-check.ts rename to examples/payments/e2e/util/gateway-check.ts diff --git a/examples/basic/e2e/util/gateway-is-enabled.ts b/examples/payments/e2e/util/gateway-is-enabled.ts similarity index 100% rename from examples/basic/e2e/util/gateway-is-enabled.ts rename to examples/payments/e2e/util/gateway-is-enabled.ts diff --git a/examples/basic/e2e/util/get-cart-id.ts b/examples/payments/e2e/util/get-cart-id.ts similarity index 100% rename from examples/basic/e2e/util/get-cart-id.ts rename to examples/payments/e2e/util/get-cart-id.ts diff --git a/examples/basic/e2e/util/has-published-catalog.ts b/examples/payments/e2e/util/has-published-catalog.ts similarity index 100% rename from examples/basic/e2e/util/has-published-catalog.ts rename to examples/payments/e2e/util/has-published-catalog.ts diff --git a/examples/basic/e2e/util/missing-published-catalog.ts b/examples/payments/e2e/util/missing-published-catalog.ts similarity index 100% rename from examples/basic/e2e/util/missing-published-catalog.ts rename to examples/payments/e2e/util/missing-published-catalog.ts diff --git a/examples/basic/e2e/util/resolver-product-from-store.ts b/examples/payments/e2e/util/resolver-product-from-store.ts similarity index 100% rename from examples/basic/e2e/util/resolver-product-from-store.ts rename to examples/payments/e2e/util/resolver-product-from-store.ts diff --git a/examples/basic/e2e/util/skip-ci-env.ts b/examples/payments/e2e/util/skip-ci-env.ts similarity index 100% rename from examples/basic/e2e/util/skip-ci-env.ts rename to examples/payments/e2e/util/skip-ci-env.ts diff --git a/examples/basic/license.md b/examples/payments/license.md similarity index 100% rename from examples/basic/license.md rename to examples/payments/license.md diff --git a/examples/basic/next-env.d.ts b/examples/payments/next-env.d.ts similarity index 100% rename from examples/basic/next-env.d.ts rename to examples/payments/next-env.d.ts diff --git a/examples/basic/next.config.js b/examples/payments/next.config.js similarity index 100% rename from examples/basic/next.config.js rename to examples/payments/next.config.js diff --git a/examples/basic/package.json b/examples/payments/package.json similarity index 94% rename from examples/basic/package.json rename to examples/payments/package.json index f4941e2f..699658a6 100644 --- a/examples/basic/package.json +++ b/examples/payments/package.json @@ -1,5 +1,5 @@ { - "name": "basic", + "name": "payments", "version": "0.0.0", "private": true, "scripts": { @@ -20,8 +20,8 @@ "start:e2e": "NODE_ENV=test next start" }, "dependencies": { - "@elasticpath/react-shopper-hooks": "workspace:^0.6.0", - "@elasticpath/shopper-common": "workspace:^0.2.0", + "@elasticpath/react-shopper-hooks": "^0.5.1", + "@elasticpath/shopper-common": "^0.1.1", "@headlessui/react": "^1.7.17", "@heroicons/react": "^2.0.18", "@moltin/sdk": "^25.0.2", diff --git a/examples/basic/playwright.config.ts b/examples/payments/playwright.config.ts similarity index 100% rename from examples/basic/playwright.config.ts rename to examples/payments/playwright.config.ts diff --git a/examples/basic/postcss.config.js b/examples/payments/postcss.config.js similarity index 100% rename from examples/basic/postcss.config.js rename to examples/payments/postcss.config.js diff --git a/examples/basic/public/favicon.ico b/examples/payments/public/favicon.ico similarity index 100% rename from examples/basic/public/favicon.ico rename to examples/payments/public/favicon.ico diff --git a/examples/basic/src/app/about/page.tsx b/examples/payments/src/app/about/page.tsx similarity index 100% rename from examples/basic/src/app/about/page.tsx rename to examples/payments/src/app/about/page.tsx diff --git a/examples/basic/src/app/cart/page.tsx b/examples/payments/src/app/cart/page.tsx similarity index 100% rename from examples/basic/src/app/cart/page.tsx rename to examples/payments/src/app/cart/page.tsx diff --git a/examples/basic/src/app/checkout/[cartId]/cart-display.tsx b/examples/payments/src/app/checkout/[cartId]/cart-display.tsx similarity index 100% rename from examples/basic/src/app/checkout/[cartId]/cart-display.tsx rename to examples/payments/src/app/checkout/[cartId]/cart-display.tsx diff --git a/examples/basic/src/app/checkout/[cartId]/page.tsx b/examples/payments/src/app/checkout/[cartId]/page.tsx similarity index 100% rename from examples/basic/src/app/checkout/[cartId]/page.tsx rename to examples/payments/src/app/checkout/[cartId]/page.tsx diff --git a/examples/basic/src/app/configuration-error/page.tsx b/examples/payments/src/app/configuration-error/page.tsx similarity index 100% rename from examples/basic/src/app/configuration-error/page.tsx rename to examples/payments/src/app/configuration-error/page.tsx diff --git a/examples/basic/src/app/error.tsx b/examples/payments/src/app/error.tsx similarity index 100% rename from examples/basic/src/app/error.tsx rename to examples/payments/src/app/error.tsx diff --git a/examples/basic/src/app/faq/page.tsx b/examples/payments/src/app/faq/page.tsx similarity index 100% rename from examples/basic/src/app/faq/page.tsx rename to examples/payments/src/app/faq/page.tsx diff --git a/examples/basic/src/app/layout.tsx b/examples/payments/src/app/layout.tsx similarity index 100% rename from examples/basic/src/app/layout.tsx rename to examples/payments/src/app/layout.tsx diff --git a/examples/basic/src/app/not-found.tsx b/examples/payments/src/app/not-found.tsx similarity index 100% rename from examples/basic/src/app/not-found.tsx rename to examples/payments/src/app/not-found.tsx diff --git a/examples/basic/src/app/page.tsx b/examples/payments/src/app/page.tsx similarity index 100% rename from examples/basic/src/app/page.tsx rename to examples/payments/src/app/page.tsx diff --git a/examples/basic/src/app/products/[productId]/page.tsx b/examples/payments/src/app/products/[productId]/page.tsx similarity index 100% rename from examples/basic/src/app/products/[productId]/page.tsx rename to examples/payments/src/app/products/[productId]/page.tsx diff --git a/examples/basic/src/app/products/[productId]/product-display.tsx b/examples/payments/src/app/products/[productId]/product-display.tsx similarity index 100% rename from examples/basic/src/app/products/[productId]/product-display.tsx rename to examples/payments/src/app/products/[productId]/product-display.tsx diff --git a/examples/basic/src/app/providers.tsx b/examples/payments/src/app/providers.tsx similarity index 100% rename from examples/basic/src/app/providers.tsx rename to examples/payments/src/app/providers.tsx diff --git a/examples/basic/src/app/shipping/page.tsx b/examples/payments/src/app/shipping/page.tsx similarity index 100% rename from examples/basic/src/app/shipping/page.tsx rename to examples/payments/src/app/shipping/page.tsx diff --git a/examples/basic/src/app/terms/page.tsx b/examples/payments/src/app/terms/page.tsx similarity index 100% rename from examples/basic/src/app/terms/page.tsx rename to examples/payments/src/app/terms/page.tsx diff --git a/examples/basic/src/components/NoImage.tsx b/examples/payments/src/components/NoImage.tsx similarity index 100% rename from examples/basic/src/components/NoImage.tsx rename to examples/payments/src/components/NoImage.tsx diff --git a/examples/basic/src/components/Spinner.tsx b/examples/payments/src/components/Spinner.tsx similarity index 100% rename from examples/basic/src/components/Spinner.tsx rename to examples/payments/src/components/Spinner.tsx diff --git a/examples/basic/src/components/cart/Cart.tsx b/examples/payments/src/components/cart/Cart.tsx similarity index 100% rename from examples/basic/src/components/cart/Cart.tsx rename to examples/payments/src/components/cart/Cart.tsx diff --git a/examples/basic/src/components/cart/CartItemList.tsx b/examples/payments/src/components/cart/CartItemList.tsx similarity index 100% rename from examples/basic/src/components/cart/CartItemList.tsx rename to examples/payments/src/components/cart/CartItemList.tsx diff --git a/examples/basic/src/components/cart/CartOrderSummary.tsx b/examples/payments/src/components/cart/CartOrderSummary.tsx similarity index 100% rename from examples/basic/src/components/cart/CartOrderSummary.tsx rename to examples/payments/src/components/cart/CartOrderSummary.tsx diff --git a/examples/basic/src/components/cart/Promotion.tsx b/examples/payments/src/components/cart/Promotion.tsx similarity index 100% rename from examples/basic/src/components/cart/Promotion.tsx rename to examples/payments/src/components/cart/Promotion.tsx diff --git a/examples/basic/src/components/checkout/BillingForm.tsx b/examples/payments/src/components/checkout/BillingForm.tsx similarity index 100% rename from examples/basic/src/components/checkout/BillingForm.tsx rename to examples/payments/src/components/checkout/BillingForm.tsx diff --git a/examples/basic/src/components/checkout/CountrySelect.tsx b/examples/payments/src/components/checkout/CountrySelect.tsx similarity index 100% rename from examples/basic/src/components/checkout/CountrySelect.tsx rename to examples/payments/src/components/checkout/CountrySelect.tsx diff --git a/examples/basic/src/components/checkout/CustomFormControl.tsx b/examples/payments/src/components/checkout/CustomFormControl.tsx similarity index 100% rename from examples/basic/src/components/checkout/CustomFormControl.tsx rename to examples/payments/src/components/checkout/CustomFormControl.tsx diff --git a/examples/basic/src/components/checkout/CustomFormSelect.tsx b/examples/payments/src/components/checkout/CustomFormSelect.tsx similarity index 100% rename from examples/basic/src/components/checkout/CustomFormSelect.tsx rename to examples/payments/src/components/checkout/CustomFormSelect.tsx diff --git a/examples/basic/src/components/checkout/OrderComplete.tsx b/examples/payments/src/components/checkout/OrderComplete.tsx similarity index 100% rename from examples/basic/src/components/checkout/OrderComplete.tsx rename to examples/payments/src/components/checkout/OrderComplete.tsx diff --git a/examples/basic/src/components/checkout/OrderSummary.tsx b/examples/payments/src/components/checkout/OrderSummary.tsx similarity index 100% rename from examples/basic/src/components/checkout/OrderSummary.tsx rename to examples/payments/src/components/checkout/OrderSummary.tsx diff --git a/examples/basic/src/components/checkout/ShippingForm.tsx b/examples/payments/src/components/checkout/ShippingForm.tsx similarity index 100% rename from examples/basic/src/components/checkout/ShippingForm.tsx rename to examples/payments/src/components/checkout/ShippingForm.tsx diff --git a/examples/basic/src/components/checkout/form-schema/checkout-form-schema.ts b/examples/payments/src/components/checkout/form-schema/checkout-form-schema.ts similarity index 100% rename from examples/basic/src/components/checkout/form-schema/checkout-form-schema.ts rename to examples/payments/src/components/checkout/form-schema/checkout-form-schema.ts diff --git a/examples/basic/src/components/checkout/payments/CheckoutForm.tsx b/examples/payments/src/components/checkout/payments/CheckoutForm.tsx similarity index 100% rename from examples/basic/src/components/checkout/payments/CheckoutForm.tsx rename to examples/payments/src/components/checkout/payments/CheckoutForm.tsx diff --git a/examples/basic/src/components/checkout/payments/EpStripePayment.module.css b/examples/payments/src/components/checkout/payments/EpStripePayment.module.css similarity index 100% rename from examples/basic/src/components/checkout/payments/EpStripePayment.module.css rename to examples/payments/src/components/checkout/payments/EpStripePayment.module.css diff --git a/examples/basic/src/components/checkout/payments/EpStripePayment.tsx b/examples/payments/src/components/checkout/payments/EpStripePayment.tsx similarity index 100% rename from examples/basic/src/components/checkout/payments/EpStripePayment.tsx rename to examples/payments/src/components/checkout/payments/EpStripePayment.tsx diff --git a/examples/basic/src/components/checkout/payments/EpStripePaymentForm.tsx b/examples/payments/src/components/checkout/payments/EpStripePaymentForm.tsx similarity index 100% rename from examples/basic/src/components/checkout/payments/EpStripePaymentForm.tsx rename to examples/payments/src/components/checkout/payments/EpStripePaymentForm.tsx diff --git a/examples/basic/src/components/checkout/types/checkout-form.ts b/examples/payments/src/components/checkout/types/checkout-form.ts similarity index 100% rename from examples/basic/src/components/checkout/types/checkout-form.ts rename to examples/payments/src/components/checkout/types/checkout-form.ts diff --git a/examples/basic/src/components/checkout/types/order-pending-state.ts b/examples/payments/src/components/checkout/types/order-pending-state.ts similarity index 100% rename from examples/basic/src/components/checkout/types/order-pending-state.ts rename to examples/payments/src/components/checkout/types/order-pending-state.ts diff --git a/examples/basic/src/components/featured-products/FeaturedProducts.tsx b/examples/payments/src/components/featured-products/FeaturedProducts.tsx similarity index 100% rename from examples/basic/src/components/featured-products/FeaturedProducts.tsx rename to examples/payments/src/components/featured-products/FeaturedProducts.tsx diff --git a/examples/basic/src/components/featured-products/fetchFeaturedProducts.ts b/examples/payments/src/components/featured-products/fetchFeaturedProducts.ts similarity index 100% rename from examples/basic/src/components/featured-products/fetchFeaturedProducts.ts rename to examples/payments/src/components/featured-products/fetchFeaturedProducts.ts diff --git a/examples/basic/src/components/footer/Footer.tsx b/examples/payments/src/components/footer/Footer.tsx similarity index 100% rename from examples/basic/src/components/footer/Footer.tsx rename to examples/payments/src/components/footer/Footer.tsx diff --git a/examples/basic/src/components/header/Header.tsx b/examples/payments/src/components/header/Header.tsx similarity index 100% rename from examples/basic/src/components/header/Header.tsx rename to examples/payments/src/components/header/Header.tsx diff --git a/examples/basic/src/components/header/cart/CartMenu.tsx b/examples/payments/src/components/header/cart/CartMenu.tsx similarity index 100% rename from examples/basic/src/components/header/cart/CartMenu.tsx rename to examples/payments/src/components/header/cart/CartMenu.tsx diff --git a/examples/basic/src/components/header/cart/ModalCartItem.tsx b/examples/payments/src/components/header/cart/ModalCartItem.tsx similarity index 100% rename from examples/basic/src/components/header/cart/ModalCartItem.tsx rename to examples/payments/src/components/header/cart/ModalCartItem.tsx diff --git a/examples/basic/src/components/header/navigation/MobileNavBar.tsx b/examples/payments/src/components/header/navigation/MobileNavBar.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/MobileNavBar.tsx rename to examples/payments/src/components/header/navigation/MobileNavBar.tsx diff --git a/examples/basic/src/components/header/navigation/MobileNavBarButton.tsx b/examples/payments/src/components/header/navigation/MobileNavBarButton.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/MobileNavBarButton.tsx rename to examples/payments/src/components/header/navigation/MobileNavBarButton.tsx diff --git a/examples/basic/src/components/header/navigation/NavBar.tsx b/examples/payments/src/components/header/navigation/NavBar.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/NavBar.tsx rename to examples/payments/src/components/header/navigation/NavBar.tsx diff --git a/examples/basic/src/components/header/navigation/NavBarPopover.tsx b/examples/payments/src/components/header/navigation/NavBarPopover.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/NavBarPopover.tsx rename to examples/payments/src/components/header/navigation/NavBarPopover.tsx diff --git a/examples/basic/src/components/header/navigation/NavItemContent.tsx b/examples/payments/src/components/header/navigation/NavItemContent.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/NavItemContent.tsx rename to examples/payments/src/components/header/navigation/NavItemContent.tsx diff --git a/examples/basic/src/components/header/navigation/NavMenu.tsx b/examples/payments/src/components/header/navigation/NavMenu.tsx similarity index 100% rename from examples/basic/src/components/header/navigation/NavMenu.tsx rename to examples/payments/src/components/header/navigation/NavMenu.tsx diff --git a/examples/basic/src/components/icons/cart.tsx b/examples/payments/src/components/icons/cart.tsx similarity index 100% rename from examples/basic/src/components/icons/cart.tsx rename to examples/payments/src/components/icons/cart.tsx diff --git a/examples/basic/src/components/icons/ep-icon.tsx b/examples/payments/src/components/icons/ep-icon.tsx similarity index 100% rename from examples/basic/src/components/icons/ep-icon.tsx rename to examples/payments/src/components/icons/ep-icon.tsx diff --git a/examples/basic/src/components/icons/ep-logo.tsx b/examples/payments/src/components/icons/ep-logo.tsx similarity index 100% rename from examples/basic/src/components/icons/ep-logo.tsx rename to examples/payments/src/components/icons/ep-logo.tsx diff --git a/examples/basic/src/components/icons/github-icon.tsx b/examples/payments/src/components/icons/github-icon.tsx similarity index 100% rename from examples/basic/src/components/icons/github-icon.tsx rename to examples/payments/src/components/icons/github-icon.tsx diff --git a/examples/basic/src/components/product/CartActions.tsx b/examples/payments/src/components/product/CartActions.tsx similarity index 100% rename from examples/basic/src/components/product/CartActions.tsx rename to examples/payments/src/components/product/CartActions.tsx diff --git a/examples/basic/src/components/product/Price.tsx b/examples/payments/src/components/product/Price.tsx similarity index 100% rename from examples/basic/src/components/product/Price.tsx rename to examples/payments/src/components/product/Price.tsx diff --git a/examples/basic/src/components/product/ProductContainer.tsx b/examples/payments/src/components/product/ProductContainer.tsx similarity index 100% rename from examples/basic/src/components/product/ProductContainer.tsx rename to examples/payments/src/components/product/ProductContainer.tsx diff --git a/examples/basic/src/components/product/ProductDetails.tsx b/examples/payments/src/components/product/ProductDetails.tsx similarity index 100% rename from examples/basic/src/components/product/ProductDetails.tsx rename to examples/payments/src/components/product/ProductDetails.tsx diff --git a/examples/basic/src/components/product/ProductExtensions.tsx b/examples/payments/src/components/product/ProductExtensions.tsx similarity index 100% rename from examples/basic/src/components/product/ProductExtensions.tsx rename to examples/payments/src/components/product/ProductExtensions.tsx diff --git a/examples/basic/src/components/product/ProductSummary.tsx b/examples/payments/src/components/product/ProductSummary.tsx similarity index 100% rename from examples/basic/src/components/product/ProductSummary.tsx rename to examples/payments/src/components/product/ProductSummary.tsx diff --git a/examples/basic/src/components/product/SimpleProduct.tsx b/examples/payments/src/components/product/SimpleProduct.tsx similarity index 100% rename from examples/basic/src/components/product/SimpleProduct.tsx rename to examples/payments/src/components/product/SimpleProduct.tsx diff --git a/examples/basic/src/components/product/StrikePrice.tsx b/examples/payments/src/components/product/StrikePrice.tsx similarity index 100% rename from examples/basic/src/components/product/StrikePrice.tsx rename to examples/payments/src/components/product/StrikePrice.tsx diff --git a/examples/basic/src/components/product/bundles/BundleProduct.tsx b/examples/payments/src/components/product/bundles/BundleProduct.tsx similarity index 100% rename from examples/basic/src/components/product/bundles/BundleProduct.tsx rename to examples/payments/src/components/product/bundles/BundleProduct.tsx diff --git a/examples/basic/src/components/product/bundles/ProductComponent.tsx b/examples/payments/src/components/product/bundles/ProductComponent.tsx similarity index 100% rename from examples/basic/src/components/product/bundles/ProductComponent.tsx rename to examples/payments/src/components/product/bundles/ProductComponent.tsx diff --git a/examples/basic/src/components/product/bundles/ProductComponents.tsx b/examples/payments/src/components/product/bundles/ProductComponents.tsx similarity index 100% rename from examples/basic/src/components/product/bundles/ProductComponents.tsx rename to examples/payments/src/components/product/bundles/ProductComponents.tsx diff --git a/examples/basic/src/components/product/bundles/form-parsers.test.ts b/examples/payments/src/components/product/bundles/form-parsers.test.ts similarity index 100% rename from examples/basic/src/components/product/bundles/form-parsers.test.ts rename to examples/payments/src/components/product/bundles/form-parsers.test.ts diff --git a/examples/basic/src/components/product/bundles/form-parsers.ts b/examples/payments/src/components/product/bundles/form-parsers.ts similarity index 100% rename from examples/basic/src/components/product/bundles/form-parsers.ts rename to examples/payments/src/components/product/bundles/form-parsers.ts diff --git a/examples/basic/src/components/product/bundles/sort-by-order.ts b/examples/payments/src/components/product/bundles/sort-by-order.ts similarity index 100% rename from examples/basic/src/components/product/bundles/sort-by-order.ts rename to examples/payments/src/components/product/bundles/sort-by-order.ts diff --git a/examples/basic/src/components/product/bundles/validation-schema.test.ts b/examples/payments/src/components/product/bundles/validation-schema.test.ts similarity index 100% rename from examples/basic/src/components/product/bundles/validation-schema.test.ts rename to examples/payments/src/components/product/bundles/validation-schema.test.ts diff --git a/examples/basic/src/components/product/bundles/validation-schema.ts b/examples/payments/src/components/product/bundles/validation-schema.ts similarity index 100% rename from examples/basic/src/components/product/bundles/validation-schema.ts rename to examples/payments/src/components/product/bundles/validation-schema.ts diff --git a/examples/basic/src/components/product/carousel/CarouselListener.tsx b/examples/payments/src/components/product/carousel/CarouselListener.tsx similarity index 100% rename from examples/basic/src/components/product/carousel/CarouselListener.tsx rename to examples/payments/src/components/product/carousel/CarouselListener.tsx diff --git a/examples/basic/src/components/product/carousel/HorizontalCarousel.tsx b/examples/payments/src/components/product/carousel/HorizontalCarousel.tsx similarity index 100% rename from examples/basic/src/components/product/carousel/HorizontalCarousel.tsx rename to examples/payments/src/components/product/carousel/HorizontalCarousel.tsx diff --git a/examples/basic/src/components/product/carousel/ProductCarousel.module.css b/examples/payments/src/components/product/carousel/ProductCarousel.module.css similarity index 100% rename from examples/basic/src/components/product/carousel/ProductCarousel.module.css rename to examples/payments/src/components/product/carousel/ProductCarousel.module.css diff --git a/examples/basic/src/components/product/carousel/ProductCarousel.tsx b/examples/payments/src/components/product/carousel/ProductCarousel.tsx similarity index 100% rename from examples/basic/src/components/product/carousel/ProductCarousel.tsx rename to examples/payments/src/components/product/carousel/ProductCarousel.tsx diff --git a/examples/basic/src/components/product/carousel/ProductHighlightCarousel.tsx b/examples/payments/src/components/product/carousel/ProductHighlightCarousel.tsx similarity index 100% rename from examples/basic/src/components/product/carousel/ProductHighlightCarousel.tsx rename to examples/payments/src/components/product/carousel/ProductHighlightCarousel.tsx diff --git a/examples/basic/src/components/product/variations/ProductVariationColor.tsx b/examples/payments/src/components/product/variations/ProductVariationColor.tsx similarity index 100% rename from examples/basic/src/components/product/variations/ProductVariationColor.tsx rename to examples/payments/src/components/product/variations/ProductVariationColor.tsx diff --git a/examples/basic/src/components/product/variations/ProductVariationStandard.tsx b/examples/payments/src/components/product/variations/ProductVariationStandard.tsx similarity index 100% rename from examples/basic/src/components/product/variations/ProductVariationStandard.tsx rename to examples/payments/src/components/product/variations/ProductVariationStandard.tsx diff --git a/examples/basic/src/components/product/variations/ProductVariations.tsx b/examples/payments/src/components/product/variations/ProductVariations.tsx similarity index 100% rename from examples/basic/src/components/product/variations/ProductVariations.tsx rename to examples/payments/src/components/product/variations/ProductVariations.tsx diff --git a/examples/basic/src/components/product/variations/VariationProduct.tsx b/examples/payments/src/components/product/variations/VariationProduct.tsx similarity index 100% rename from examples/basic/src/components/product/variations/VariationProduct.tsx rename to examples/payments/src/components/product/variations/VariationProduct.tsx diff --git a/examples/basic/src/components/promotion-banner/PromotionBanner.tsx b/examples/payments/src/components/promotion-banner/PromotionBanner.tsx similarity index 100% rename from examples/basic/src/components/promotion-banner/PromotionBanner.tsx rename to examples/payments/src/components/promotion-banner/PromotionBanner.tsx diff --git a/examples/basic/src/components/quantity-handler/QuantityHandler.tsx b/examples/payments/src/components/quantity-handler/QuantityHandler.tsx similarity index 100% rename from examples/basic/src/components/quantity-handler/QuantityHandler.tsx rename to examples/payments/src/components/quantity-handler/QuantityHandler.tsx diff --git a/examples/basic/src/components/shared/blurb.tsx b/examples/payments/src/components/shared/blurb.tsx similarity index 100% rename from examples/basic/src/components/shared/blurb.tsx rename to examples/payments/src/components/shared/blurb.tsx diff --git a/examples/basic/src/components/shimmer.tsx b/examples/payments/src/components/shimmer.tsx similarity index 100% rename from examples/basic/src/components/shimmer.tsx rename to examples/payments/src/components/shimmer.tsx diff --git a/examples/basic/src/components/toast/toaster.tsx b/examples/payments/src/components/toast/toaster.tsx similarity index 100% rename from examples/basic/src/components/toast/toaster.tsx rename to examples/payments/src/components/toast/toaster.tsx diff --git a/examples/basic/src/lib/build-site-navigation.ts b/examples/payments/src/lib/build-site-navigation.ts similarity index 100% rename from examples/basic/src/lib/build-site-navigation.ts rename to examples/payments/src/lib/build-site-navigation.ts diff --git a/examples/basic/src/lib/cart-cookie-client.ts b/examples/payments/src/lib/cart-cookie-client.ts similarity index 100% rename from examples/basic/src/lib/cart-cookie-client.ts rename to examples/payments/src/lib/cart-cookie-client.ts diff --git a/examples/basic/src/lib/cart-cookie-server.ts b/examples/payments/src/lib/cart-cookie-server.ts similarity index 100% rename from examples/basic/src/lib/cart-cookie-server.ts rename to examples/payments/src/lib/cart-cookie-server.ts diff --git a/examples/basic/src/lib/color-lookup.ts b/examples/payments/src/lib/color-lookup.ts similarity index 100% rename from examples/basic/src/lib/color-lookup.ts rename to examples/payments/src/lib/color-lookup.ts diff --git a/examples/basic/src/lib/connect-products-with-main-images.ts b/examples/payments/src/lib/connect-products-with-main-images.ts similarity index 100% rename from examples/basic/src/lib/connect-products-with-main-images.ts rename to examples/payments/src/lib/connect-products-with-main-images.ts diff --git a/examples/basic/src/lib/cookie-constants.ts b/examples/payments/src/lib/cookie-constants.ts similarity index 100% rename from examples/basic/src/lib/cookie-constants.ts rename to examples/payments/src/lib/cookie-constants.ts diff --git a/examples/basic/src/lib/custom-rule-headers.ts b/examples/payments/src/lib/custom-rule-headers.ts similarity index 100% rename from examples/basic/src/lib/custom-rule-headers.ts rename to examples/payments/src/lib/custom-rule-headers.ts diff --git a/examples/basic/src/lib/ep-client-store.ts b/examples/payments/src/lib/ep-client-store.ts similarity index 100% rename from examples/basic/src/lib/ep-client-store.ts rename to examples/payments/src/lib/ep-client-store.ts diff --git a/examples/basic/src/lib/epcc-errors.ts b/examples/payments/src/lib/epcc-errors.ts similarity index 100% rename from examples/basic/src/lib/epcc-errors.ts rename to examples/payments/src/lib/epcc-errors.ts diff --git a/examples/basic/src/lib/epcc-implicit-client.ts b/examples/payments/src/lib/epcc-implicit-client.ts similarity index 100% rename from examples/basic/src/lib/epcc-implicit-client.ts rename to examples/payments/src/lib/epcc-implicit-client.ts diff --git a/examples/basic/src/lib/epcc-server-client.ts b/examples/payments/src/lib/epcc-server-client.ts similarity index 100% rename from examples/basic/src/lib/epcc-server-client.ts rename to examples/payments/src/lib/epcc-server-client.ts diff --git a/examples/basic/src/lib/epcc-server-side-implicit-client.ts b/examples/payments/src/lib/epcc-server-side-implicit-client.ts similarity index 100% rename from examples/basic/src/lib/epcc-server-side-implicit-client.ts rename to examples/payments/src/lib/epcc-server-side-implicit-client.ts diff --git a/examples/basic/src/lib/form-url-encode-body.ts b/examples/payments/src/lib/form-url-encode-body.ts similarity index 100% rename from examples/basic/src/lib/form-url-encode-body.ts rename to examples/payments/src/lib/form-url-encode-body.ts diff --git a/examples/basic/src/lib/get-store-context.ts b/examples/payments/src/lib/get-store-context.ts similarity index 100% rename from examples/basic/src/lib/get-store-context.ts rename to examples/payments/src/lib/get-store-context.ts diff --git a/examples/basic/src/lib/is-empty-object.ts b/examples/payments/src/lib/is-empty-object.ts similarity index 100% rename from examples/basic/src/lib/is-empty-object.ts rename to examples/payments/src/lib/is-empty-object.ts diff --git a/examples/basic/src/lib/is-supported-extension.ts b/examples/payments/src/lib/is-supported-extension.ts similarity index 100% rename from examples/basic/src/lib/is-supported-extension.ts rename to examples/payments/src/lib/is-supported-extension.ts diff --git a/examples/basic/src/lib/middleware/apply-set-cookie.ts b/examples/payments/src/lib/middleware/apply-set-cookie.ts similarity index 100% rename from examples/basic/src/lib/middleware/apply-set-cookie.ts rename to examples/payments/src/lib/middleware/apply-set-cookie.ts diff --git a/examples/basic/src/lib/middleware/cart-cookie-middleware.ts b/examples/payments/src/lib/middleware/cart-cookie-middleware.ts similarity index 100% rename from examples/basic/src/lib/middleware/cart-cookie-middleware.ts rename to examples/payments/src/lib/middleware/cart-cookie-middleware.ts diff --git a/examples/basic/src/lib/middleware/create-missing-environment-variable-url.ts b/examples/payments/src/lib/middleware/create-missing-environment-variable-url.ts similarity index 100% rename from examples/basic/src/lib/middleware/create-missing-environment-variable-url.ts rename to examples/payments/src/lib/middleware/create-missing-environment-variable-url.ts diff --git a/examples/basic/src/lib/middleware/implicit-auth-middleware.ts b/examples/payments/src/lib/middleware/implicit-auth-middleware.ts similarity index 100% rename from examples/basic/src/lib/middleware/implicit-auth-middleware.ts rename to examples/payments/src/lib/middleware/implicit-auth-middleware.ts diff --git a/examples/basic/src/lib/middleware/middleware-runner.ts b/examples/payments/src/lib/middleware/middleware-runner.ts similarity index 100% rename from examples/basic/src/lib/middleware/middleware-runner.ts rename to examples/payments/src/lib/middleware/middleware-runner.ts diff --git a/examples/basic/src/lib/parse-cookie.ts b/examples/payments/src/lib/parse-cookie.ts similarity index 100% rename from examples/basic/src/lib/parse-cookie.ts rename to examples/payments/src/lib/parse-cookie.ts diff --git a/examples/basic/src/lib/product-context.ts b/examples/payments/src/lib/product-context.ts similarity index 100% rename from examples/basic/src/lib/product-context.ts rename to examples/payments/src/lib/product-context.ts diff --git a/examples/basic/src/lib/product-helper.test.ts b/examples/payments/src/lib/product-helper.test.ts similarity index 100% rename from examples/basic/src/lib/product-helper.test.ts rename to examples/payments/src/lib/product-helper.test.ts diff --git a/examples/basic/src/lib/product-helper.ts b/examples/payments/src/lib/product-helper.ts similarity index 100% rename from examples/basic/src/lib/product-helper.ts rename to examples/payments/src/lib/product-helper.ts diff --git a/examples/basic/src/lib/product-util.test.ts b/examples/payments/src/lib/product-util.test.ts similarity index 100% rename from examples/basic/src/lib/product-util.test.ts rename to examples/payments/src/lib/product-util.test.ts diff --git a/examples/basic/src/lib/product-util.ts b/examples/payments/src/lib/product-util.ts similarity index 100% rename from examples/basic/src/lib/product-util.ts rename to examples/payments/src/lib/product-util.ts diff --git a/examples/basic/src/lib/providers/store-provider.tsx b/examples/payments/src/lib/providers/store-provider.tsx similarity index 100% rename from examples/basic/src/lib/providers/store-provider.tsx rename to examples/payments/src/lib/providers/store-provider.tsx diff --git a/examples/basic/src/lib/resolve-cart-env.ts b/examples/payments/src/lib/resolve-cart-env.ts similarity index 100% rename from examples/basic/src/lib/resolve-cart-env.ts rename to examples/payments/src/lib/resolve-cart-env.ts diff --git a/examples/basic/src/lib/resolve-ep-currency-code.ts b/examples/payments/src/lib/resolve-ep-currency-code.ts similarity index 100% rename from examples/basic/src/lib/resolve-ep-currency-code.ts rename to examples/payments/src/lib/resolve-ep-currency-code.ts diff --git a/examples/basic/src/lib/resolve-ep-stripe-env.ts b/examples/payments/src/lib/resolve-ep-stripe-env.ts similarity index 100% rename from examples/basic/src/lib/resolve-ep-stripe-env.ts rename to examples/payments/src/lib/resolve-ep-stripe-env.ts diff --git a/examples/basic/src/lib/resolve-epcc-env.ts b/examples/payments/src/lib/resolve-epcc-env.ts similarity index 100% rename from examples/basic/src/lib/resolve-epcc-env.ts rename to examples/payments/src/lib/resolve-epcc-env.ts diff --git a/examples/basic/src/lib/resolve-shopping-cart-props.ts b/examples/payments/src/lib/resolve-shopping-cart-props.ts similarity index 100% rename from examples/basic/src/lib/resolve-shopping-cart-props.ts rename to examples/payments/src/lib/resolve-shopping-cart-props.ts diff --git a/examples/basic/src/lib/sort-alphabetically.ts b/examples/payments/src/lib/sort-alphabetically.ts similarity index 100% rename from examples/basic/src/lib/sort-alphabetically.ts rename to examples/payments/src/lib/sort-alphabetically.ts diff --git a/examples/basic/src/lib/to-base-64.ts b/examples/payments/src/lib/to-base-64.ts similarity index 100% rename from examples/basic/src/lib/to-base-64.ts rename to examples/payments/src/lib/to-base-64.ts diff --git a/examples/basic/src/lib/token-expired.ts b/examples/payments/src/lib/token-expired.ts similarity index 100% rename from examples/basic/src/lib/token-expired.ts rename to examples/payments/src/lib/token-expired.ts diff --git a/examples/basic/src/lib/types/deep-partial.ts b/examples/payments/src/lib/types/deep-partial.ts similarity index 100% rename from examples/basic/src/lib/types/deep-partial.ts rename to examples/payments/src/lib/types/deep-partial.ts diff --git a/examples/basic/src/lib/types/matrix-object-entry.ts b/examples/payments/src/lib/types/matrix-object-entry.ts similarity index 100% rename from examples/basic/src/lib/types/matrix-object-entry.ts rename to examples/payments/src/lib/types/matrix-object-entry.ts diff --git a/examples/basic/src/lib/types/non-empty-array.ts b/examples/payments/src/lib/types/non-empty-array.ts similarity index 100% rename from examples/basic/src/lib/types/non-empty-array.ts rename to examples/payments/src/lib/types/non-empty-array.ts diff --git a/examples/basic/src/lib/types/product-types.ts b/examples/payments/src/lib/types/product-types.ts similarity index 100% rename from examples/basic/src/lib/types/product-types.ts rename to examples/payments/src/lib/types/product-types.ts diff --git a/examples/basic/src/lib/types/read-only-non-empty-array.ts b/examples/payments/src/lib/types/read-only-non-empty-array.ts similarity index 100% rename from examples/basic/src/lib/types/read-only-non-empty-array.ts rename to examples/payments/src/lib/types/read-only-non-empty-array.ts diff --git a/examples/basic/src/lib/types/store-context.ts b/examples/payments/src/lib/types/store-context.ts similarity index 100% rename from examples/basic/src/lib/types/store-context.ts rename to examples/payments/src/lib/types/store-context.ts diff --git a/examples/basic/src/middleware.ts b/examples/payments/src/middleware.ts similarity index 100% rename from examples/basic/src/middleware.ts rename to examples/payments/src/middleware.ts diff --git a/examples/basic/src/services/cart.ts b/examples/payments/src/services/cart.ts similarity index 100% rename from examples/basic/src/services/cart.ts rename to examples/payments/src/services/cart.ts diff --git a/examples/basic/src/services/checkout.ts b/examples/payments/src/services/checkout.ts similarity index 100% rename from examples/basic/src/services/checkout.ts rename to examples/payments/src/services/checkout.ts diff --git a/examples/basic/src/services/hierarchy.ts b/examples/payments/src/services/hierarchy.ts similarity index 100% rename from examples/basic/src/services/hierarchy.ts rename to examples/payments/src/services/hierarchy.ts diff --git a/examples/basic/src/services/products.ts b/examples/payments/src/services/products.ts similarity index 100% rename from examples/basic/src/services/products.ts rename to examples/payments/src/services/products.ts diff --git a/examples/basic/src/styles/globals.css b/examples/payments/src/styles/globals.css similarity index 100% rename from examples/basic/src/styles/globals.css rename to examples/payments/src/styles/globals.css diff --git a/examples/basic/tailwind.config.ts b/examples/payments/tailwind.config.ts similarity index 100% rename from examples/basic/tailwind.config.ts rename to examples/payments/tailwind.config.ts diff --git a/examples/basic/tsconfig.json b/examples/payments/tsconfig.json similarity index 100% rename from examples/basic/tsconfig.json rename to examples/payments/tsconfig.json diff --git a/examples/basic/vite.config.ts b/examples/payments/vite.config.ts similarity index 100% rename from examples/basic/vite.config.ts rename to examples/payments/vite.config.ts diff --git a/examples/simple/.composablerc b/examples/simple/.composablerc new file mode 100644 index 00000000..7537a1b3 --- /dev/null +++ b/examples/simple/.composablerc @@ -0,0 +1,6 @@ +{ + "version": 1, + "cli": { + "packageManager": "pnpm" + } +} \ No newline at end of file diff --git a/examples/simple/.eslintrc.json b/examples/simple/.eslintrc.json new file mode 100644 index 00000000..d7dcbb98 --- /dev/null +++ b/examples/simple/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "extends": ["next/core-web-vitals", "prettier"], + "plugins": ["react"], + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, + "rules": { + "react/jsx-curly-brace-presence": "error" + } +} diff --git a/examples/simple/.gitignore b/examples/simple/.gitignore new file mode 100644 index 00000000..537bd7aa --- /dev/null +++ b/examples/simple/.gitignore @@ -0,0 +1,45 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.idea/ + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local +.env.* + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# Being generated by the moltin js-sdk during dev server from server side requests +/localStorage + +test-results \ No newline at end of file diff --git a/examples/simple/.lintstagedrc.js b/examples/simple/.lintstagedrc.js new file mode 100644 index 00000000..0fadc0d4 --- /dev/null +++ b/examples/simple/.lintstagedrc.js @@ -0,0 +1,32 @@ +const path = require("path"); + +/** + * Using next lint with lint-staged requires this setup + * https://nextjs.org/docs/basic-features/eslint#lint-staged + */ + +const buildEslintCommand = (filenames) => + `next lint --fix --file ${filenames + .map((f) => path.relative(process.cwd(), f)) + .join(" --file ")}`; + +/** + * () => "npm run type:check" + * needs to be a function because arguments are getting passed from lint-staged + * when those arguments get through to the "tsc" command that "npm run type:check" + * is calling the args cause "tsc" to ignore the tsconfig.json in our root directory. + * https://github.com/microsoft/TypeScript/issues/27379 + */ +module.exports = { + "*.{js,jsx}": [ + "npm run format:fix", + buildEslintCommand, + "npm run format:check", + ], + "*.{ts,tsx}": [ + "npm run format:fix", + () => "npm run type:check", + buildEslintCommand, + "npm run format:check", + ], +}; diff --git a/examples/simple/.prettierignore b/examples/simple/.prettierignore new file mode 100644 index 00000000..b14c3ee4 --- /dev/null +++ b/examples/simple/.prettierignore @@ -0,0 +1 @@ +**/.next/** \ No newline at end of file diff --git a/examples/simple/.prettierrc b/examples/simple/.prettierrc new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/examples/simple/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/examples/simple/README.md b/examples/simple/README.md new file mode 100644 index 00000000..e0a45c5a --- /dev/null +++ b/examples/simple/README.md @@ -0,0 +1,201 @@ +# `BETA` Elastic Path D2C Starter Kit - mystorefront678 + +This project was generated with [Elastic Path Commerce Cloud CLI](https://www.elasticpath.com/). + +The Elastic Path D2C Starter Kit is an opinionated tool box aimed at accelerating the development of direct-to-consumer +ecommerce storefronts +using [Elastic Path PXM APIs](https://documentation.elasticpath.com/commerce-cloud/docs/developer/how-to/get-started-pcm.html#__docusaurus). +Some of the aims of this project are: + +- **"Not Another Demo Store"** :yawning_face:: provide useful tooling rather than a rigid API showcase +- **Configurability** :construction:: components and building blocks that can be selected and customized to specific use + cases +- **Composable Commerce** :handshake:: the starter kit should integrate with best-in-class services to enable modern + ecommerce workflows +- **Extensibility** :rocket:: can be expanded to include new integrations over time +- **Performance** :racing_car:: Elastic Path and Next.js framework working together to provide a fast, scalable + storefront + +## Tech Stack + +- [Elastic Path PXM](https://www.elasticpath.com/products/product-experience-manager): our next generation product and + catalog management APIs + +- [Next.js](https://nextjs.org/): a React framework for building static and server-side rendered applications + +- [Tailwind CSS](https://tailwindcss.com/): enabling you to get started with a range of out the box components that are + easy to customize + +- [Headless UI](https://headlessui.com/): completely unstyled, fully accessible UI components, designed to integrate + beautifully with Tailwind CSS. + +- [Typescript](https://www.typescriptlang.org/): JavaScript with syntax for types + +## Current feature set reference + +| **Feature** | **Notes** | +|------------------------------------------|-----------------------------------------------------------------------------------------------| +| PDP | Product Display Pages | +| PLP | Product Listing Pages. | +| EPCC PXM product variations | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-product-variations/pxm-variations) | +| EPCC PXM bundles | [Learn more](https://elasticpath.dev/docs/pxm/products/pxm-bundles/pxm-bundles) | +| EPCC PXM hierarchy-based navigation menu | Main site nav driven directly from your store's hierarchy and node structure | +| Prebuilt helper components | Some basic building blocks for typical ecommerce store features | +| Checkout | [Learn more](https://elasticpath.dev/docs/commerce-cloud/checkout/checkout-workflow) | +| Cart | [Learn more](https://elasticpath.dev/docs/commerce-cloud/carts/carts) | + +## Helper components: + +### Navigation + +The store navigation component is node/hierarchy driven and built statically. The ‘top level’ is created directly by the +base hierarchies in your EPCC store. This is currently limited to 5 items. 5 ‘direct child’ nodes of each hierarchy, and +the nodes attached to them, are supported. + +### Footer + +A simple static component with links to placeholder pages provided + +### Featured products + +Helper display component that will show basic information about products in a given hierarchy or node. + +### Promotion banner + +Helper display component that will show a basic banner with info (title, description) about a promotion. + +### Cart and checkout + +Currently supporting Elastic Path Payments + +## Setup + +> If you have already configured your integrations at generation time then you're good to go and can skip this section. + +> :warning: **Requires Algolia account and index**: the current early release of this project requires a properly +> configured Algolia index. + +You can configure your site via composable cli or manually. + +### Composable CLI Configuration + +The easiest way to get started is to use the [composable cli](https://www.npmjs.com/package/composable-cli) to configure +the project. + +#### Algolia Configuration + +From inside your project directory run: + +```bash +composable-cli init algolia +``` + +#### Elastic Path Payments + +From inside your project directory run: + +```bash +composable-cli payments ep-payments +``` + +### Manual Configuration + +#### Algolia Configuration + +There are a couple of setup steps that need to be done to get started: + +- Local environment +- Algolia index + +### Setup Local Environment + +First, make a copy of the `.env.example` and rename it to `.env.local.` Set at least the values marked `` + +### Setup Currency + +Add `NEXT_PUBLIC_DEFAULT_CURRENCY_CODE` value in your environment file. Make sure you use ISO currency code in uppercase +e.g. USD, GBP, EUR, CAD etc. + +### Setup Algolia index + +#### Initial setup + +Make sure you have an Algolia account. Free accounts can be created [on their website](https://www.algolia.com/). + +Once you have your api keys from Algolia you need to configure the Algolia integration from Commerce Manager +e.g. https://euwest.cm.elasticpath.com/integrations-hub + +Follow +the [Integrating with Algolia](https://documentation.elasticpath.com/commerce-cloud/docs/dashboard/integrations/algolia-integration.html#__docusaurus) +instructions as outlined in our docs. + +You're looking for the **"Algolia Integration - Full / Delta / Large Catalog"** integration. + +#### Supporting category pages + +Our category pages depend on Algolia at the moment and more specially make use of the Aloglia instantsearch widgets. +These widgets make use of Facets which have to be configured currently. + +##### Configuring facets + +Use the +instructions [in the Algolia docs](https://www.algolia.com/doc/guides/solutions/ecommerce/business-users/initial-configuration/faceting/#step-1-declare-attributes-for-faceting) +to configure the following attribute for faceting: + +``` +ep_categories.lvl0 +ep_categories.lvl1 +ep_categories.lvl2 +ep_categories.lvl3 + +ep_slug_categories.lvl0 +ep_slug_categories.lvl1 +ep_slug_categories.lvl2 +ep_slug_categories.lvl3 +``` + +Use default settings. + +##### Create Replicas (standard) + +We make use of two **standard** replicas two demonstrate sort: + +``` +my_catalog_index_price_asc +my_catalog_index_price_desc +``` + +Follow ["Creating a replica"](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-by-attribute/#using-the-dashboard) +in the Algolia docs to set both of these up based on the main index created previously by the integrations hub Aloglia +integration. Make sure to create a **standard** replica. + +#### Finally + +Make sure you add the three required Algolia environment variables to your `.env.local` file for local dev and your +production environment. + +``` +NEXT_PUBLIC_ALGOLIA_APP_ID= +NEXT_PUBLIC_ALGOLIA_API_KEY= +NEXT_PUBLIC_ALGOLIA_INDEX_NAME= +``` + +### Dev Server + +then, run the development server: + +```bash +yarn dev +# or +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page will hot reload as you edit the file. + +## Deployment + +Deployment is typical for a Next.js site. We recommend using a provider +like [Netlify](https://www.netlify.com/blog/2020/11/30/how-to-deploy-next.js-sites-to-netlify/) +or [Vercel](https://vercel.com/docs/frameworks/nextjs) to get full Next.js feature support. diff --git a/examples/simple/e2e/checkout-flow.spec.ts b/examples/simple/e2e/checkout-flow.spec.ts new file mode 100644 index 00000000..a50a4094 --- /dev/null +++ b/examples/simple/e2e/checkout-flow.spec.ts @@ -0,0 +1,68 @@ +import { test } from "@playwright/test"; +import { createD2CProductDetailPage } from "./models/d2c-product-detail-page"; +import { client } from "./util/epcc-client"; +import { skipIfMissingCatalog } from "./util/missing-published-catalog"; +import { createD2CCartPage } from "./models/d2c-cart-page"; +import { createD2CCheckoutPage } from "./models/d2c-checkout-page"; +import { gatewayIsEnabled } from "./util/gateway-is-enabled"; +import { skipIfCIEnvironment } from "./util/skip-ci-env"; + +test.describe("Checkout flow", async () => { + test.beforeEach(async () => { + skipIfCIEnvironment(); + await skipIfMissingCatalog(); + await gatewayIsEnabled(); + }); + + test("should perform product checkout", async ({ page }) => { + const productDetailPage = createD2CProductDetailPage(page, client); + const cartPage = createD2CCartPage(page); + const checkoutPage = createD2CCheckoutPage(page); + + /* Go to simple product page */ + await productDetailPage.gotoSimpleProduct(); + + /* Add the product to cart */ + await productDetailPage.addProductToCart(); + + /* Go to cart page and checkout */ + await cartPage.goto(); + await cartPage.checkoutCart(); + + /* Enter information */ + await checkoutPage.enterInformation({ + "Email Address": { value: "test@tester.com", fieldType: "input" }, + "First Name": { value: "Jim", fieldType: "input" }, + "Last Name": { value: "Brown", fieldType: "input" }, + "Street Address": { value: "Main Street", fieldType: "input" }, + "Extended Address": { value: "Extended Address", fieldType: "input" }, + City: { value: "Brownsville", fieldType: "input" }, + County: { value: "Brownsville County", fieldType: "input" }, + Region: { value: "Browns", fieldType: "input" }, + Postcode: { value: "ABC 123", fieldType: "input" }, + Country: { value: "Algeria", fieldType: "select" }, + "Phone Number": { value: "01234567891", fieldType: "input" }, + "Additional Instructions": { + value: "This is some extra instructions.", + fieldType: "input", + }, + }); + + /* Move to payment */ + await checkoutPage.checkout(); + + await checkoutPage.enterPaymentInformation({ + "Card number": { value: "4242424242424242", fieldType: "input" }, + Expiration: { value: "1272", fieldType: "input" }, + CVC: { value: "123", fieldType: "input" }, + Country: { value: "United Kingdom", fieldType: "select" }, + "Postal code": { value: "ABC 123", fieldType: "input" }, + }); + + await checkoutPage.submitPayment(); + await checkoutPage.checkOrderComplete; + + /* Continue Shopping */ + await checkoutPage.continueShopping(); + }); +}); diff --git a/examples/simple/e2e/home-page.spec.ts b/examples/simple/e2e/home-page.spec.ts new file mode 100644 index 00000000..07cbb0f7 --- /dev/null +++ b/examples/simple/e2e/home-page.spec.ts @@ -0,0 +1,14 @@ +import { test } from "@playwright/test"; +import { createD2CHomePage } from "./models/d2c-home-page"; +import { skipIfMissingCatalog } from "./util/missing-published-catalog"; + +test.describe("Home Page", async () => { + test.beforeEach(async () => { + await skipIfMissingCatalog(); + }); + + test("should load home page", async ({ page }) => { + const d2cHomePage = createD2CHomePage(page); + await d2cHomePage.goto(); + }); +}); diff --git a/examples/simple/e2e/models/d2c-cart-page.ts b/examples/simple/e2e/models/d2c-cart-page.ts new file mode 100644 index 00000000..e52f6700 --- /dev/null +++ b/examples/simple/e2e/models/d2c-cart-page.ts @@ -0,0 +1,27 @@ +import type { Locator, Page } from "@playwright/test"; +import { expect } from "@playwright/test"; +import { getCartId } from "../util/get-cart-id"; + +export interface D2CCartPage { + readonly page: Page; + readonly checkoutBtn: Locator; + readonly goto: () => Promise; + readonly checkoutCart: () => Promise; +} + +export function createD2CCartPage(page: Page): D2CCartPage { + const checkoutBtn = page.getByRole("button", { name: "Checkout" }); + + return { + page, + checkoutBtn, + async goto() { + await page.goto(`/cart`); + }, + async checkoutCart() { + await checkoutBtn.click(); + const cartId = await getCartId(page)(); + await expect(page).toHaveURL(`/checkout/${cartId}`); + }, + }; +} diff --git a/examples/simple/e2e/models/d2c-checkout-page.ts b/examples/simple/e2e/models/d2c-checkout-page.ts new file mode 100644 index 00000000..2e5ac81a --- /dev/null +++ b/examples/simple/e2e/models/d2c-checkout-page.ts @@ -0,0 +1,53 @@ +import type { Locator, Page } from "@playwright/test"; +import { fillAllFormFields, FormInput } from "../util/fill-form-field"; +import { expect } from "@playwright/test"; +import { enterPaymentInformation as _enterPaymentInformation } from "../util/enter-payment-information"; + +export interface D2CCheckoutPage { + readonly page: Page; + readonly payNowBtn: Locator; + readonly checkoutBtn: Locator; + readonly goto: () => Promise; + readonly enterInformation: (values: FormInput) => Promise; + readonly checkout: () => Promise; + readonly enterPaymentInformation: (values: FormInput) => Promise; + readonly submitPayment: () => Promise; + readonly checkOrderComplete: () => Promise; + readonly continueShopping: () => Promise; +} + +export function createD2CCheckoutPage(page: Page): D2CCheckoutPage { + const payNowBtn = page.getByRole("button", { name: "Pay now" }); + const checkoutBtn = page.getByRole("button", { name: "Checkout Now" }); + const continueShoppingBtn = page.getByRole("button", { + name: "Continue Shopping", + }); + + return { + page, + payNowBtn, + checkoutBtn, + async goto() { + await page.goto(`/cart`); + }, + async enterPaymentInformation(values: FormInput) { + await _enterPaymentInformation(page, values); + }, + async enterInformation(values: FormInput) { + await fillAllFormFields(page, values); + }, + async submitPayment() { + await payNowBtn.click(); + }, + async checkout() { + await checkoutBtn.click(); + }, + async checkOrderComplete() { + await page.getByText("Thank you for your order!"); + }, + async continueShopping() { + await continueShoppingBtn.click(); + await expect(page).toHaveURL(`/`); + }, + }; +} diff --git a/examples/simple/e2e/models/d2c-home-page.ts b/examples/simple/e2e/models/d2c-home-page.ts new file mode 100644 index 00000000..9a847b8b --- /dev/null +++ b/examples/simple/e2e/models/d2c-home-page.ts @@ -0,0 +1,15 @@ +import type { Page } from "@playwright/test"; + +export interface D2CHomePage { + readonly page: Page; + readonly goto: () => Promise; +} + +export function createD2CHomePage(page: Page): D2CHomePage { + return { + page, + async goto() { + await page.goto("/"); + }, + }; +} diff --git a/examples/simple/e2e/models/d2c-product-detail-page.ts b/examples/simple/e2e/models/d2c-product-detail-page.ts new file mode 100644 index 00000000..d2501290 --- /dev/null +++ b/examples/simple/e2e/models/d2c-product-detail-page.ts @@ -0,0 +1,132 @@ +import type { Page } from "@playwright/test"; +import { expect, test } from "@playwright/test"; +import { + getProductById, + getSimpleProduct, + getVariationsProduct, +} from "../util/resolver-product-from-store"; +import type { Moltin as EPCCClient, ProductResponse } from "@moltin/sdk"; +import { getCartId } from "../util/get-cart-id"; +import { getSkuIdFromOptions } from "../../src/lib/product-helper"; + +const host = process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL; + +export interface D2CProductDetailPage { + readonly page: Page; + readonly gotoSimpleProduct: () => Promise; + readonly gotoVariationsProduct: () => Promise; + readonly getCartId: () => Promise; + readonly addProductToCart: () => Promise; + readonly gotoProductVariation: () => Promise; +} + +export function createD2CProductDetailPage( + page: Page, + client: EPCCClient, +): D2CProductDetailPage { + let activeProduct: ProductResponse | undefined; + const addToCartBtn = page.getByRole("button", { name: "Add to Cart" }); + + return { + page, + async gotoSimpleProduct() { + activeProduct = await getSimpleProduct(client); + await skipOrGotoProduct( + page, + "Can't run test because there is no simple product published in the store.", + activeProduct, + ); + }, + async gotoVariationsProduct() { + activeProduct = await getVariationsProduct(client); + await skipOrGotoProduct( + page, + "Can't run test because there is no variation product published in the store.", + activeProduct, + ); + }, + async gotoProductVariation() { + expect( + activeProduct, + "Make sure you call one of the gotoVariationsProduct function first before calling gotoProductVariation", + ).toBeDefined(); + expect(activeProduct?.attributes.base_product).toEqual(true); + + const expectedProductId = await selectOptions(activeProduct!, page); + const product = await getProductById(client, expectedProductId); + + expect(product.data?.id).toBeDefined(); + activeProduct = product.data; + + /* Check to make sure the page has navigated to the selected product */ + await expect(page).toHaveURL(`/products/${expectedProductId}`); + }, + getCartId: getCartId(page), + async addProductToCart() { + expect( + activeProduct, + "Make sure you call one of the gotoProduct function first before calling addProductToCart", + ).toBeDefined(); + /* Get the cart id */ + const cartId = await getCartId(page)(); + + /* Add the product to cart */ + await addToCartBtn.click(); + /* Wait for the cart POST request to complete */ + const reqUrl = `https://${host}/v2/carts/${cartId}/items`; + await page.waitForResponse(reqUrl); + + /* Check to make sure the product has been added to cart */ + const result = await client.Cart(cartId).With("items").Get(); + await expect( + activeProduct?.attributes.price, + "Missing price on active product - make sure the product has a price set can't add to cart without one.", + ).toBeDefined(); + await expect( + result.included?.items.find( + (item) => item.product_id === activeProduct!.id, + ), + ).toHaveProperty("product_id", activeProduct!.id); + }, + }; +} + +async function skipOrGotoProduct( + page: Page, + msg: string, + product?: ProductResponse, +) { + if (!product) { + test.skip(!product, msg); + } else { + await page.goto(`/products/${product.id}`); + } +} + +async function selectOptions( + baseProduct: ProductResponse, + page: Page, +): Promise { + /* select one of each variation option */ + const options = baseProduct.meta.variations?.reduce((acc, variation) => { + return [...acc, ...([variation.options?.[0]] ?? [])]; + }, []); + + if (options && baseProduct.meta.variation_matrix) { + for (const option of options) { + await page.click(`text=${option.name}`); + } + + const variationId = getSkuIdFromOptions( + options.map((x) => x.id), + baseProduct.meta.variation_matrix, + ); + + if (!variationId) { + throw new Error("Unable to resolve variation id."); + } + return variationId; + } + + throw Error("Unable to select options they were not defined."); +} diff --git a/examples/simple/e2e/product-details-page.spec.ts b/examples/simple/e2e/product-details-page.spec.ts new file mode 100644 index 00000000..fd8823c8 --- /dev/null +++ b/examples/simple/e2e/product-details-page.spec.ts @@ -0,0 +1,33 @@ +import { test } from "@playwright/test"; +import { createD2CProductDetailPage } from "./models/d2c-product-detail-page"; +import { client } from "./util/epcc-client"; +import { skipIfMissingCatalog } from "./util/missing-published-catalog"; + +test.describe("Product Details Page", async () => { + test.beforeEach(async () => { + await skipIfMissingCatalog(); + }); + + test("should add a simple product to cart", async ({ page }) => { + const productDetailPage = createD2CProductDetailPage(page, client); + + /* Go to base product page */ + await productDetailPage.gotoSimpleProduct(); + + /* Add the product to cart */ + await productDetailPage.addProductToCart(); + }); + + test("should add variation product to cart", async ({ page }) => { + const productDetailPage = createD2CProductDetailPage(page, client); + + /* Go to base product page */ + await productDetailPage.gotoVariationsProduct(); + + /* Select the product variations */ + await productDetailPage.gotoProductVariation(); + + /* Add the product to cart */ + await productDetailPage.addProductToCart(); + }); +}); diff --git a/examples/simple/e2e/util/enter-payment-information.ts b/examples/simple/e2e/util/enter-payment-information.ts new file mode 100644 index 00000000..738196b7 --- /dev/null +++ b/examples/simple/e2e/util/enter-payment-information.ts @@ -0,0 +1,10 @@ +import { Page } from "@playwright/test"; +import { fillAllFormFields, FormInput } from "./fill-form-field"; + +export async function enterPaymentInformation(page: Page, values: FormInput) { + const paymentIframe = await page + .locator('[id="payment-element"]') + .frameLocator("iframe"); + + await fillAllFormFields(paymentIframe, values); +} diff --git a/examples/simple/e2e/util/epcc-admin-client.ts b/examples/simple/e2e/util/epcc-admin-client.ts new file mode 100644 index 00000000..80b942f4 --- /dev/null +++ b/examples/simple/e2e/util/epcc-admin-client.ts @@ -0,0 +1,14 @@ +import { gateway, MemoryStorageFactory } from "@moltin/sdk"; + +const host = process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL; +const client_id = process.env.NEXT_PUBLIC_EPCC_CLIENT_ID; +const client_secret = process.env.EPCC_CLIENT_SECRET; + +export const adminClient = gateway({ + client_id, + client_secret, + host, + throttleEnabled: true, + name: "admin_client", + storage: new MemoryStorageFactory(), +}); diff --git a/examples/simple/e2e/util/epcc-client.ts b/examples/simple/e2e/util/epcc-client.ts new file mode 100644 index 00000000..96c13b11 --- /dev/null +++ b/examples/simple/e2e/util/epcc-client.ts @@ -0,0 +1,12 @@ +import { gateway, MemoryStorageFactory } from "@moltin/sdk"; + +const host = process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL; +const client_id = process.env.NEXT_PUBLIC_EPCC_CLIENT_ID; + +export const client = gateway({ + client_id, + host, + throttleEnabled: true, + name: "implicit_client", + storage: new MemoryStorageFactory(), +}); diff --git a/examples/simple/e2e/util/fill-form-field.ts b/examples/simple/e2e/util/fill-form-field.ts new file mode 100644 index 00000000..f2b06696 --- /dev/null +++ b/examples/simple/e2e/util/fill-form-field.ts @@ -0,0 +1,34 @@ +import { FrameLocator, Page } from "@playwright/test"; + +export type FormInputValue = { value: string; fieldType: "input" | "select" }; +export type FormInput = Record; + +export async function fillAllFormFields( + page: Page | FrameLocator, + input: FormInput, +) { + const fillers = Object.keys(input).map((key) => { + return () => fillFormField(page, key, input[key]); + }); + + for (const filler of fillers) { + await filler(); + } +} + +export async function fillFormField( + page: Page | FrameLocator, + key: string, + { value, fieldType }: FormInputValue, +): Promise { + const locator = page.getByLabel(key); + + switch (fieldType) { + case "input": + return locator.fill(value); + case "select": { + await locator.selectOption(value); + return; + } + } +} diff --git a/examples/simple/e2e/util/gateway-check.ts b/examples/simple/e2e/util/gateway-check.ts new file mode 100644 index 00000000..b1ecf29c --- /dev/null +++ b/examples/simple/e2e/util/gateway-check.ts @@ -0,0 +1,13 @@ +import type { Moltin as EPCCClient } from "@moltin/sdk"; + +export async function gatewayCheck(client: EPCCClient): Promise { + try { + const gateways = await client.Gateways.All(); + const epPaymentGateway = gateways.data.find( + (gateway) => gateway.slug === "elastic_path_payments_stripe", + )?.enabled; + return !!epPaymentGateway; + } catch (err) { + return false; + } +} diff --git a/examples/simple/e2e/util/gateway-is-enabled.ts b/examples/simple/e2e/util/gateway-is-enabled.ts new file mode 100644 index 00000000..12e55d3a --- /dev/null +++ b/examples/simple/e2e/util/gateway-is-enabled.ts @@ -0,0 +1,10 @@ +import { test } from "@playwright/test"; +import { gatewayCheck } from "./gateway-check"; +import { adminClient } from "./epcc-admin-client"; + +export async function gatewayIsEnabled(): Promise { + test.skip( + !(await gatewayCheck(adminClient)), + "Skipping tests because they payment gateway is not enabled.", + ); +} diff --git a/examples/simple/e2e/util/get-cart-id.ts b/examples/simple/e2e/util/get-cart-id.ts new file mode 100644 index 00000000..2e015869 --- /dev/null +++ b/examples/simple/e2e/util/get-cart-id.ts @@ -0,0 +1,13 @@ +import { expect, Page } from "@playwright/test"; + +export function getCartId(page: Page) { + return async function _getCartId(): Promise { + /* Get the cart id from the cookie */ + const allCookies = await page.context().cookies(); + const cartId = allCookies.find((cookie) => cookie.name === "_store_ep_cart") + ?.value; + + expect(cartId).toBeDefined(); + return cartId!; + }; +} diff --git a/examples/simple/e2e/util/has-published-catalog.ts b/examples/simple/e2e/util/has-published-catalog.ts new file mode 100644 index 00000000..1dc49894 --- /dev/null +++ b/examples/simple/e2e/util/has-published-catalog.ts @@ -0,0 +1,12 @@ +import type { Moltin as EPCCClient } from "@moltin/sdk"; + +export async function hasPublishedCatalog( + client: EPCCClient, +): Promise { + try { + await client.ShopperCatalog.Get(); + return false; + } catch (err) { + return true; + } +} diff --git a/examples/simple/e2e/util/missing-published-catalog.ts b/examples/simple/e2e/util/missing-published-catalog.ts new file mode 100644 index 00000000..d48fec22 --- /dev/null +++ b/examples/simple/e2e/util/missing-published-catalog.ts @@ -0,0 +1,10 @@ +import { test } from "@playwright/test"; +import { hasPublishedCatalog } from "./has-published-catalog"; +import { client } from "./epcc-client"; + +export async function skipIfMissingCatalog(): Promise { + test.skip( + await hasPublishedCatalog(client), + "Skipping tests because there is no published catalog.", + ); +} diff --git a/examples/simple/e2e/util/resolver-product-from-store.ts b/examples/simple/e2e/util/resolver-product-from-store.ts new file mode 100644 index 00000000..75d4269f --- /dev/null +++ b/examples/simple/e2e/util/resolver-product-from-store.ts @@ -0,0 +1,84 @@ +import type { + Moltin as EPCCClient, + ShopperCatalogResourcePage, + ProductResponse, + ShopperCatalogResource, +} from "@moltin/sdk"; + +export async function getSimpleProduct( + client: EPCCClient, +): Promise { + const paginator = paginateShopperProducts(client, { limit: 100 }); + + if (paginator) { + for await (const page of paginator) { + const simpleProduct = page.data.find( + (x) => !x.attributes.base_product && !x.attributes.base_product_id, + ); + if (simpleProduct) { + return simpleProduct; + } + } + } +} + +export async function getProductById( + client: EPCCClient, + productId: string, +): Promise> { + return client.ShopperCatalog.Products.Get({ + productId: productId, + }); +} + +export async function getVariationsProduct( + client: EPCCClient, +): Promise { + const paginator = paginateShopperProducts(client, { limit: 100 }); + + if (paginator) { + for await (const page of paginator) { + const variationsProduct = page.data.find( + (x) => x.attributes.base_product, + ); + if (variationsProduct) { + return variationsProduct; + } + } + } +} + +const makePagedClientRequest = async ( + client: EPCCClient, + { limit = 100, offset }: { limit?: number; offset: number }, +): Promise> => { + return await client.ShopperCatalog.Products.Offset(offset).Limit(limit).All(); +}; + +export type Paginator = AsyncGenerator; + +export async function* paginateShopperProducts( + client: EPCCClient, + input: { limit?: number; offset?: number }, +): Paginator> | undefined { + let page: ShopperCatalogResourcePage; + + let nextOffset: number = input.offset ?? 0; + let hasNext = true; + + while (hasNext) { + page = await makePagedClientRequest(client, { + limit: input.limit, + offset: nextOffset, + }); + yield page; + const { + results: { total: totalItems }, + page: { current, limit }, + } = page.meta; + hasNext = current * limit < totalItems; + nextOffset = nextOffset + limit; + } + + return undefined; +} diff --git a/examples/simple/e2e/util/skip-ci-env.ts b/examples/simple/e2e/util/skip-ci-env.ts new file mode 100644 index 00000000..1f93e2d5 --- /dev/null +++ b/examples/simple/e2e/util/skip-ci-env.ts @@ -0,0 +1,8 @@ +import { test } from "@playwright/test"; + +export function skipIfCIEnvironment(): void { + test.skip( + process.env.CI === "true", + "Skipping tests because we are in a CI environment.", + ); +} diff --git a/examples/simple/license.md b/examples/simple/license.md new file mode 100644 index 00000000..714fa3a8 --- /dev/null +++ b/examples/simple/license.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Elastic Path Software Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/examples/simple/next-env.d.ts b/examples/simple/next-env.d.ts new file mode 100644 index 00000000..4f11a03d --- /dev/null +++ b/examples/simple/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/examples/simple/next.config.js b/examples/simple/next.config.js new file mode 100644 index 00000000..d825be37 --- /dev/null +++ b/examples/simple/next.config.js @@ -0,0 +1,34 @@ +// @ts-check + +/** + * @type {import('next').NextConfig} + **/ +const nextConfig = { + images: { + formats: ["image/avif", "image/webp"], + remotePatterns: [ + { + protocol: "https", + hostname: "**.epusercontent.com", + }, + ], + }, + i18n: { + locales: ["en"], + defaultLocale: "en", + }, + webpack(config) { + config.resolve.fallback = { + ...config.resolve.fallback, + fs: false, + }; + + return config; + }, +}; + +const withBundleAnalyzer = require("@next/bundle-analyzer")({ + enabled: process.env.ANALYZE === "true", +}); + +module.exports = withBundleAnalyzer(nextConfig); diff --git a/examples/simple/package.json b/examples/simple/package.json new file mode 100644 index 00000000..21980b9d --- /dev/null +++ b/examples/simple/package.json @@ -0,0 +1,73 @@ +{ + "name": "simple", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format:check": "prettier --check .", + "format:fix": "prettier --write .", + "type:check": "tsc --noEmit", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "test:watch": "vitest", + "test:ci:e2e": "NODE_ENV=test pnpm build && (pnpm start & (sleep 5 && npx playwright install --with-deps && pnpm test:e2e && kill $(lsof -t -i tcp:3000)))", + "test:e2e": "NODE_ENV=test playwright test", + "build:e2e": "NODE_ENV=test next build", + "start:e2e": "NODE_ENV=test next start" + }, + "dependencies": { + "@moltin/sdk": "^25.0.2", + "@elasticpath/react-shopper-hooks": "^0.5.1", + "@elasticpath/shopper-common": "^0.1.1", + "clsx": "^1.2.1", + "cookies-next": "^4.0.0", + "focus-visible": "^5.2.0", + "formik": "^2.2.9", + "next": "^14.0.0", + "server-only": "^0.0.1", + "pure-react-carousel": "^1.29.0", + "react": "^18.2.0", + "react-device-detect": "^2.2.2", + "react-dom": "^18.2.0", + "react-toastify": "^9.1.3", + "@heroicons/react": "^2.0.18", + "@headlessui/react": "^1.7.17", + "rc-slider": "^10.3.0", + "zod": "^3.22.4", + "zod-formik-adapter": "^1.2.0" + }, + "devDependencies": { + "@babel/core": "^7.18.10", + "@next/bundle-analyzer": "^14.0.0", + "@next/env": "^14.0.0", + "@svgr/webpack": "^6.3.1", + "@types/node": "18.7.3", + "@types/react": "^18.2.33", + "@types/react-dom": "^18.2.14", + "babel-loader": "^8.2.5", + "eslint": "^8.49.0", + "eslint-config-next": "^14.0.0", + "eslint-config-prettier": "^9.0.0", + "encoding": "^0.1.13", + "eslint-plugin-react": "^7.33.2", + "vite": "^4.2.1", + "vitest": "^0.34.5", + "@vitest/coverage-istanbul": "^0.34.5", + "@testing-library/jest-dom": "^6.1.3", + "@testing-library/react": "^14.0.0", + "@playwright/test": "^1.28.1", + "lint-staged": "^13.0.3", + "prettier": "^3.0.3", + "prettier-eslint": "^15.0.1", + "prettier-eslint-cli": "^7.1.0", + "typescript": "^5.2.2", + "tailwindcss": "^3.3.3", + "autoprefixer": "^10.4.14", + "postcss": "^8.4.30", + "prettier-plugin-tailwindcss": "^0.5.4" + } +} diff --git a/examples/simple/playwright.config.ts b/examples/simple/playwright.config.ts new file mode 100644 index 00000000..67514457 --- /dev/null +++ b/examples/simple/playwright.config.ts @@ -0,0 +1,82 @@ +import { PlaywrightTestConfig, devices } from "@playwright/test"; +import { join } from "path"; +import { loadEnvConfig } from "@next/env"; + +loadEnvConfig(process.env.PWD!); + +// Use process.env.PORT by default and fallback to port 3000 +const PORT = process.env.PORT || 3000; + +// Set webServer.url and use.baseURL with the location of the WebServer respecting the correct set port +const baseURL = `http://localhost:${PORT}`; + +// Reference: https://playwright.dev/docs/test-configuration +const config: PlaywrightTestConfig = { + // Timeout per test + timeout: 15 * 1000, + // Test directory + testDir: join(__dirname, "e2e"), + // If a test fails, retry it additional 2 times + retries: 2, + // Artifacts folder where screenshots, videos, and traces are stored. + outputDir: "test-results/", + + // Run your local dev server before starting the tests: + // https://playwright.dev/docs/test-advanced#launching-a-development-web-server-during-the-tests + // webServer: { + // command: 'yarn run dev', + // url: baseURL, + // timeout: 120 * 1000, + // reuseExistingServer: !process.env.CI, + // }, + + use: { + // Use baseURL so to make navigations relative. + // More information: https://playwright.dev/docs/api/class-testoptions#test-options-base-url + baseURL, + + screenshot: "only-on-failure", + + // Retry a test if its failing with enabled tracing. This allows you to analyse the DOM, console logs, network traffic etc. + // More information: https://playwright.dev/docs/trace-viewer + trace: "retry-with-trace", + + // All available context options: https://playwright.dev/docs/api/class-browser#browser-new-context + // contextOptions: { + // ignoreHTTPSErrors: true, + // }, + }, + + projects: [ + { + name: "Desktop Chrome", + use: { + ...devices["Desktop Chrome"], + }, + }, + { + name: "Desktop Firefox", + use: { + ...devices["Desktop Firefox"], + }, + }, + { + name: "Desktop Safari", + use: { + ...devices["Desktop Safari"], + }, + }, + // Test against mobile viewports. + { + name: "Mobile Chrome", + use: { + ...devices["Pixel 5"], + }, + }, + { + name: "Mobile Safari", + use: devices["iPhone 12"], + }, + ], +}; +export default config; diff --git a/examples/simple/postcss.config.js b/examples/simple/postcss.config.js new file mode 100644 index 00000000..12a703d9 --- /dev/null +++ b/examples/simple/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/simple/public/favicon.ico b/examples/simple/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a61f60f199cfd37e307bfb0ee3cd285f35d733e8 GIT binary patch literal 15086 zcmd^GONbmr7_N2IC@P7H_yE~KBb%A3T~}8`4>CE3B6v~QgBPD5KJX9_HOY2QpK|ab zJ`M^#qA11)isIuSxa%6DPIqr`qbwrGViW||7(sNSrXp`zmWcJu=bM9 zT;&wt{{g8~b`Nv0J#jDQhFjoI*M^hj*RHYriRoU6#(;1edA`%!1pBu_eq`1TedX+- zDt3{@dC&)+mKwjyA1yO}%jLJ08h>-5US|A`SG~N{`2FftWyWv&;-)g=$2#J^GUIQ! z{IN3Q$Nc4&760z0xDeReWyO!!#QnBkyTkTsGjS94p{EyN^`qC%Kf?A4MZuKBF19On zhy3OM#k!Kz&$BGWH-Y`P5Pn)Om!coE-*{cv&3gaiBKwjf*$J4hUhjk5a44iN&Fr5U z1@65O*cAuFe2erw5cY=0UyOxaohM0(TNi=DK-deyLHUe$Ig#BBb|qJ>W{u}HRyqgy zSy|KSN3bi=rMW?4ui|!G`I~9&8y86 z&sIk&I~co2Hl}zubtR1*ja`kMmEGx{Rt0H0QsRi)Zy4<*X+!J& zUd^wBJ3D(s< ze<>1!D?UnVKOBi6Mcu{{gaPL_?|0k5^2G-`4qkn#DVL_C{)PUp4Tdar8(!F}ULWX( z&FQ|F$a9N}r&tI>Vfo@sIXf;91f|81rZ`pAVe_3!#joD01h#gbFiPu!q?wGVfS z%VeSd%m2ClpY!6s%d4Ib-MRT6;^B8~z&&;U!(IF*9h}AXy0b!gv+m@0THn9piR(1? zce1`8L-RXycfLEz6K~$_O`nt&*LCemHiEU#Yd!iseLFxm?k+vmLpS*vrSvdF>kcrW zFWybVAHX~2of~m@=jYqbUe{+s_`_P{m%_e3ZD4!izf5D&5Y+W-A_7Au_X+!8$V9g> zisOsPP+w%Ky9>j@Fz$-WzY({fPz8l4C`3Ww2?|ZjBdy^581mK{QnsWF%C<+O+$d3d zRF>*cU222c3fjaw=^$OC6J;q*K^wLTQYTJ<-uI_S(7ujJwYwiy6x6(Kwti$ zzALdmNgLV^j;p$Q5A5=NvD=_A%@a?A>(lSsXpa?_J6k^=?4|S0*mZTm;LODp&(pZ| z$kN1_l#-624ZrqWn1|(xJCoqR__`y{`tY9@2GvjYLj70hcPC~amgF20J`kMO;a<`+ z)Srp$=7~LF+<1RBt`F$3^v`hpq-z;`12?0-X=9-IpV}b3Q9jp3?6%clY^|;my;EL$ N3^8V%=)GULe*oDd7EAyD literal 0 HcmV?d00001 diff --git a/examples/simple/src/app/about/page.tsx b/examples/simple/src/app/about/page.tsx new file mode 100644 index 00000000..edda2df8 --- /dev/null +++ b/examples/simple/src/app/about/page.tsx @@ -0,0 +1,5 @@ +import Blurb from "../../components/shared/blurb"; + +export default function About() { + return ; +} diff --git a/examples/simple/src/app/cart/page.tsx b/examples/simple/src/app/cart/page.tsx new file mode 100644 index 00000000..4dc53f4b --- /dev/null +++ b/examples/simple/src/app/cart/page.tsx @@ -0,0 +1,42 @@ +"use client"; +import Cart from "../../components/cart/Cart"; +import CartIcon from "../../components/icons/cart"; +import { useCart } from "@elasticpath/react-shopper-hooks"; +import { resolveShoppingCartProps } from "../../lib/resolve-shopping-cart-props"; +import Link from "next/link"; + +export default function CartPage() { + const { removeCartItem, state } = useCart(); + const shoppingCartProps = resolveShoppingCartProps(state, removeCartItem); + + return ( +
+ {shoppingCartProps && ( + <> +

Your Shopping Cart

+ + + )} + {(state.kind === "empty-cart-state" || + state.kind === "uninitialised-cart-state" || + state.kind === "loading-cart-state") && ( +
+ +

+ Empty Cart +

+

Your cart is empty

+
+ + Start shopping + +
+
+ )} +
+ ); +} diff --git a/examples/simple/src/app/checkout/[cartId]/cart-display.tsx b/examples/simple/src/app/checkout/[cartId]/cart-display.tsx new file mode 100644 index 00000000..c06693bf --- /dev/null +++ b/examples/simple/src/app/checkout/[cartId]/cart-display.tsx @@ -0,0 +1,75 @@ +"use client"; +import { + getPresentCartStateCheckout, + PresentCartState, + useCart, +} from "@elasticpath/react-shopper-hooks"; +import { ReactElement, useCallback, useState } from "react"; +import type { + Cart, + CartIncluded, + ConfirmPaymentResponse, + ResourceIncluded, +} from "@moltin/sdk"; +import { OrderCompleteState } from "../../../components/checkout/types/order-pending-state"; +import OrderComplete from "../../../components/checkout/OrderComplete"; +import CheckoutForm from "../../../components/checkout/payments/CheckoutForm"; +import { OrderSummary } from "../../../components/checkout/OrderSummary"; +import { CheckoutForm as CheckoutFormType } from "../../../components/checkout/form-schema/checkout-form-schema"; + +export function CartDisplay(_props: { + cart: ResourceIncluded; +}): ReactElement { + const { state } = useCart(); + const [orderCompleteState, setOrderCompleteState] = useState< + OrderCompleteState | undefined + >(undefined); + + const showCompletedOrder = useCallback( + function (cart: PresentCartState) { + return ( + paymentResponse: ConfirmPaymentResponse, + checkoutForm: CheckoutFormType, + ): void => { + setOrderCompleteState({ + paymentResponse, + checkoutForm, + cart, + }); + window.scrollTo({ top: 0, left: 0 }); + }; + }, + [setOrderCompleteState], + ); + + const presentCart = getPresentCartStateCheckout(state); + + return ( +
+ {orderCompleteState ? ( + + ) : ( + <> +

Checkout

+ {presentCart && ( +
+
+ +
+
+ +
+
+ )} + + )} +
+ ); +} diff --git a/examples/simple/src/app/checkout/[cartId]/page.tsx b/examples/simple/src/app/checkout/[cartId]/page.tsx new file mode 100644 index 00000000..cfabc84e --- /dev/null +++ b/examples/simple/src/app/checkout/[cartId]/page.tsx @@ -0,0 +1,28 @@ +import { Metadata } from "next"; +import { getServerSideImplicitClient } from "../../../lib/epcc-server-side-implicit-client"; +import { notFound } from "next/navigation"; +import { CartDisplay } from "./cart-display"; +import { getCart } from "../../../services/cart"; + +type Props = { + params: { cartId: string }; +}; +export const metadata: Metadata = { + title: "Checkout", + description: "Checkout page", +}; +export default async function CartPage({ params }: Props) { + const { cartId } = params; + + if (!cartId) { + notFound(); + } + const client = getServerSideImplicitClient(); + const cart = await getCart(cartId, client); + + if (!cart) { + notFound(); + } + + return ; +} diff --git a/examples/simple/src/app/configuration-error/page.tsx b/examples/simple/src/app/configuration-error/page.tsx new file mode 100644 index 00000000..5b9a5dba --- /dev/null +++ b/examples/simple/src/app/configuration-error/page.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "Configuration Error", + description: "Configuration error page", +}; + +type Props = { + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export default function ConfigurationErrorPage({ searchParams }: Props) { + const { + "missing-env-variable": missingEnvVariables, + authentication, + from, + } = searchParams; + + const issues: { [key: string]: string | string[] } = { + ...(missingEnvVariables && { missingEnvVariables }), + ...(authentication && { authentication }), + }; + const fromProcessed = Array.isArray(from) ? from[0] : from; + + return ( +
+ + There is a problem with the stores setup + + + Refresh + + + + + + + + + + {issues && + Object.keys(issues).map((key) => { + const issue = issues[key]; + return ( + + + + + ); + })} + +
IssueDetails
{key} +
    + {(Array.isArray(issue) ? issue : [issue]).map( + (message) => ( +
  • + {decodeURIComponent(message)} +
  • + ), + )} +
+
+
+ ); +} diff --git a/examples/simple/src/app/error.tsx b/examples/simple/src/app/error.tsx new file mode 100644 index 00000000..f4724026 --- /dev/null +++ b/examples/simple/src/app/error.tsx @@ -0,0 +1,31 @@ +"use client"; +import Link from "next/link"; + +export default function GlobalError({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( + + +
+ + {error.digest} - Internal server error. + + + Back to home + + +
+ + + ); +} diff --git a/examples/simple/src/app/faq/page.tsx b/examples/simple/src/app/faq/page.tsx new file mode 100644 index 00000000..7aac9fba --- /dev/null +++ b/examples/simple/src/app/faq/page.tsx @@ -0,0 +1,5 @@ +import Blurb from "../../components/shared/blurb"; + +export default function FAQ() { + return ; +} diff --git a/examples/simple/src/app/layout.tsx b/examples/simple/src/app/layout.tsx new file mode 100644 index 00000000..b4b06802 --- /dev/null +++ b/examples/simple/src/app/layout.tsx @@ -0,0 +1,59 @@ +import { Inter } from "next/font/google"; +import { ReactNode, Suspense } from "react"; +import "../styles/globals.css"; +import Header from "../components/header/Header"; +import { getStoreContext } from "../lib/get-store-context"; +import { getServerSideImplicitClient } from "../lib/epcc-server-side-implicit-client"; +import { Providers } from "./providers"; +import { Toaster } from "../components/toast/toaster"; +import Footer from "../components/footer/Footer"; + +const { SITE_NAME } = process.env; +const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL + ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` + : "http://localhost:3000"; + +export const metadata = { + metadataBase: new URL(baseUrl), + title: { + default: SITE_NAME!, + template: `%s | ${SITE_NAME}`, + }, + robots: { + follow: true, + index: true, + }, +}; + +const inter = Inter({ + subsets: ["latin"], + display: "swap", + variable: "--font-inter", +}); + +export default async function RootLayout({ + children, +}: { + children: ReactNode; +}) { + const client = getServerSideImplicitClient(); + const storeContext = await getStoreContext(client); + + return ( + + + {/* headless ui needs this div - https://github.com/tailwindlabs/headlessui/issues/2752#issuecomment-1745272229 */} +
+ +
+ + +
{children}
+
+
+ +
+ + + ); +} diff --git a/examples/simple/src/app/not-found.tsx b/examples/simple/src/app/not-found.tsx new file mode 100644 index 00000000..d97d88f4 --- /dev/null +++ b/examples/simple/src/app/not-found.tsx @@ -0,0 +1,13 @@ +import Link from "next/link"; +export default function NotFound() { + return ( +
+ + 404 - The page could not be found. + + + Back to home + +
+ ); +} diff --git a/examples/simple/src/app/page.tsx b/examples/simple/src/app/page.tsx new file mode 100644 index 00000000..dfd06746 --- /dev/null +++ b/examples/simple/src/app/page.tsx @@ -0,0 +1,38 @@ +import PromotionBanner from "../components/promotion-banner/PromotionBanner"; +import FeaturedProducts from "../components/featured-products/FeaturedProducts"; +import { Suspense } from "react"; + +export default async function Home() { + const promotion = { + title: "Your Elastic Path storefront", + description: + "This marks the beginning, embark on the journey of crafting something truly extraordinary, uniquely yours.", + }; + + return ( +
+ +
+
+
+ + + +
+
+
+
+ ); +} diff --git a/examples/simple/src/app/products/[productId]/page.tsx b/examples/simple/src/app/products/[productId]/page.tsx new file mode 100644 index 00000000..f0a54aed --- /dev/null +++ b/examples/simple/src/app/products/[productId]/page.tsx @@ -0,0 +1,48 @@ +import { Metadata } from "next"; +import { ProductDisplay } from "./product-display"; +import { getServerSideImplicitClient } from "../../../lib/epcc-server-side-implicit-client"; +import { getProductById } from "../../../services/products"; +import { notFound } from "next/navigation"; +import { parseProductResponse } from "@elasticpath/shopper-common"; + +export const dynamic = "force-dynamic"; + +type Props = { + params: { productId: string }; +}; + +export async function generateMetadata({ + params: { productId }, +}: Props): Promise { + const client = getServerSideImplicitClient(); + const product = await getProductById(productId, client); + + if (!product) { + notFound(); + } + + return { + title: product.data.attributes.name, + description: product.data.attributes.description, + }; +} + +export default async function ProductPage({ params }: Props) { + const client = getServerSideImplicitClient(); + const product = await getProductById(params.productId, client); + + if (!product) { + notFound(); + } + + const shopperProduct = await parseProductResponse(product, client); + + return ( +
+ +
+ ); +} diff --git a/examples/simple/src/app/products/[productId]/product-display.tsx b/examples/simple/src/app/products/[productId]/product-display.tsx new file mode 100644 index 00000000..2664951b --- /dev/null +++ b/examples/simple/src/app/products/[productId]/product-display.tsx @@ -0,0 +1,39 @@ +"use client"; +import React, { ReactElement, useState } from "react"; +import { ShopperProduct } from "@elasticpath/react-shopper-hooks"; +import { VariationProductDetail } from "../../../components/product/variations/VariationProduct"; +import BundleProductDetail from "../../../components/product/bundles/BundleProduct"; +import { ProductContext } from "../../../lib/product-context"; +import SimpleProductDetail from "../../../components/product/SimpleProduct"; + +export function ProductDisplay({ + product, +}: { + product: ShopperProduct; +}): ReactElement { + const [isChangingSku, setIsChangingSku] = useState(false); + + return ( + + {resolveProductDetailComponent(product)} + + ); +} + +function resolveProductDetailComponent(product: ShopperProduct): JSX.Element { + switch (product.kind) { + case "base-product": + return ; + case "child-product": + return ; + case "simple-product": + return ; + case "bundle-product": + return ; + } +} diff --git a/examples/simple/src/app/providers.tsx b/examples/simple/src/app/providers.tsx new file mode 100644 index 00000000..5bcdc503 --- /dev/null +++ b/examples/simple/src/app/providers.tsx @@ -0,0 +1,17 @@ +"use client"; + +import StoreNextJSProvider from "../lib/providers/store-provider"; +import { ReactNode } from "react"; +import { StoreContext } from "@elasticpath/react-shopper-hooks"; + +export function Providers({ + children, + store, +}: { + children: ReactNode; + store: StoreContext; +}) { + return ( + {children} + ); +} diff --git a/examples/simple/src/app/shipping/page.tsx b/examples/simple/src/app/shipping/page.tsx new file mode 100644 index 00000000..7be31b80 --- /dev/null +++ b/examples/simple/src/app/shipping/page.tsx @@ -0,0 +1,5 @@ +import Blurb from "../../components/shared/blurb"; + +export default function Shipping() { + return ; +} diff --git a/examples/simple/src/app/terms/page.tsx b/examples/simple/src/app/terms/page.tsx new file mode 100644 index 00000000..8efeb5e7 --- /dev/null +++ b/examples/simple/src/app/terms/page.tsx @@ -0,0 +1,5 @@ +import Blurb from "../../components/shared/blurb"; + +export default function Terms() { + return ; +} diff --git a/examples/simple/src/components/NoImage.tsx b/examples/simple/src/components/NoImage.tsx new file mode 100644 index 00000000..bf84f253 --- /dev/null +++ b/examples/simple/src/components/NoImage.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { EyeSlashIcon } from "@heroicons/react/24/solid"; + +export const NoImage = (): JSX.Element => { + return ( +
+ +
+ ); +}; + +export default NoImage; diff --git a/examples/simple/src/components/Spinner.tsx b/examples/simple/src/components/Spinner.tsx new file mode 100644 index 00000000..9bd90a3e --- /dev/null +++ b/examples/simple/src/components/Spinner.tsx @@ -0,0 +1,30 @@ +interface IProps { + width: string; + height: string; + absolute: boolean; +} + +const Spinner = (props: IProps) => { + return ( + + ); +}; + +export default Spinner; diff --git a/examples/simple/src/components/cart/Cart.tsx b/examples/simple/src/components/cart/Cart.tsx new file mode 100644 index 00000000..ebe3e097 --- /dev/null +++ b/examples/simple/src/components/cart/Cart.tsx @@ -0,0 +1,38 @@ +import { + GroupedCartItems, + RefinedCartItem, +} from "@elasticpath/react-shopper-hooks"; +import { CartItemList } from "./CartItemList"; +import { CartOrderSummary } from "./CartOrderSummary"; +import { ReadonlyNonEmptyArray } from "../../lib/types/read-only-non-empty-array"; + +export interface ICart { + id: string; + items: ReadonlyNonEmptyArray; + groupedItems: GroupedCartItems; + totalPrice: string; + subtotal: string; + removeCartItem: (itemId: string) => Promise; +} + +export default function Cart({ + id, + items, + groupedItems, + totalPrice, + subtotal, + removeCartItem, +}: ICart): JSX.Element { + return ( +
+ + +
+ ); +} diff --git a/examples/simple/src/components/cart/CartItemList.tsx b/examples/simple/src/components/cart/CartItemList.tsx new file mode 100644 index 00000000..71c8c0dc --- /dev/null +++ b/examples/simple/src/components/cart/CartItemList.tsx @@ -0,0 +1,65 @@ +import { RefinedCartItem } from "@elasticpath/react-shopper-hooks"; +import QuantityHandler from "../quantity-handler/QuantityHandler"; +import { NonEmptyArray } from "../../lib/types/non-empty-array"; +import { ReadonlyNonEmptyArray } from "../../lib/types/read-only-non-empty-array"; +import Image from "next/image"; +import { XMarkIcon } from "@heroicons/react/24/solid"; + +export function CartItemList({ + items, + handleRemoveItem, +}: { + items: + | RefinedCartItem[] + | NonEmptyArray + | ReadonlyNonEmptyArray; + handleRemoveItem: (itemId: string) => Promise; +}): JSX.Element { + return ( +
+ {items.map((item) => ( +
+
+ {item.image?.href && ( + {item.name} + )} +
+ +
+ + {item.name} + + + {item.meta.display_price.without_tax.unit.formatted} + +
+
+ + { + handleRemoveItem(item.id); + }} + /> +
+
+ ))} +
+ ); +} diff --git a/examples/simple/src/components/cart/CartOrderSummary.tsx b/examples/simple/src/components/cart/CartOrderSummary.tsx new file mode 100644 index 00000000..891304ef --- /dev/null +++ b/examples/simple/src/components/cart/CartOrderSummary.tsx @@ -0,0 +1,80 @@ +import { PromotionCartItem } from "@elasticpath/react-shopper-hooks"; +import { Promotion } from "./Promotion"; +import Link from "next/link"; + +export function CartOrderSummary({ + cartId, + totalPrice, + subtotal, + promotionItems, + handleRemoveItem, +}: { + cartId: string; + totalPrice: string; + subtotal: string; + promotionItems: PromotionCartItem[]; + handleRemoveItem: (itemId: string) => Promise; +}): JSX.Element { + return ( +
+ Order Summary + + + + + + + {/* Couldn't find any promotional items */} + {promotionItems?.map((item) => { + return ( + + + + + ); + })} + + + + + +
Subtotal{subtotal}
+
+ Discount + {item.sku} +
+
+ {promotionItems && promotionItems.length > 0 ? ( +
+ + { + promotionItems[0].meta.display_price.without_tax.unit + .formatted + } + + +
+ ) : ( + "$0.00" + )} +
Order Total{totalPrice}
+ +
+ +
+
+ + + + + + +
+
+ ); +} diff --git a/examples/simple/src/components/cart/Promotion.tsx b/examples/simple/src/components/cart/Promotion.tsx new file mode 100644 index 00000000..19906809 --- /dev/null +++ b/examples/simple/src/components/cart/Promotion.tsx @@ -0,0 +1,59 @@ +import { useFormik } from "formik"; +import { useCart } from "@elasticpath/react-shopper-hooks"; + +interface FormValues { + promoCode: string; +} + +export const Promotion = (): JSX.Element => { + const { addPromotionToCart, state } = useCart(); + + const initialValues: FormValues = { + promoCode: "", + }; + + const { handleSubmit, handleChange, values } = useFormik({ + initialValues, + onSubmit: async (values) => { + await addPromotionToCart(values.promoCode); + // TODO handle invalid promo code setErrors(error.errors[0].detail); + }, + }); + + const shouldDisableInput = + state.kind !== "present-cart-state" || + state.groupedItems.promotion.length > 0; + + return ( +
+
+
+
+ +
+ + +
+
+
+
+
+ ); +}; diff --git a/examples/simple/src/components/checkout/BillingForm.tsx b/examples/simple/src/components/checkout/BillingForm.tsx new file mode 100644 index 00000000..e5348da7 --- /dev/null +++ b/examples/simple/src/components/checkout/BillingForm.tsx @@ -0,0 +1,93 @@ +import CustomFormControl from "./CustomFormControl"; +import CountrySelect from "./CountrySelect"; + +export default function BillingForm(): JSX.Element { + return ( +
+
+ + +
+ + +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/examples/simple/src/components/checkout/CountrySelect.tsx b/examples/simple/src/components/checkout/CountrySelect.tsx new file mode 100644 index 00000000..b5658b2b --- /dev/null +++ b/examples/simple/src/components/checkout/CountrySelect.tsx @@ -0,0 +1,262 @@ +import CustomFormSelect, { ISelectField } from "./CustomFormSelect"; + +interface ICountrySelect extends Omit {} + +export default function CountrySelect(props: ICountrySelect): JSX.Element { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/examples/simple/src/components/checkout/CustomFormControl.tsx b/examples/simple/src/components/checkout/CustomFormControl.tsx new file mode 100644 index 00000000..164b6eeb --- /dev/null +++ b/examples/simple/src/components/checkout/CustomFormControl.tsx @@ -0,0 +1,46 @@ +import { useField } from "formik"; +import clsx from "clsx"; + +interface ITextField { + id: string; + type: string; + label: string; + name: string; + autoComplete: string; + isRequired?: boolean; + helperText?: string; +} + +export default function CustomFormControl({ + label, + isRequired = false, + helperText, + ...props +}: ITextField): JSX.Element { + const [field, meta] = useField(props as any); + + return ( +
+ + + {helperText && {helperText}} + {meta.touched && meta.error ? ( + {meta.error} + ) : null} +
+ ); +} diff --git a/examples/simple/src/components/checkout/CustomFormSelect.tsx b/examples/simple/src/components/checkout/CustomFormSelect.tsx new file mode 100644 index 00000000..6b26edd9 --- /dev/null +++ b/examples/simple/src/components/checkout/CustomFormSelect.tsx @@ -0,0 +1,50 @@ +import clsx from "clsx"; +import { useField } from "formik"; +import { ReactNode } from "react"; + +export interface ISelectField { + id: string; + name: string; + autoComplete: string; + label: string; + isRequired?: boolean; + placeholder?: string; + children: ReactNode[]; +} + +export default function CustomFormSelect({ + label, + isRequired = false, + children, + placeholder, + ...props +}: ISelectField): JSX.Element { + const [field, meta] = useField(props as any); + + return ( +
+ + + {meta.touched && meta.error ? ( + {meta.error} + ) : null} +
+ ); +} diff --git a/examples/simple/src/components/checkout/OrderComplete.tsx b/examples/simple/src/components/checkout/OrderComplete.tsx new file mode 100644 index 00000000..f7edfe6f --- /dev/null +++ b/examples/simple/src/components/checkout/OrderComplete.tsx @@ -0,0 +1,173 @@ +import Link from "next/link"; +import { OrderCompleteState } from "./types/order-pending-state"; +import Image from "next/image"; +import NextLink from "next/link"; +import { PresentCartState } from "@elasticpath/react-shopper-hooks"; +import * as React from "react"; + +interface IOrderComplete { + state: OrderCompleteState; +} + +export default function OrderComplete({ + state: { + paymentResponse, + cart, + checkoutForm: { + shippingAddress, + billingAddress, + personal: { email }, + }, + }, +}: IOrderComplete): JSX.Element { + return ( +
+

Thank you for your order!

+

Order Complete

+ + Your order number: #{paymentResponse.data.id} + +
+
+ {cart.items.map((item) => { + return ( +
+
+ + {item.name} + +
+
+ {item.name} + + {item.description} + +
+ {`Quantity ${item.quantity}`} +
+ + {`Price ${item.meta.display_price.with_tax.value.formatted}`} + +
+
+
+ ); + })} +
+
+
+ + +
+ Contact Information + {email} +
+
+
+ +
+ + + +
+
+ ); +} + +interface IAddressBlock { + label: string; + first_name: string; + last_name: string; + line_1: string; + line_2?: string; + postcode: string; + region: string; +} + +function AddressBlock({ + label, + first_name, + last_name, + region, + line_1, + line_2, + postcode, +}: IAddressBlock): JSX.Element { + return ( +
+ {label} + {`${first_name} ${last_name}`} + {line_1} + {line_2} + {postcode} + {region} +
+ ); +} + +interface ICompleteOrderSummary { + cart: PresentCartState; +} + +function CompleteOrderSummary({ + cart: { + withTax, + withoutTax, + groupedItems: { promotion }, + }, +}: ICompleteOrderSummary): JSX.Element { + return ( + + + + + + + + + + + + + + + +
Subtotal{withoutTax}
+
+ Discount + {promotion.length > 0 && ( + + ( {promotion[0].sku} ) + + )} +
+
+ {promotion && promotion.length > 0 ? ( +
+ + {promotion[0].meta.display_price.without_tax.unit.formatted} + + +
+ ) : ( + "$0.00" + )} +
Order Total{withTax}
+ ); +} diff --git a/examples/simple/src/components/checkout/OrderSummary.tsx b/examples/simple/src/components/checkout/OrderSummary.tsx new file mode 100644 index 00000000..35f706cc --- /dev/null +++ b/examples/simple/src/components/checkout/OrderSummary.tsx @@ -0,0 +1,106 @@ +import { + PromotionCartItem, + RefinedCartItem, +} from "@elasticpath/react-shopper-hooks"; +import { NonEmptyArray } from "../../lib/types/non-empty-array"; +import { ReadonlyNonEmptyArray } from "../../lib/types/read-only-non-empty-array"; +import Link from "next/link"; +import Image from "next/image"; + +interface IOrderSummary { + items: + | RefinedCartItem[] + | NonEmptyArray + | ReadonlyNonEmptyArray; + promotionItems: PromotionCartItem[]; + totalPrice: string; + subtotal: string; +} + +export function OrderSummary({ + items, + promotionItems, + totalPrice, + subtotal, +}: IOrderSummary): JSX.Element { + return ( +
+ Order Summary + {items.map((item) => ( +
+
+ {item.image?.href && ( + + {item.name} + + )} +
+
+ + {item.name} + + + {item.meta.display_price.without_tax.value.formatted} + + + Qty {item.quantity} + +
+
+ ))} +
+ + + + + + + {/* Promotional items, unsure */} + + + + + + + + + +
Subtotal{subtotal}
+
+ Discount + {promotionItems && promotionItems.length > 0 && ( + + ( {promotionItems[0].sku} ) + + )} +
+
+ {promotionItems && promotionItems.length > 0 ? ( +
+ + { + promotionItems[0].meta.display_price.without_tax.unit + .formatted + } + + +
+ ) : ( + "$0.00" + )} +
Order Total{totalPrice}
+
+ ); +} diff --git a/examples/simple/src/components/checkout/ShippingForm.tsx b/examples/simple/src/components/checkout/ShippingForm.tsx new file mode 100644 index 00000000..8d7a786a --- /dev/null +++ b/examples/simple/src/components/checkout/ShippingForm.tsx @@ -0,0 +1,109 @@ +import CustomFormControl from "./CustomFormControl"; +import CountrySelect from "./CountrySelect"; + +export default function ShippingForm(): JSX.Element { + return ( +
+
+ + +
+ + +
+ + +
+ +
+ + +
+ + +
+ ); +} diff --git a/examples/simple/src/components/checkout/form-schema/checkout-form-schema.ts b/examples/simple/src/components/checkout/form-schema/checkout-form-schema.ts new file mode 100644 index 00000000..8d333280 --- /dev/null +++ b/examples/simple/src/components/checkout/form-schema/checkout-form-schema.ts @@ -0,0 +1,50 @@ +import { z } from "zod"; + +const personalInformationSchema = z.object({ + email: z.string({ required_error: "Required" }).email("Invalid email"), +}); + +const billingAddressSchema = z.object({ + first_name: z + .string({ required_error: "You need to provided a first name." }) + .min(2), + last_name: z + .string({ required_error: "You need to provided a last name." }) + .min(2), + company_name: z.string().min(1).optional(), + line_1: z + .string({ required_error: "You need to provided an address." }) + .min(1), + line_2: z.string().min(1).optional(), + city: z.string().min(1).optional(), + county: z.string().min(1).optional(), + region: z.string({ required_error: "You need to provided a region." }).min(1), + postcode: z + .string({ required_error: "You need to provided a postcode." }) + .min(1), + country: z + .string({ required_error: "You need to provided a country." }) + .min(1), +}); + +const shippingAddressSchema = z + .object({ + phone_number: z + .string() + .regex( + /^(\+?\d{0,4})?\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{3}\)?)\s?-?\s?(\(?\d{4}\)?)?$/, + "Phone number is not valid", + ) + .optional(), + instructions: z.string().min(1).optional(), + }) + .merge(billingAddressSchema); + +export const checkoutFormSchema = z.object({ + personal: personalInformationSchema, + shippingAddress: shippingAddressSchema, + sameAsShipping: z.boolean().default(true), + billingAddress: billingAddressSchema.optional(), +}); + +export type CheckoutForm = z.TypeOf; diff --git a/examples/simple/src/components/checkout/payments/CheckoutForm.tsx b/examples/simple/src/components/checkout/payments/CheckoutForm.tsx new file mode 100644 index 00000000..0878af72 --- /dev/null +++ b/examples/simple/src/components/checkout/payments/CheckoutForm.tsx @@ -0,0 +1,137 @@ +import { Form, Formik } from "formik"; +import { + CheckoutForm as CheckoutFormType, + checkoutFormSchema, +} from "../form-schema/checkout-form-schema"; +import { PaymentRequestBody } from "@moltin/sdk"; +import ShippingForm from "../ShippingForm"; +import CustomFormControl from "../CustomFormControl"; +import BillingForm from "../BillingForm"; +import { useCart, useStore } from "@elasticpath/react-shopper-hooks"; +import { CheckoutFormComponent } from "../types/checkout-form"; +import { toFormikValidationSchema } from "zod-formik-adapter"; + +const initialValues: Partial = { + personal: { + email: "", + }, + sameAsShipping: true, + shippingAddress: { + first_name: "", + last_name: "", + line_1: "", + country: "", + region: "", + postcode: "", + }, +}; + +const CheckoutForm: CheckoutFormComponent = ({ + showCompletedOrder, +}): JSX.Element => { + const { client } = useStore(); + const { checkout } = useCart(); + + return ( + <> + { + const { + personal, + shippingAddress, + billingAddress, + sameAsShipping, + } = validatedValues; + + const payment: PaymentRequestBody = { + gateway: "manual", + method: "purchase", + }; + + const orderResponse = await checkout( + personal.email, + shippingAddress, + payment, + sameAsShipping, + billingAddress ?? undefined, + ); + + showCompletedOrder(orderResponse, validatedValues); + }} + > + {({ handleChange, values, isSubmitting }) => ( +
+
+
+

+ {" "} + Contact Information +

+ +
+
+

Shipping Address

+ + +
+ {!values.sameAsShipping && } + + +
+
+ )} +
+ + ); +}; + +export default CheckoutForm; diff --git a/examples/simple/src/components/checkout/types/checkout-form.ts b/examples/simple/src/components/checkout/types/checkout-form.ts new file mode 100644 index 00000000..03f9c643 --- /dev/null +++ b/examples/simple/src/components/checkout/types/checkout-form.ts @@ -0,0 +1,11 @@ +import { ConfirmPaymentResponse } from "@moltin/sdk"; +import { CheckoutForm as CheckoutFormType } from "../form-schema/checkout-form-schema"; + +export interface ICheckoutForm { + showCompletedOrder: ( + paymentResponse: ConfirmPaymentResponse, + checkoutForm: CheckoutFormType, + ) => void; +} + +export type CheckoutFormComponent = (props: ICheckoutForm) => JSX.Element; diff --git a/examples/simple/src/components/checkout/types/order-pending-state.ts b/examples/simple/src/components/checkout/types/order-pending-state.ts new file mode 100644 index 00000000..9cd83a59 --- /dev/null +++ b/examples/simple/src/components/checkout/types/order-pending-state.ts @@ -0,0 +1,9 @@ +import { PresentCartState } from "@elasticpath/react-shopper-hooks"; +import { CheckoutForm as CheckoutFormType } from "../form-schema/checkout-form-schema"; +import { ConfirmPaymentResponse } from "@moltin/sdk"; + +export type OrderCompleteState = { + paymentResponse: ConfirmPaymentResponse; + cart: PresentCartState; + checkoutForm: CheckoutFormType; +}; diff --git a/examples/simple/src/components/featured-products/FeaturedProducts.tsx b/examples/simple/src/components/featured-products/FeaturedProducts.tsx new file mode 100644 index 00000000..d8ca7240 --- /dev/null +++ b/examples/simple/src/components/featured-products/FeaturedProducts.tsx @@ -0,0 +1,86 @@ +"use server"; +import clsx from "clsx"; +import Link from "next/link"; +import { ArrowRightIcon, EyeSlashIcon } from "@heroicons/react/24/outline"; +import Image from "next/image"; +import { getServerSideImplicitClient } from "../../lib/epcc-server-side-implicit-client"; +import { fetchFeaturedProducts } from "./fetchFeaturedProducts"; + +interface IFeaturedProductsProps { + title: string; + linkProps?: { + link: string; + text: string; + }; +} + +export default async function FeaturedProducts({ + title, + linkProps, +}: IFeaturedProductsProps) { + const client = getServerSideImplicitClient(); + const products = await fetchFeaturedProducts(client); + + return ( +
+
+

+ {title} +

+ {linkProps && ( + + + {linkProps.text} + + + )} +
+
    + {products.map((product) => ( + +
  • +
    +
    + {product.main_image?.link.href ? ( + {product.main_image?.file_name!} + ) : ( +
    + +
    + )} +
    +
    +

    + {product.attributes.name} +

    +

    + {product.meta.display_price?.without_tax.formatted} +

    +
  • + + ))} +
+
+ ); +} diff --git a/examples/simple/src/components/featured-products/fetchFeaturedProducts.ts b/examples/simple/src/components/featured-products/fetchFeaturedProducts.ts new file mode 100644 index 00000000..9a5679e6 --- /dev/null +++ b/examples/simple/src/components/featured-products/fetchFeaturedProducts.ts @@ -0,0 +1,19 @@ +import { getProducts } from "../../services/products"; +import { Moltin } from "@moltin/sdk"; +import { ProductResponseWithImage } from "../../lib/types/product-types"; +import { connectProductsWithMainImages } from "../../lib/connect-products-with-main-images"; + +// Fetching the first 4 products of in the catalog to display in the featured-products component +export const fetchFeaturedProducts = async ( + client: Moltin, +): Promise => { + const { data: productsResponse, included: productsIncluded } = + await getProducts(client); + + return productsIncluded?.main_images + ? connectProductsWithMainImages( + productsResponse.slice(0, 4), // Only need the first 4 products to feature + productsIncluded?.main_images, + ) + : productsResponse; +}; diff --git a/examples/simple/src/components/footer/Footer.tsx b/examples/simple/src/components/footer/Footer.tsx new file mode 100644 index 00000000..46759f53 --- /dev/null +++ b/examples/simple/src/components/footer/Footer.tsx @@ -0,0 +1,72 @@ +import Link from "next/link"; +import { PhoneIcon, InformationCircleIcon } from "@heroicons/react/24/solid"; +import { GitHubIcon } from "../icons/github-icon"; +import EpLogo from "../icons/ep-logo"; + +const Footer = (): JSX.Element => ( +
+
+
+
+ +
+
+ + Home + + + Shipping + + + FAQ + +
+
+ + About + + + Terms + +
+
+
+ + {" "} + + + + {" "} + + + + + +
+
+
+
+); + +export default Footer; diff --git a/examples/simple/src/components/header/Header.tsx b/examples/simple/src/components/header/Header.tsx new file mode 100644 index 00000000..0b41d05a --- /dev/null +++ b/examples/simple/src/components/header/Header.tsx @@ -0,0 +1,35 @@ +import MobileNavBar from "./navigation/MobileNavBar"; +import NavBar from "./navigation/NavBar"; +import Link from "next/link"; +import CartMenu from "./cart/CartMenu"; +import EpIcon from "../icons/ep-icon"; +import { Suspense } from "react"; + +const Header = (): JSX.Element => { + return ( +
+ + + +
+
+ + + +
+
+ +
+ +
+
+
+
+ +
+
+
+ ); +}; + +export default Header; diff --git a/examples/simple/src/components/header/cart/CartMenu.tsx b/examples/simple/src/components/header/cart/CartMenu.tsx new file mode 100644 index 00000000..84234349 --- /dev/null +++ b/examples/simple/src/components/header/cart/CartMenu.tsx @@ -0,0 +1,115 @@ +"use client"; +import Link from "next/link"; +import ModalCartItems from "./ModalCartItem"; +import { + CartState, + getPresentCartState, + RefinedCartItem, + useCart, +} from "@elasticpath/react-shopper-hooks"; +import { Popover, Transition } from "@headlessui/react"; +import { Fragment } from "react"; +import { ReadonlyNonEmptyArray } from "@elasticpath/react-shopper-hooks"; + +export default function CartMenu(): JSX.Element { + const { state } = useCart(); + + const stateItems = resolveStateCartItems(state); + + function resolveStateCartItems( + state: CartState, + ): ReadonlyNonEmptyArray | undefined { + const presentCartState = getPresentCartState(state); + return presentCartState && presentCartState.items; + } + + return ( +
+ {/* Headless */} + + {({ close }) => ( + <> + + + {stateItems?.length} + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ + )} +
+
+ ); +} + +function CartPopoverFooter({ + state, + onClose, +}: { + state: CartState; + onClose: () => void; +}): JSX.Element { + const checkoutHref = + state.kind === "present-cart-state" ? `/checkout/${state.id}` : "#"; + const hasCartItems = state.kind === "present-cart-state"; + return ( +
+ + + + + + +
+ ); +} diff --git a/examples/simple/src/components/header/cart/ModalCartItem.tsx b/examples/simple/src/components/header/cart/ModalCartItem.tsx new file mode 100644 index 00000000..6cd27b08 --- /dev/null +++ b/examples/simple/src/components/header/cart/ModalCartItem.tsx @@ -0,0 +1,168 @@ +import { useState } from "react"; +import { getPresentCartState, useCart } from "@elasticpath/react-shopper-hooks"; +import { + CartState, + CustomCartItem, + RefinedCartItem, + RegularCartItem, +} from "@elasticpath/react-shopper-hooks"; +import { XMarkIcon } from "@heroicons/react/20/solid"; +import Image from "next/image"; +import Link from "next/link"; +import { ReadonlyNonEmptyArray } from "../../../lib/types/read-only-non-empty-array"; + +function resolveStateCartItems( + state: CartState, +): ReadonlyNonEmptyArray | undefined { + const presentCartState = getPresentCartState(state); + return presentCartState && presentCartState.items; +} + +function ModalCartItem({ + item, + handleRemove, + onClose, +}: { + item: CustomCartItem | RegularCartItem; + handleRemove: (itemId: string) => void; + onClose: () => void; +}): JSX.Element { + const [removing, setRemoving] = useState(false); + + return ( +
+
+ {item.image?.href && ( +
+ onClose()} + > + {" "} + {item.name} + +
+ )} +
+ onClose()} + className="line-clamp-2 text-sm font-semibold hover:underline" + > + {item.name} + + + {item.meta.display_price.without_tax.value.formatted} + + Qty {item.quantity} +
+ +
+
+ ); +} + +export default function ModalCartItems({ + onClose, +}: { + onClose: () => void; +}): JSX.Element { + const { state, removeCartItem } = useCart(); + + const stateItems = resolveStateCartItems(state); + + if (stateItems) { + return ( +
+ {stateItems.map((item) => ( +
+ +
+ ))} +
+ ); + } + + if ( + state.kind === "uninitialised-cart-state" || + state.kind === "loading-cart-state" + ) { + return ( +
+ {/* Turn this spinner into a component with size props */} + +
+ ); + } + + return ( +
+ You have no items in your cart! + + + +
+ ); +} diff --git a/examples/simple/src/components/header/navigation/MobileNavBar.tsx b/examples/simple/src/components/header/navigation/MobileNavBar.tsx new file mode 100644 index 00000000..93156749 --- /dev/null +++ b/examples/simple/src/components/header/navigation/MobileNavBar.tsx @@ -0,0 +1,32 @@ +"use server"; +import Link from "next/link"; +import CartMenu from "../cart/CartMenu"; +import EpIcon from "../../icons/ep-icon"; +import { MobileNavBarButton } from "./MobileNavBarButton"; +import { getServerSideImplicitClient } from "../../../lib/epcc-server-side-implicit-client"; +import { buildSiteNavigation } from "../../../lib/build-site-navigation"; + +export default async function MobileNavBar() { + const client = getServerSideImplicitClient(); + const nav = await buildSiteNavigation(client); + + return ( +
+
+
+
+ +
+ + + +
+
+ +
+
+
+
+
+ ); +} diff --git a/examples/simple/src/components/header/navigation/MobileNavBarButton.tsx b/examples/simple/src/components/header/navigation/MobileNavBarButton.tsx new file mode 100644 index 00000000..7e9392c6 --- /dev/null +++ b/examples/simple/src/components/header/navigation/MobileNavBarButton.tsx @@ -0,0 +1,33 @@ +"use client"; +import { useState } from "react"; +import NavMenu from "./NavMenu"; +import { NavigationNode } from "@elasticpath/react-shopper-hooks"; + +export function MobileNavBarButton({ nav }: { nav: NavigationNode[] }) { + const [showMenu, setShowMenu] = useState(false); + + return ( + <> + + + + ); +} diff --git a/examples/simple/src/components/header/navigation/NavBar.tsx b/examples/simple/src/components/header/navigation/NavBar.tsx new file mode 100644 index 00000000..a18dd638 --- /dev/null +++ b/examples/simple/src/components/header/navigation/NavBar.tsx @@ -0,0 +1,17 @@ +"use server"; +import { NavBarPopover } from "./NavBarPopover"; +import { getServerSideImplicitClient } from "../../../lib/epcc-server-side-implicit-client"; +import { buildSiteNavigation } from "@elasticpath/shopper-common"; + +export default async function NavBar() { + const client = getServerSideImplicitClient(); + const nav = await buildSiteNavigation(client); + + return ( +
+
+ +
+
+ ); +} diff --git a/examples/simple/src/components/header/navigation/NavBarPopover.tsx b/examples/simple/src/components/header/navigation/NavBarPopover.tsx new file mode 100644 index 00000000..b7b445c3 --- /dev/null +++ b/examples/simple/src/components/header/navigation/NavBarPopover.tsx @@ -0,0 +1,44 @@ +"use client"; +import { Popover, Transition } from "@headlessui/react"; +import { Fragment, ReactElement } from "react"; +import NavItemContent from "./NavItemContent"; +import { NavigationNode } from "../../../lib/build-site-navigation"; + +export function NavBarPopover({ + nav, +}: { + nav: NavigationNode[]; +}): ReactElement { + return ( + <> + {nav && + nav.map((item: NavigationNode) => ( + + {({ close }) => ( + <> + + {item.name} + + + + +
+ +
+
+
+ + )} +
+ ))} + + ); +} diff --git a/examples/simple/src/components/header/navigation/NavItemContent.tsx b/examples/simple/src/components/header/navigation/NavItemContent.tsx new file mode 100644 index 00000000..de3ee02c --- /dev/null +++ b/examples/simple/src/components/header/navigation/NavItemContent.tsx @@ -0,0 +1,59 @@ +import Link from "next/link"; +import { NavigationNode } from "../../../lib/build-site-navigation"; +import { ArrowRightIcon } from "@heroicons/react/20/solid"; + +interface IProps { + item: NavigationNode; + onClose: () => void; +} + +const NavItemContent = ({ item, onClose }: IProps): JSX.Element => { + const buildStack = (item: NavigationNode) => { + return ( +
+ {item.name} + {item.children.map((child: NavigationNode) => ( + onClose()} + > + {child.name} + + ))} + onClose()} + > + Browse All + +
+ ); + }; + + return ( +
+
+ {item.children.map((parent: NavigationNode, index: number) => { + return
{buildStack(parent)}
; + })} +
+
+ onClose()} + > + Browse All {item.name} + + +
+ ); +}; + +export default NavItemContent; diff --git a/examples/simple/src/components/header/navigation/NavMenu.tsx b/examples/simple/src/components/header/navigation/NavMenu.tsx new file mode 100644 index 00000000..c22dd9ca --- /dev/null +++ b/examples/simple/src/components/header/navigation/NavMenu.tsx @@ -0,0 +1,115 @@ +"use client"; +import { Dispatch, SetStateAction, Fragment, useState } from "react"; +import { Transition, Dialog, Disclosure } from "@headlessui/react"; +import { NavigationNode } from "@elasticpath/react-shopper-hooks"; +import { ChevronUpIcon } from "@heroicons/react/20/solid"; +import NavItemContent from "./NavItemContent"; + +interface IProps { + showMenu: boolean; + setShowMenu: Dispatch>; + nav: NavigationNode[]; +} + +const NavMenu = (props: IProps) => { + const [expandedDisclosure, setExpandedDisclosure] = useState({ + index: 0, + open: false, + }); + + // If clicked disclosure isn't the same as the open one, close all other disclosures + function handleDisclosureChange(state: number) { + if (state !== expandedDisclosure.index) { + const panels = [ + ...document.querySelectorAll( + "[aria-expanded=true][aria-label=panel]", + ), + ]; + panels.map((panel) => panel.click()); + setExpandedDisclosure({ index: state, open: true }); + } + } + + return ( + + props.setShowMenu(false)} + > + + +
+ +
+ +
+ {props.nav && + props.nav.map((item, index) => { + return ( + + {({ open }) => ( + <> + handleDisclosureChange(index)} + > + {item.name} + + + + props.setShowMenu(false)} + /> + + + )} + + ); + })} +
+
+
+
+
+ ); +}; + +export default NavMenu; diff --git a/examples/simple/src/components/icons/cart.tsx b/examples/simple/src/components/icons/cart.tsx new file mode 100644 index 00000000..bbb0fe34 --- /dev/null +++ b/examples/simple/src/components/icons/cart.tsx @@ -0,0 +1,16 @@ +import clsx from "clsx"; +import { SVGProps } from "react"; + +export default function Cart(props: SVGProps) { + return ( + + + + + ); +} diff --git a/examples/simple/src/components/icons/ep-icon.tsx b/examples/simple/src/components/icons/ep-icon.tsx new file mode 100644 index 00000000..ea703d5d --- /dev/null +++ b/examples/simple/src/components/icons/ep-icon.tsx @@ -0,0 +1,20 @@ +import clsx from "clsx"; +import { SVGProps } from "react"; + +export default function EpIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/examples/simple/src/components/icons/ep-logo.tsx b/examples/simple/src/components/icons/ep-logo.tsx new file mode 100644 index 00000000..21bf4d4f --- /dev/null +++ b/examples/simple/src/components/icons/ep-logo.tsx @@ -0,0 +1,31 @@ +import clsx from "clsx"; +import { SVGProps } from "react"; + +export default function EpLogo(props: SVGProps) { + return ( + + + + + + + + + + + + ); +} diff --git a/examples/simple/src/components/icons/github-icon.tsx b/examples/simple/src/components/icons/github-icon.tsx new file mode 100644 index 00000000..5b4ef518 --- /dev/null +++ b/examples/simple/src/components/icons/github-icon.tsx @@ -0,0 +1,16 @@ +import * as React from "react"; +import { SVGProps } from "react"; +import clsx from "clsx"; +export function GitHubIcon(props: SVGProps) { + return ( + + + + ); +} diff --git a/examples/simple/src/components/product/CartActions.tsx b/examples/simple/src/components/product/CartActions.tsx new file mode 100644 index 00000000..a4857559 --- /dev/null +++ b/examples/simple/src/components/product/CartActions.tsx @@ -0,0 +1,35 @@ +import { useContext } from "react"; +import { useCart } from "@elasticpath/react-shopper-hooks"; +import Spinner from "../Spinner"; +import clsx from "clsx"; +import { ProductContext } from "../../lib/product-context"; + +const CartActions = (): JSX.Element => { + const context = useContext(ProductContext); + const { isUpdatingCart } = useCart(); + + return ( +
+ +
+ ); +}; + +export default CartActions; diff --git a/examples/simple/src/components/product/Price.tsx b/examples/simple/src/components/product/Price.tsx new file mode 100644 index 00000000..02c62d43 --- /dev/null +++ b/examples/simple/src/components/product/Price.tsx @@ -0,0 +1,17 @@ +interface IPriceProps { + price: string; + currency: string; + size?: string; +} + +const Price = ({ price, currency, size }: IPriceProps): JSX.Element => { + return ( + + {price} {currency} + + ); +}; + +export default Price; diff --git a/examples/simple/src/components/product/ProductContainer.tsx b/examples/simple/src/components/product/ProductContainer.tsx new file mode 100644 index 00000000..e8b45033 --- /dev/null +++ b/examples/simple/src/components/product/ProductContainer.tsx @@ -0,0 +1,42 @@ +import ProductCarousel from "./carousel/ProductCarousel"; +import ProductSummary from "./ProductSummary"; +import ProductDetails from "./ProductDetails"; +import ProductExtensions from "./ProductExtensions"; +import CartActions from "./CartActions"; +import { ReactElement } from "react"; +import { ShopperProduct } from "@elasticpath/react-shopper-hooks"; +import { Form } from "formik"; + +interface IProductContainer { + product: ShopperProduct; + children?: ReactElement; +} + +export default function ProductContainer({ + product: { response, main_image, otherImages }, + children, +}: IProductContainer): JSX.Element { + const { extensions } = response.attributes; + return ( +
+
+
+ {main_image && ( + + )} +
+
+
+
+ + {children} + + {extensions && } + +
+
+
+
+
+ ); +} diff --git a/examples/simple/src/components/product/ProductDetails.tsx b/examples/simple/src/components/product/ProductDetails.tsx new file mode 100644 index 00000000..88e873ff --- /dev/null +++ b/examples/simple/src/components/product/ProductDetails.tsx @@ -0,0 +1,28 @@ +import { useContext } from "react"; +import clsx from "clsx"; +import type { ShopperProduct } from "@elasticpath/react-shopper-hooks"; +import { ProductContext } from "../../lib/product-context"; + +interface IProductDetails { + product: ShopperProduct["response"]; +} + +const ProductDetails = ({ product }: IProductDetails): JSX.Element => { + const context = useContext(ProductContext); + + return ( +
+ + Product Details + + {product.attributes.description} +
+ ); +}; + +export default ProductDetails; diff --git a/examples/simple/src/components/product/ProductExtensions.tsx b/examples/simple/src/components/product/ProductExtensions.tsx new file mode 100644 index 00000000..d607dd6e --- /dev/null +++ b/examples/simple/src/components/product/ProductExtensions.tsx @@ -0,0 +1,69 @@ +import { Extensions } from "@moltin/sdk"; +import { isSupportedExtension } from "../../lib/is-supported-extension"; + +interface IProductExtensions { + extensions: Extensions; +} + +const ProductExtensions = ({ extensions }: IProductExtensions): JSX.Element => { + const extensionsValues = Object.values(extensions ?? {}).flat(); + return ( +
+
+ + More Info + +
+ {extensionsValues.map((extension) => { + const extensionKeys = Object.keys(extension); + return extensionKeys.map((key) => { + const value = extension[key]; + + const EmptyEntry = ( +

Unsupported product key: {key}

+ ); + + if (!isSupportedExtension(value)) { + console.warn( + `Unsupported product extension unable to render "${key}" key`, + value, + ); + return EmptyEntry; + } + + if (!value) { + return EmptyEntry; + } + + return ( + + ); + }); + })} +
+
+
+ ); +}; + +function Extension({ + extKey, + value, +}: { + extKey: string; + value: string | number | boolean; +}) { + let decoratedValue = value; + if (typeof value === "boolean") { + decoratedValue = value ? "Yes" : "No"; + } + + return ( + <> +
{extKey}
+
{decoratedValue}
+ + ); +} + +export default ProductExtensions; diff --git a/examples/simple/src/components/product/ProductSummary.tsx b/examples/simple/src/components/product/ProductSummary.tsx new file mode 100644 index 00000000..6363a6b1 --- /dev/null +++ b/examples/simple/src/components/product/ProductSummary.tsx @@ -0,0 +1,46 @@ +import { useContext } from "react"; +import Price from "./Price"; +import StrikePrice from "./StrikePrice"; +import clsx from "clsx"; +import type { ShopperProduct } from "@elasticpath/react-shopper-hooks"; +import { ProductContext } from "../../lib/product-context"; + +interface IProductSummary { + product: ShopperProduct["response"]; +} + +const ProductSummary = ({ product }: IProductSummary): JSX.Element => { + const { + attributes, + meta: { display_price, original_display_price }, + } = product; + const context = useContext(ProductContext); + + return ( + // +
+ + {attributes.name} + + {display_price && ( +
+ + {original_display_price && ( + + )} +
+ )} +
+ ); +}; + +export default ProductSummary; diff --git a/examples/simple/src/components/product/SimpleProduct.tsx b/examples/simple/src/components/product/SimpleProduct.tsx new file mode 100644 index 00000000..793740c3 --- /dev/null +++ b/examples/simple/src/components/product/SimpleProduct.tsx @@ -0,0 +1,45 @@ +import type { SimpleProduct } from "@elasticpath/react-shopper-hooks"; +import ProductContainer from "./ProductContainer"; +import { useCallback } from "react"; +import { + SimpleProductProvider, + useCart, + useSimpleProduct, +} from "@elasticpath/react-shopper-hooks"; +import { Formik } from "formik"; + +interface ISimpleProductDetail { + simpleProduct: SimpleProduct; +} + +function SimpleProductDetail({ + simpleProduct, +}: ISimpleProductDetail): JSX.Element { + return ( + + + + ); +} + +function SimpleProductContainer(): JSX.Element { + const { addProductToCart } = useCart(); + const { product } = useSimpleProduct(); + + const submit = useCallback(async () => { + await addProductToCart(product.response.id, 1); + }, [product, addProductToCart]); + + return ( + { + await submit(); + }} + > + + + ); +} + +export default SimpleProductDetail; diff --git a/examples/simple/src/components/product/StrikePrice.tsx b/examples/simple/src/components/product/StrikePrice.tsx new file mode 100644 index 00000000..745a3d64 --- /dev/null +++ b/examples/simple/src/components/product/StrikePrice.tsx @@ -0,0 +1,19 @@ +interface IPrice { + price: string; + currency: string; + size?: string; +} + +const StrikePrice = ({ price, currency, size }: IPrice): JSX.Element => { + return ( +
+ {price} {currency} +
+ ); +}; + +export default StrikePrice; diff --git a/examples/simple/src/components/product/bundles/BundleProduct.tsx b/examples/simple/src/components/product/bundles/BundleProduct.tsx new file mode 100644 index 00000000..1c560d81 --- /dev/null +++ b/examples/simple/src/components/product/bundles/BundleProduct.tsx @@ -0,0 +1,68 @@ +import ProductComponents from "./ProductComponents"; +import ProductContainer from "../ProductContainer"; +import { Formik } from "formik"; +import { + BundleProduct, + BundleProductProvider, + useBundle, + useCart, +} from "@elasticpath/react-shopper-hooks"; +import { useCallback, useMemo } from "react"; +import { + FormSelectedOptions, + formSelectedOptionsToData, + selectedOptionsToFormValues, +} from "./form-parsers"; +import { createBundleFormSchema } from "./validation-schema"; +import { toFormikValidate } from "zod-formik-adapter"; + +interface IBundleProductDetail { + bundleProduct: BundleProduct; +} + +const BundleProductDetail = ({ + bundleProduct, +}: IBundleProductDetail): JSX.Element => { + return ( + + + + ); +}; + +function BundleProductContainer(): JSX.Element { + const { configuredProduct, selectedOptions, components } = useBundle(); + const { addBundleProductToCart } = useCart(); + + const submit = useCallback( + async (values: { selectedOptions: FormSelectedOptions }) => { + await addBundleProductToCart( + configuredProduct.response.id, + formSelectedOptionsToData(values.selectedOptions), + 1, + ); + }, + [addBundleProductToCart, configuredProduct.response.id], + ); + + const validationSchema = useMemo( + () => createBundleFormSchema(components), + [components], + ); + + return ( + submit(values)} + > + + + + + ); +} + +export default BundleProductDetail; diff --git a/examples/simple/src/components/product/bundles/ProductComponent.tsx b/examples/simple/src/components/product/bundles/ProductComponent.tsx new file mode 100644 index 00000000..41d2262c --- /dev/null +++ b/examples/simple/src/components/product/bundles/ProductComponent.tsx @@ -0,0 +1,165 @@ +import { + useBundleComponent, + useBundle, + useBundleComponentOption, +} from "@elasticpath/react-shopper-hooks"; +import type { BundleComponent } from "@elasticpath/react-shopper-hooks"; +import { ProductComponentOption, ProductResponse } from "@moltin/sdk"; +import { sortByOrder } from "./sort-by-order"; +import { useField, useFormikContext } from "formik"; + +import clsx from "clsx"; +import Image from "next/image"; +import * as React from "react"; +import NoImage from "../../NoImage"; + +export const ProductComponent = ({ + component, + componentLookupKey, +}: { + component: BundleComponent; + componentLookupKey: string; +}): JSX.Element => { + const { componentProducts } = useBundle(); + + const { name } = component; + + const { errors, touched } = useFormikContext<{ + selectedOptions: any; + }>(); + + return ( +
+
+ {name} +
+ {(errors as any)[`selectedOptions.${componentLookupKey}`] && ( +
+ {(errors as any)[`selectedOptions.${componentLookupKey}`]} +
+ )} + +
+
+
+ ); +}; + +function CheckboxComponentOptions({ + options, + componentLookupKey, +}: { + componentProducts: ProductResponse[]; + options: ProductComponentOption[]; + max?: number; + min?: number; + componentLookupKey: string; +}): JSX.Element { + return ( +
+ {options.sort(sortByOrder).map((option) => { + return ( + + ); + })} +
+ ); +} + +function CheckboxComponentOption({ + option, + componentKey, +}: { + option: ProductComponentOption; + componentKey: string; +}): JSX.Element { + const { selected, component } = useBundleComponent(componentKey); + const { optionProduct, mainImage } = useBundleComponentOption( + componentKey, + option.id, + ); + + const selectedOptionKey = Object.keys(selected); + + const reachedMax = + !!component.max && Object.keys(selected).length === component.max; + + const isDisabled = + reachedMax && + !selectedOptionKey.some((optionKey) => optionKey === option.id); + + const name = `selectedOptions.${componentKey}`; + const inputId = `${name}.${option.id}`; + + const [field] = useField({ + name, + type: "checkbox", + value: JSON.stringify({ [option.id]: option.quantity }), + disabled: isDisabled, + id: inputId, + }); + + return ( +
+ +

{optionProduct.attributes.name}

+

+ {optionProduct.meta.display_price?.without_tax.formatted} +

+
+ ); +} diff --git a/examples/simple/src/components/product/bundles/ProductComponents.tsx b/examples/simple/src/components/product/bundles/ProductComponents.tsx new file mode 100644 index 00000000..d00ae8a2 --- /dev/null +++ b/examples/simple/src/components/product/bundles/ProductComponents.tsx @@ -0,0 +1,33 @@ +import { useBundle } from "@elasticpath/react-shopper-hooks"; +import { ProductComponent } from "./ProductComponent"; +import { useFormikContext } from "formik"; +import { useEffect } from "react"; +import { FormSelectedOptions, formSelectedOptionsToData } from "./form-parsers"; + +const ProductComponents = (): JSX.Element => { + const { components, updateSelectedOptions } = useBundle(); + + const { values } = useFormikContext<{ + selectedOptions: FormSelectedOptions; + }>(); + + useEffect(() => { + updateSelectedOptions(formSelectedOptionsToData(values.selectedOptions)); + }, [values, updateSelectedOptions]); + + return ( +
+ {Object.keys(components).map((key) => { + return ( + + ); + })} +
+ ); +}; + +export default ProductComponents; diff --git a/examples/simple/src/components/product/bundles/form-parsers.test.ts b/examples/simple/src/components/product/bundles/form-parsers.test.ts new file mode 100644 index 00000000..591fca06 --- /dev/null +++ b/examples/simple/src/components/product/bundles/form-parsers.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from "vitest"; +import { BundleConfigurationSelectedOptions } from "@elasticpath/react-shopper-hooks"; +import { + formSelectedOptionsToData, + selectedOptionsToFormValues, +} from "./form-parsers"; + +describe("form-parsers", () => { + test("component options to form", () => { + const data: BundleConfigurationSelectedOptions = { + plants: { + "a158ffa0-5d16-4325-8dcc-be8acd55eecf": 1, + "2131231dwadwd12-1d21d2dqw-dd12dqwdaw": 1, + }, + pots: { + "fc520b37-a709-4032-99b3-8d4ecc990027": 1, + }, + tools: {}, + }; + + const expectedResult = { + plants: [ + JSON.stringify({ + "a158ffa0-5d16-4325-8dcc-be8acd55eecf": 1, + }), + JSON.stringify({ + "2131231dwadwd12-1d21d2dqw-dd12dqwdaw": 1, + }), + ], + pots: [ + JSON.stringify({ + "fc520b37-a709-4032-99b3-8d4ecc990027": 1, + }), + ], + tools: [], + }; + + expect(selectedOptionsToFormValues(data)).toEqual(expectedResult); + }); + + test("form to component options", () => { + const data = { + plants: [ + JSON.stringify({ + "a158ffa0-5d16-4325-8dcc-be8acd55eecf": 1, + }), + JSON.stringify({ + "2131231dwadwd12-1d21d2dqw-dd12dqwdaw": 1, + }), + ], + pots: [ + JSON.stringify({ + "fc520b37-a709-4032-99b3-8d4ecc990027": 1, + }), + ], + tools: [], + }; + + const expectedResult: BundleConfigurationSelectedOptions = { + plants: { + "a158ffa0-5d16-4325-8dcc-be8acd55eecf": 1, + "2131231dwadwd12-1d21d2dqw-dd12dqwdaw": 1, + }, + pots: { + "fc520b37-a709-4032-99b3-8d4ecc990027": 1, + }, + tools: {}, + }; + + expect(formSelectedOptionsToData(data)).toEqual(expectedResult); + }); +}); diff --git a/examples/simple/src/components/product/bundles/form-parsers.ts b/examples/simple/src/components/product/bundles/form-parsers.ts new file mode 100644 index 00000000..6966c7b4 --- /dev/null +++ b/examples/simple/src/components/product/bundles/form-parsers.ts @@ -0,0 +1,51 @@ +import { BundleConfigurationSelectedOptions } from "@elasticpath/react-shopper-hooks"; + +export interface FormSelectedOptions { + [key: string]: string[]; +} + +export function selectedOptionsToFormValues( + selectedOptions: BundleConfigurationSelectedOptions, +): FormSelectedOptions { + return Object.keys(selectedOptions).reduce((acc, componentKey) => { + const componentOptions = selectedOptions[componentKey]; + + return { + ...acc, + [componentKey]: Object.keys(componentOptions).reduce( + (innerAcc, optionKey) => { + return [ + ...innerAcc, + JSON.stringify({ [optionKey]: componentOptions[optionKey] }), + ]; + }, + [] as string[], + ), + }; + }, {}); +} + +export function formSelectedOptionsToData( + selectedOptions: FormSelectedOptions, +): BundleConfigurationSelectedOptions { + return Object.keys(selectedOptions).reduce((acc, componentKey) => { + const componentOptions = selectedOptions[componentKey]; + + return { + ...acc, + [componentKey]: componentOptions.reduce( + (innerAcc, optionStr) => { + const parsed = JSON.parse( + optionStr, + ) as BundleConfigurationSelectedOptions[0]; + + return { + ...innerAcc, + ...parsed, + }; + }, + {} as BundleConfigurationSelectedOptions[0], + ), + }; + }, {}); +} diff --git a/examples/simple/src/components/product/bundles/sort-by-order.ts b/examples/simple/src/components/product/bundles/sort-by-order.ts new file mode 100644 index 00000000..672821f8 --- /dev/null +++ b/examples/simple/src/components/product/bundles/sort-by-order.ts @@ -0,0 +1,6 @@ +export function sortByOrder( + { sort_order: a }: { sort_order?: number | null }, + { sort_order: b }: { sort_order?: number | null }, +): number { + return a || b ? (!a ? -1 : !b ? 1 : a - b) : 0; +} diff --git a/examples/simple/src/components/product/bundles/validation-schema.test.ts b/examples/simple/src/components/product/bundles/validation-schema.test.ts new file mode 100644 index 00000000..bfdfb103 --- /dev/null +++ b/examples/simple/src/components/product/bundles/validation-schema.test.ts @@ -0,0 +1,136 @@ +import { describe, expect, test } from "vitest"; +import { BundleComponents } from "@elasticpath/react-shopper-hooks"; + +import { createBundleFormSchema } from "./validation-schema"; +import { DeepPartial } from "../../../lib/types/deep-partial"; + +describe("validation-schema", () => { + test("createBundleFormSchema valid", () => { + const bundleComponents: DeepPartial = { + plants: { + max: 1, + min: 1, + name: "Plants", + options: [ + { + id: "a158ffa0-5d16-4325-8dcc-be8acd55eecf", + quantity: 1, + type: "product", + }, + ], + sort_order: 1, + }, + pots: { + max: 1, + min: 1, + name: "Pots", + options: [ + { + id: "fc520b37-a709-4032-99b3-8d4ecc990027", + quantity: 1, + type: "product", + }, + { + id: "28c13338-07f8-4e40-85b0-e85c0917fb28", + quantity: 1, + type: "product", + }, + ], + sort_order: 2, + }, + tools: { + max: 1, + min: 0, + name: "Tools", + options: [ + { + id: "7ffe107d-c5bd-4de4-b8f0-a58d90cb3cd3", + quantity: 1, + type: "product", + }, + ], + sort_order: 3, + }, + }; + + const validData = { + selectedOptions: { + plants: ['{"a158ffa0-5d16-4325-8dcc-be8acd55eecf":1}'], + pots: ['{"fc520b37-a709-4032-99b3-8d4ecc990027":1}'], + tools: [], + }, + }; + + const result = createBundleFormSchema( + bundleComponents as BundleComponents, + ).safeParse(validData); + + expect(result.success).toEqual(true); + }); + + test("createBundleFormSchema with invalid min max values", () => { + const bundleComponents: DeepPartial = { + plants: { + max: 1, + min: 1, + name: "Plants", + options: [ + { + id: "a158ffa0-5d16-4325-8dcc-be8acd55eecf", + quantity: 1, + type: "product", + }, + ], + sort_order: 1, + }, + pots: { + max: 1, + min: 1, + name: "Pots", + options: [ + { + id: "fc520b37-a709-4032-99b3-8d4ecc990027", + quantity: 1, + type: "product", + }, + { + id: "28c13338-07f8-4e40-85b0-e85c0917fb28", + quantity: 1, + type: "product", + }, + ], + sort_order: 2, + }, + tools: { + max: 1, + min: 0, + name: "Tools", + options: [ + { + id: "7ffe107d-c5bd-4de4-b8f0-a58d90cb3cd3", + quantity: 1, + type: "product", + }, + ], + sort_order: 3, + }, + }; + + const validData = { + selectedOptions: { + plants: [ + '{"a158ffa0-5d16-4325-8dcc-be8acd55eecf":1}', + '{"awdawdawdaw-awdawjnawd-awdauunwda":1}', + ], + pots: [], + tools: [], + }, + }; + + const result = createBundleFormSchema( + bundleComponents as BundleComponents, + ).safeParse(validData); + + expect(result.success).toEqual(false); + }); +}); diff --git a/examples/simple/src/components/product/bundles/validation-schema.ts b/examples/simple/src/components/product/bundles/validation-schema.ts new file mode 100644 index 00000000..4e0792ac --- /dev/null +++ b/examples/simple/src/components/product/bundles/validation-schema.ts @@ -0,0 +1,38 @@ +import { z } from "zod"; +import type { + BundleComponent, + BundleComponents, +} from "@elasticpath/react-shopper-hooks"; + +export const createBundleComponentSchema = (component: BundleComponent) => { + let schema = z.array(z.string()); + + const { min, max } = component; + + if (min) { + schema = schema.min(min, `Must select at least ${min} options`); + } + + if (max) { + schema = schema.max(max, `Must select no more than ${max} options`); + } + return schema; +}; + +export const createBundleFormSchema = (bundleComponents: BundleComponents) => { + const selectedOptionsSchema = Object.keys(bundleComponents).reduce( + (acc, componentKey) => { + return { + ...acc, + [componentKey]: createBundleComponentSchema( + bundleComponents[componentKey], + ), + }; + }, + {} as Record>, + ); + + return z.object({ + selectedOptions: z.object(selectedOptionsSchema), + }); +}; diff --git a/examples/simple/src/components/product/carousel/CarouselListener.tsx b/examples/simple/src/components/product/carousel/CarouselListener.tsx new file mode 100644 index 00000000..4516d420 --- /dev/null +++ b/examples/simple/src/components/product/carousel/CarouselListener.tsx @@ -0,0 +1,22 @@ +import { useContext, useEffect } from "react"; +import { CarouselContext } from "pure-react-carousel"; + +interface CarouselListenerProps { + setCurrentSlide: (index: number) => void; +} + +export const CarouselListener: ( + props: CarouselListenerProps, +) => JSX.Element = ({ setCurrentSlide }) => { + const carouselContext = useContext(CarouselContext); + + useEffect(() => { + function onChange() { + setCurrentSlide(carouselContext.state.currentSlide); + } + carouselContext.subscribe(onChange); + return () => carouselContext.unsubscribe(onChange); + }, [carouselContext, setCurrentSlide]); + + return <>; +}; diff --git a/examples/simple/src/components/product/carousel/HorizontalCarousel.tsx b/examples/simple/src/components/product/carousel/HorizontalCarousel.tsx new file mode 100644 index 00000000..bcc2411a --- /dev/null +++ b/examples/simple/src/components/product/carousel/HorizontalCarousel.tsx @@ -0,0 +1,93 @@ +import { + CarouselProvider, + Slider, + Slide, + ButtonBack, + ButtonNext, +} from "pure-react-carousel"; +import "pure-react-carousel/dist/react-carousel.es.css"; +import styles from "./ProductCarousel.module.css"; +import { isMobile } from "react-device-detect"; +import { ChevronRightIcon, ChevronLeftIcon } from "@heroicons/react/24/solid"; +import clsx from "clsx"; +import Image from "next/image"; + +interface ICarouselImage { + src: string; + name: string | undefined; +} + +interface IHorizontalCarousel { + images: ICarouselImage[]; + visibleSlides: number; + selectedImage: ICarouselImage; + setSelectedImage: (image: ICarouselImage) => void; +} + +const HorizontalCarousel = ({ + images, + visibleSlides, + selectedImage, + setSelectedImage, +}: IHorizontalCarousel): JSX.Element => { + const horizontalStyle = styles["horizontal-product-carousel-inner"]; + + const shouldDisplayControls = images.length > visibleSlides; + const controlsDisplaySettings = shouldDisplayControls ? "flex" : "hidden"; + + return ( + = visibleSlides} + dragEnabled={isMobile} + > +
+ + + + + {images.map((image, index) => ( + + setSelectedImage(image)} + alt={image.name ?? `Product image ${index + 1}`} + src={image.src} + fill + sizes="(min-width: 110px) 66vw, 100vw" + /> + + ))} + + + + +
+
+ ); +}; + +export default HorizontalCarousel; diff --git a/examples/simple/src/components/product/carousel/ProductCarousel.module.css b/examples/simple/src/components/product/carousel/ProductCarousel.module.css new file mode 100644 index 00000000..bb4b807f --- /dev/null +++ b/examples/simple/src/components/product/carousel/ProductCarousel.module.css @@ -0,0 +1,20 @@ +.vertical-product-carousel-inner { + height: calc(100% - 20px); + cursor: pointer; +} + +.horizontal-product-carousel-inner { + width: calc(100% - 10px); +} + +.product-carousel-selected { + border-radius: 0.375rem; + border: 2px solid #2bcc7e; + padding: 0.125rem; +} + +.showcase-product-carousel-inner { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/examples/simple/src/components/product/carousel/ProductCarousel.tsx b/examples/simple/src/components/product/carousel/ProductCarousel.tsx new file mode 100644 index 00000000..04105df2 --- /dev/null +++ b/examples/simple/src/components/product/carousel/ProductCarousel.tsx @@ -0,0 +1,52 @@ +import type { File } from "@moltin/sdk"; +import "pure-react-carousel/dist/react-carousel.es.css"; +import { useState } from "react"; +import HorizontalCarousel from "./HorizontalCarousel"; +import ProductHighlightCarousel from "./ProductHighlightCarousel"; + +interface IProductCarousel { + images: File[]; + mainImage: File | undefined; +} + +const ProductCarousel = ({ + images, + mainImage, +}: IProductCarousel): JSX.Element => { + const completeImages: File[] = [...(mainImage ? [mainImage] : []), ...images]; + + const [selectedProductImage, setSelectedProductImage] = useState( + completeImages[0], + ); + + return ( +
+
+ +
+
+ ({ + src: item.link.href, + name: item.file_name, + }))} + visibleSlides={5} + selectedImage={{ + src: selectedProductImage.link.href, + name: selectedProductImage.file_name, + }} + setSelectedImage={({ src }) => { + const found = completeImages.find((item) => item.link.href === src); + setSelectedProductImage(found!); + }} + /> +
+
+ ); +}; + +export default ProductCarousel; diff --git a/examples/simple/src/components/product/carousel/ProductHighlightCarousel.tsx b/examples/simple/src/components/product/carousel/ProductHighlightCarousel.tsx new file mode 100644 index 00000000..a7879e7b --- /dev/null +++ b/examples/simple/src/components/product/carousel/ProductHighlightCarousel.tsx @@ -0,0 +1,89 @@ +import { useCallback } from "react"; +import { isMobile } from "react-device-detect"; +import type { File } from "@moltin/sdk"; +import { CarouselProvider, Slide, Slider } from "pure-react-carousel"; +import "pure-react-carousel/dist/react-carousel.es.css"; +import { CarouselListener } from "./CarouselListener"; +import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/solid"; +import Image from "next/image"; + +interface IProductHighlightCarousel { + images: File[]; + selectedProductImage: File; + setSelectedProductImage: (file: File) => void; +} + +const ProductHighlightCarousel = ({ + images, + selectedProductImage, + setSelectedProductImage, +}: IProductHighlightCarousel): JSX.Element => { + const selectedImageIndex = images.findIndex( + (img) => img.id === selectedProductImage.id, + ); + + const selectPrevImage = (currentIndex: number) => + setSelectedProductImage(images[currentIndex - 1]); + + const selectNextImage = (currentIndex: number) => + setSelectedProductImage(images[currentIndex + 1]); + + const selectImageWithListener = useCallback( + (currentIndex: number) => setSelectedProductImage(images[currentIndex]), + [images, setSelectedProductImage], + ); + + return ( + + + +
+
+ selectPrevImage(selectedImageIndex)} + /> +
+
+ selectNextImage(selectedImageIndex)} + /> +
+
+ + {images.map((imageFile, index) => ( + + {imageFile.file_name + + ))} + +
+ ); +}; + +export default ProductHighlightCarousel; diff --git a/examples/simple/src/components/product/variations/ProductVariationColor.tsx b/examples/simple/src/components/product/variations/ProductVariationColor.tsx new file mode 100644 index 00000000..d60cbd37 --- /dev/null +++ b/examples/simple/src/components/product/variations/ProductVariationColor.tsx @@ -0,0 +1,55 @@ +import clsx from "clsx"; +import { colorLookup } from "../../../lib/color-lookup"; +import type { useVariationProduct } from "@elasticpath/react-shopper-hooks"; + +interface ProductVariationOption { + id: string; + description: string; + name: string; +} + +interface IProductVariation { + variation: { + id: string; + name: string; + options: ProductVariationOption[]; + }; + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"]; + selectedOptionId?: string; +} + +const ProductVariationColor = ({ + variation, + selectedOptionId, + updateOptionHandler, +}: IProductVariation): JSX.Element => { + return ( +
+

{variation.name}

+
+ {variation.options.map((o) => ( +
+
+ ))} +
+
+ ); +}; + +export default ProductVariationColor; diff --git a/examples/simple/src/components/product/variations/ProductVariationStandard.tsx b/examples/simple/src/components/product/variations/ProductVariationStandard.tsx new file mode 100644 index 00000000..a326191c --- /dev/null +++ b/examples/simple/src/components/product/variations/ProductVariationStandard.tsx @@ -0,0 +1,55 @@ +import clsx from "clsx"; +import type { useVariationProduct } from "@elasticpath/react-shopper-hooks"; + +interface ProductVariationOption { + id: string; + description: string; + name: string; +} + +export type UpdateOptionHandler = ( + variationId: string, +) => (optionId: string) => void; + +interface IProductVariation { + variation: { + id: string; + name: string; + options: ProductVariationOption[]; + }; + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"]; + selectedOptionId?: string; +} + +const ProductVariationStandard = ({ + variation, + selectedOptionId, + updateOptionHandler, +}: IProductVariation): JSX.Element => { + return ( +
+

{variation.name}

+
+ {variation.options.map((o) => ( + + ))} +
+
+ ); +}; + +export default ProductVariationStandard; diff --git a/examples/simple/src/components/product/variations/ProductVariations.tsx b/examples/simple/src/components/product/variations/ProductVariations.tsx new file mode 100644 index 00000000..5dc26bc8 --- /dev/null +++ b/examples/simple/src/components/product/variations/ProductVariations.tsx @@ -0,0 +1,108 @@ +import type { CatalogsProductVariation } from "@moltin/sdk"; +import { useRouter } from "next/navigation"; +import { useContext } from "react"; +import { useEffect } from "react"; +import { OptionDict } from "../../../lib/types/product-types"; +import { + allVariationsHaveSelectedOption, + getSkuIdFromOptions, +} from "../../../lib/product-helper"; +import ProductVariationStandard from "./ProductVariationStandard"; +import ProductVariationColor from "./ProductVariationColor"; +import { useVariationProduct } from "@elasticpath/react-shopper-hooks"; +import { ProductContext } from "../../../lib/product-context"; + +const getSelectedOption = ( + variationId: string, + optionLookupObj: OptionDict, +): string => { + return optionLookupObj[variationId]; +}; + +const ProductVariations = (): JSX.Element => { + const { + variations, + variationsMatrix, + product, + selectedOptions, + updateSelectedOptions, + } = useVariationProduct(); + + const currentProductId = product.response.id; + + const context = useContext(ProductContext); + + const router = useRouter(); + + useEffect(() => { + const selectedSkuId = getSkuIdFromOptions( + Object.values(selectedOptions), + variationsMatrix, + ); + + if ( + !context?.isChangingSku && + selectedSkuId && + selectedSkuId !== currentProductId && + allVariationsHaveSelectedOption(selectedOptions, variations) + ) { + context?.setIsChangingSku(true); + router.replace(`/products/${selectedSkuId}`, { scroll: false }); + context?.setIsChangingSku(false); + } + }, [ + selectedOptions, + context, + currentProductId, + router, + variations, + variationsMatrix, + ]); + + return ( +
+ {variations.map((v) => + resolveVariationComponentByName( + v, + updateSelectedOptions, + getSelectedOption(v.id, selectedOptions), + ), + )} +
+ ); +}; + +function resolveVariationComponentByName( + v: CatalogsProductVariation, + updateOptionHandler: ReturnType< + typeof useVariationProduct + >["updateSelectedOptions"], + selectedOptionId?: string, +): JSX.Element { + switch (v.name.toLowerCase()) { + case "color": + return ( + + ); + default: + return ( + + ); + } +} + +export default ProductVariations; diff --git a/examples/simple/src/components/product/variations/VariationProduct.tsx b/examples/simple/src/components/product/variations/VariationProduct.tsx new file mode 100644 index 00000000..23d30a85 --- /dev/null +++ b/examples/simple/src/components/product/variations/VariationProduct.tsx @@ -0,0 +1,43 @@ +import { + useCart, + useVariationProduct, + VariationProduct, + VariationProductProvider, +} from "@elasticpath/react-shopper-hooks"; +import { useCallback } from "react"; +import { Formik } from "formik"; +import ProductContainer from "../ProductContainer"; +import ProductVariations from "./ProductVariations"; + +export const VariationProductDetail = ({ + variationProduct, +}: { + variationProduct: VariationProduct; +}): JSX.Element => { + return ( + + + + ); +}; + +export function VariationProductContainer(): JSX.Element { + const { addProductToCart } = useCart(); + const { product } = useVariationProduct(); + + const { + response: { id }, + } = product; + + const submit = useCallback(async () => { + await addProductToCart(id, 1); + }, [id, addProductToCart]); + + return ( + submit()}> + + + + + ); +} diff --git a/examples/simple/src/components/promotion-banner/PromotionBanner.tsx b/examples/simple/src/components/promotion-banner/PromotionBanner.tsx new file mode 100644 index 00000000..4430aa13 --- /dev/null +++ b/examples/simple/src/components/promotion-banner/PromotionBanner.tsx @@ -0,0 +1,62 @@ +"use client"; +import { useRouter } from "next/navigation"; +import clsx from "clsx"; + +export interface IPromotion { + title?: string; + description?: string; + imageHref?: string; +} + +interface IPromotionBanner { + linkProps?: { + link: string; + text: string; + }; + alignment?: "center" | "left" | "right"; + promotion: IPromotion; +} + +const PromotionBanner = (props: IPromotionBanner): JSX.Element => { + const router = useRouter(); + const { linkProps, promotion } = props; + + const { title, description } = promotion; + + return ( + <> + {promotion && ( +
+
+ {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} + {linkProps && ( + + )} +
+
+ )} + + ); +}; + +export default PromotionBanner; diff --git a/examples/simple/src/components/quantity-handler/QuantityHandler.tsx b/examples/simple/src/components/quantity-handler/QuantityHandler.tsx new file mode 100644 index 00000000..5f439d11 --- /dev/null +++ b/examples/simple/src/components/quantity-handler/QuantityHandler.tsx @@ -0,0 +1,44 @@ +import type { CartItem } from "@moltin/sdk"; +import { useCart } from "@elasticpath/react-shopper-hooks"; + +interface IQuantityHandler { + item: CartItem; +} + +const QuantityHandler = ({ item }: IQuantityHandler): JSX.Element => { + const { updateCartItem } = useCart(); + + return ( +
+ + { + if (Number(event.target.value) > 0) { + updateCartItem(item.id, Number(event.target.value)); + } + }} + > + +
+ ); +}; + +export default QuantityHandler; diff --git a/examples/simple/src/components/shared/blurb.tsx b/examples/simple/src/components/shared/blurb.tsx new file mode 100644 index 00000000..87df7aae --- /dev/null +++ b/examples/simple/src/components/shared/blurb.tsx @@ -0,0 +1,76 @@ +import { ReactNode } from "react"; + +const Para = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + +interface IBlurbProps { + title: string; +} + +const Blurb = ({ title }: IBlurbProps) => ( +
+

{title}

+ + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec arcu + lectus, pharetra nec velit in, vehicula suscipit tellus. Quisque id mollis + magna. Cras nec lacinia ligula. Morbi aliquam tristique purus, nec dictum + metus euismod at. Vestibulum mollis metus lobortis lectus sodales + eleifend. Class aptent taciti sociosqu ad litora torquent per conubia + nostra, per inceptos himenaeos. Vivamus eget elementum eros, et ultricies + mi. Donec eget dolor imperdiet, gravida ante a, molestie tortor. Nullam + viverra, orci gravida sollicitudin auctor, urna magna condimentum risus, + vitae venenatis turpis mauris sed ligula. Fusce mattis, mauris ut eleifend + ullamcorper, dui felis tincidunt libero, ut commodo arcu leo a ligula. + Cras congue maximus magna, et porta nisl pulvinar in. Nam congue orci + ornare scelerisque elementum. Quisque purus justo, molestie ut leo at, + tristique pretium dui. + + + + Vestibulum imperdiet commodo egestas. Proin tincidunt leo non purus + euismod dictum. Vivamus sagittis mauris dolor, quis egestas purus placerat + eget. Mauris finibus scelerisque augue ut ultrices. Praesent vitae nulla + lorem. Ut eget accumsan risus, sed fringilla orci. Nunc volutpat, odio vel + ornare ullamcorper, massa mauris dapibus nunc, sed euismod lectus erat + eget ligula. Duis fringilla elit vel eleifend luctus. Quisque non blandit + magna. Vivamus pharetra, dolor sed molestie ultricies, tellus ex egestas + lacus, in posuere risus diam non massa. Phasellus in justo in urna + faucibus cursus. + + + + Nullam nibh nisi, lobortis at rhoncus ut, viverra at turpis. Mauris ac + sollicitudin diam. Phasellus non orci massa. Donec tincidunt odio justo. + Sed gravida leo turpis, vitae blandit sem pharetra sit amet. Vestibulum + ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia + curae; Orci varius natoque penatibus et magnis dis parturient montes, + nascetur ridiculus mus. + + + + In in pulvinar turpis, vel pulvinar ipsum. Praesent vel commodo nisi, id + maximus ex. Integer lorem augue, hendrerit et enim vel, eleifend blandit + felis. Integer egestas risus purus, ac rhoncus orci faucibus ac. + Pellentesque iaculis ligula a mauris aliquam, at ullamcorper est + vestibulum. Proin maximus sagittis purus ac pretium. Ut accumsan vitae + nisl sed viverra. + + + + Vivamus malesuada elit facilisis, fringilla lacus non, vulputate felis. + Curabitur dignissim quis ipsum eget pellentesque. Duis efficitur nec nisl + sit amet porta. Maecenas ac dui a felis finibus elementum feugiat at nibh. + Donec convallis sodales neque. Integer id libero eget diam finibus + tincidunt id id diam. Fusce ut lectus nisi. Donec orci enim, semper ac + feugiat vitae, dignissim non enim. Vestibulum commodo dolor nec sem + viverra gravida. Ut laoreet eu tortor auctor consequat. Nulla quis mauris + mollis, aliquam mi nec, laoreet ligula. Fusce laoreet lorem et malesuada + suscipit. Nullam convallis, risus a posuere ultrices, velit augue + porttitor ante, vitae lobortis ligula velit id justo. Praesent nec lorem + massa. + +
+); + +export default Blurb; diff --git a/examples/simple/src/components/shimmer.tsx b/examples/simple/src/components/shimmer.tsx new file mode 100644 index 00000000..208e3f11 --- /dev/null +++ b/examples/simple/src/components/shimmer.tsx @@ -0,0 +1,13 @@ +export const shimmer = (w: number, h: number) => ` + + + + + + + + + + + +`; diff --git a/examples/simple/src/components/toast/toaster.tsx b/examples/simple/src/components/toast/toaster.tsx new file mode 100644 index 00000000..73000a93 --- /dev/null +++ b/examples/simple/src/components/toast/toaster.tsx @@ -0,0 +1,25 @@ +"use client"; +import { useEffect } from "react"; +import { useEvent } from "@elasticpath/react-shopper-hooks"; +import { ToastContainer, toast } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; + +export function Toaster(): JSX.Element { + const { events } = useEvent(); + + useEffect(() => { + const sub = events.subscribe((event) => { + if (event.type !== "init" && event.action !== "init") { + const toastFn = event.type === "success" ? toast.success : toast.error; + toastFn(`${"message" in event ? event.message : undefined}`, { + position: "bottom-center", + autoClose: 3000, + hideProgressBar: true, + }); + } + }); + return () => sub.unsubscribe(); + }, [events]); + + return ; +} diff --git a/examples/simple/src/lib/build-site-navigation.ts b/examples/simple/src/lib/build-site-navigation.ts new file mode 100644 index 00000000..c9ae1f9d --- /dev/null +++ b/examples/simple/src/lib/build-site-navigation.ts @@ -0,0 +1,104 @@ +import type { Hierarchy, Moltin as EPCCClient } from "@moltin/sdk"; +import { + getHierarchies, + getHierarchyChildren, + getHierarchyNodes, +} from "../services/hierarchy"; + +interface ISchema { + name: string; + slug: string; + href: string; + id: string; + children: ISchema[]; +} + +export interface NavigationNode { + name: string; + slug: string; + href: string; + id: string; + children: NavigationNode[]; +} + +export async function buildSiteNavigation( + client: EPCCClient, +): Promise { + // Fetch hierarchies to be used as top level nav + const hierarchies = await getHierarchies(client); + return constructTree(hierarchies, client); +} + +/** + * Construct hierarchy tree, limited to 5 hierarchies at the top level + */ +function constructTree( + hierarchies: Hierarchy[], + client: EPCCClient, +): Promise { + const tree = hierarchies + .slice(0, 4) + .map((hierarchy) => + createNode({ + name: hierarchy.attributes.name, + id: hierarchy.id, + slug: hierarchy.attributes.slug, + }), + ) + .map(async (hierarchy) => { + // Fetch first-level nav ('parent nodes') - the direct children of each hierarchy + const directChildren = await getHierarchyChildren(hierarchy.id, client); + // Fetch all nodes in each hierarchy (i.e. all 'child nodes' belonging to a hierarchy) + const allNodes = await getHierarchyNodes(hierarchy.id, client); + + // Build 2nd level by finding all 'child nodes' belonging to each first level featured-nodes + const directs = directChildren.slice(0, 4).map((child) => { + const children: ISchema[] = allNodes + .filter((node) => node?.relationships?.parent.data.id === child.id) + .map((node) => + createNode({ + name: node.attributes.name, + id: node.id, + slug: node.attributes.slug, + hrefBase: `${hierarchy.href}/${child.attributes.slug}`, + }), + ); + + return createNode({ + name: child.attributes.name, + id: child.id, + slug: child.attributes.slug, + hrefBase: hierarchy.href, + children, + }); + }); + + return { ...hierarchy, children: directs }; + }); + + return Promise.all(tree); +} + +interface CreateNodeDefinition { + name: string; + id: string; + slug?: string; + hrefBase?: string; + children?: ISchema[]; +} + +function createNode({ + name, + id, + slug = "missing-slug", + hrefBase = "", + children = [], +}: CreateNodeDefinition): ISchema { + return { + name, + id, + slug, + href: `${hrefBase}/${slug}`, + children, + }; +} diff --git a/examples/simple/src/lib/cart-cookie-client.ts b/examples/simple/src/lib/cart-cookie-client.ts new file mode 100644 index 00000000..3dd181fd --- /dev/null +++ b/examples/simple/src/lib/cart-cookie-client.ts @@ -0,0 +1,17 @@ +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; +import { getCookie } from "cookies-next"; + +/** + * The cart cookie is set by nextjs middleware. + */ +export function getCartCookieClient(): string { + const possibleCartCookie = getCookie(`${COOKIE_PREFIX_KEY}_ep_cart`); + + if (typeof possibleCartCookie === "string") { + return possibleCartCookie; + } + + throw Error( + `Failed to fetch cart cookie! key ${`${COOKIE_PREFIX_KEY}_ep_cart`}`, + ); +} diff --git a/examples/simple/src/lib/cart-cookie-server.ts b/examples/simple/src/lib/cart-cookie-server.ts new file mode 100644 index 00000000..86bf8f6f --- /dev/null +++ b/examples/simple/src/lib/cart-cookie-server.ts @@ -0,0 +1,18 @@ +import "server-only"; +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; +import { cookies } from "next/headers"; + +const CART_COOKIE_NAME = `${COOKIE_PREFIX_KEY}_ep_cart`; + +/** + * The cart cookie is set by nextjs middleware. + */ +export function getCartCookieServer(): string { + const possibleCartCookie = cookies().get(CART_COOKIE_NAME); + + if (!possibleCartCookie) { + throw Error(`Failed to fetch cart cookie! key ${CART_COOKIE_NAME}`); + } + + return possibleCartCookie.value; +} diff --git a/examples/simple/src/lib/color-lookup.ts b/examples/simple/src/lib/color-lookup.ts new file mode 100644 index 00000000..9c147119 --- /dev/null +++ b/examples/simple/src/lib/color-lookup.ts @@ -0,0 +1,10 @@ +export const colorLookup: { [key: string]: string } = { + gray: "gray", + grey: "gray", + red: "red", + white: "white", + teal: "teal", + purple: "purple", + green: "green", + blue: "blue", +}; diff --git a/examples/simple/src/lib/connect-products-with-main-images.ts b/examples/simple/src/lib/connect-products-with-main-images.ts new file mode 100644 index 00000000..11b56569 --- /dev/null +++ b/examples/simple/src/lib/connect-products-with-main-images.ts @@ -0,0 +1,29 @@ +import { File, ProductResponse } from "@moltin/sdk"; +import { + ProductImageObject, + ProductResponseWithImage, +} from "./types/product-types"; + +export const connectProductsWithMainImages = ( + products: ProductResponse[], + images: File[], +): ProductResponseWithImage[] => { + // Object with image id as a key and File data as a value + let imagesObject: ProductImageObject = {}; + images.forEach((image) => { + imagesObject[image.id] = image; + }); + + const productList: ProductResponseWithImage[] = [...products]; + + productList.forEach((product) => { + if ( + product.relationships.main_image?.data && + imagesObject[product.relationships.main_image.data?.id] + ) { + product.main_image = + imagesObject[product.relationships.main_image.data?.id]; + } + }); + return productList; +}; diff --git a/examples/simple/src/lib/cookie-constants.ts b/examples/simple/src/lib/cookie-constants.ts new file mode 100644 index 00000000..584e6035 --- /dev/null +++ b/examples/simple/src/lib/cookie-constants.ts @@ -0,0 +1,3 @@ +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; + +export const CREDENTIALS_COOKIE_NAME = `${COOKIE_PREFIX_KEY}_ep_credentials`; diff --git a/examples/simple/src/lib/custom-rule-headers.ts b/examples/simple/src/lib/custom-rule-headers.ts new file mode 100644 index 00000000..b320e365 --- /dev/null +++ b/examples/simple/src/lib/custom-rule-headers.ts @@ -0,0 +1,17 @@ +import { isEmptyObj } from "./is-empty-object"; + +export function resolveEpccCustomRuleHeaders(): + | { "EP-Context-Tag"?: string; "EP-Channel"?: string } + | undefined { + const { epContextTag, epChannel } = { + epContextTag: process.env.NEXT_PUBLIC_CONTEXT_TAG, + epChannel: process.env.NEXT_PUBLIC_CHANNEL, + }; + + const headers = { + ...(epContextTag ? { "EP-Context-Tag": epContextTag } : {}), + ...(epChannel ? { "EP-Channel": epChannel } : {}), + }; + + return isEmptyObj(headers) ? undefined : headers; +} diff --git a/examples/simple/src/lib/ep-client-store.ts b/examples/simple/src/lib/ep-client-store.ts new file mode 100644 index 00000000..7ff963b4 --- /dev/null +++ b/examples/simple/src/lib/ep-client-store.ts @@ -0,0 +1,27 @@ +import { Moltin as EPCCClient } from "@moltin/sdk"; + +type ClientStore = Record; + +let _clientStore: ClientStore = {} as ClientStore; + +export type ClientStoreTypes = "implicit" | "client-credentials"; + +/** + * @internal Should only be used internally for client management + */ +export function _registerClient( + client: EPCCClient, + type: ClientStoreTypes, +): EPCCClient { + if (_clientStore && _clientStore[type]) { + throw Error(`Already have a client registered with type ${type}`); + } + return (_clientStore[type] = client); +} + +/** + * @internal Should only be used internally for client management + */ +export function _getClientStore(type: ClientStoreTypes) { + return _clientStore[type]; +} diff --git a/examples/simple/src/lib/epcc-errors.ts b/examples/simple/src/lib/epcc-errors.ts new file mode 100644 index 00000000..6f1f58c5 --- /dev/null +++ b/examples/simple/src/lib/epcc-errors.ts @@ -0,0 +1,27 @@ +export function isNoDefaultCatalogError( + errors: object[], +): errors is [{ detail: string }] { + const error = errors[0]; + return ( + hasDetail(error) && + error.detail === + "unable to resolve default catalog: no default catalog id can be identified: not found" + ); +} + +function hasDetail(err: object): err is { detail: string } { + return "detail" in err; +} + +export function isEPError(err: unknown): err is { errors: object[] } { + return ( + typeof err === "object" && + !!err && + hasErrors(err) && + Array.isArray(err.errors) + ); +} + +function hasErrors(err: object): err is { errors: object[] } { + return "errors" in err; +} diff --git a/examples/simple/src/lib/epcc-implicit-client.ts b/examples/simple/src/lib/epcc-implicit-client.ts new file mode 100644 index 00000000..3e6b6a97 --- /dev/null +++ b/examples/simple/src/lib/epcc-implicit-client.ts @@ -0,0 +1,37 @@ +import { gateway, StorageFactory } from "@moltin/sdk"; +import { epccEnv } from "./resolve-epcc-env"; +import { resolveEpccCustomRuleHeaders } from "./custom-rule-headers"; +import { deleteCookie, getCookie, setCookie } from "cookies-next"; +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; +import { EP_CURRENCY_CODE } from "./resolve-ep-currency-code"; + +const headers = resolveEpccCustomRuleHeaders(); + +const { client_id, host } = epccEnv; + +export function getEpccImplicitClient() { + return gateway({ + name: COOKIE_PREFIX_KEY, + client_id, + host, + currency: EP_CURRENCY_CODE, + ...(headers ? { headers } : {}), + storage: createNextCookieStorageFactory(), + }); +} + +function createNextCookieStorageFactory(): StorageFactory { + return { + set: (key: string, value: string): void => { + setCookie(key, value, { + sameSite: "strict", + }); + }, + get: (key: string): any => { + return getCookie(key); + }, + delete: (key: string) => { + deleteCookie(key); + }, + }; +} diff --git a/examples/simple/src/lib/epcc-server-client.ts b/examples/simple/src/lib/epcc-server-client.ts new file mode 100644 index 00000000..8e82d8a5 --- /dev/null +++ b/examples/simple/src/lib/epcc-server-client.ts @@ -0,0 +1,29 @@ +import { + ConfigOptions, + gateway as EPCCGateway, + MemoryStorageFactory, +} from "@moltin/sdk"; +import { epccEnv } from "./resolve-epcc-env"; +import { resolveEpccCustomRuleHeaders } from "./custom-rule-headers"; +import { EP_CURRENCY_CODE } from "../lib/resolve-ep-currency-code"; + +const headers = resolveEpccCustomRuleHeaders(); + +const { client_id, client_secret, host } = epccEnv; + +if (typeof client_secret !== "string") { + throw Error( + "Attempted to use client credentials client without a defined client_secret. This is most likely caused by trying to use server side client on the client side.", + ); +} + +const config: ConfigOptions = { + client_id, + client_secret, + host, + currency: EP_CURRENCY_CODE, + storage: new MemoryStorageFactory(), + ...(headers ? { headers } : {}), +}; + +export const epccServerClient = EPCCGateway(config); diff --git a/examples/simple/src/lib/epcc-server-side-implicit-client.ts b/examples/simple/src/lib/epcc-server-side-implicit-client.ts new file mode 100644 index 00000000..d7951cf3 --- /dev/null +++ b/examples/simple/src/lib/epcc-server-side-implicit-client.ts @@ -0,0 +1,48 @@ +import "server-only"; +import { gateway, StorageFactory } from "@moltin/sdk"; +import { epccEnv } from "./resolve-epcc-env"; +import { resolveEpccCustomRuleHeaders } from "./custom-rule-headers"; +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; +import { EP_CURRENCY_CODE } from "./resolve-ep-currency-code"; +import { CREDENTIALS_COOKIE_NAME } from "./cookie-constants"; +import { cookies } from "next/headers"; + +const customHeaders = resolveEpccCustomRuleHeaders(); + +const { client_id, host } = epccEnv; + +export function getServerSideImplicitClient() { + const credentialsCookie = cookies().get(CREDENTIALS_COOKIE_NAME); + + return gateway({ + name: COOKIE_PREFIX_KEY, + client_id, + host, + currency: EP_CURRENCY_CODE, + ...(customHeaders ? { headers: customHeaders } : {}), + reauth: false, + storage: createServerSideNextCookieStorageFactory(credentialsCookie?.value), + }); +} + +function createServerSideNextCookieStorageFactory( + initialCookieValue?: string, +): StorageFactory { + let state = new Map(); + + if (initialCookieValue) { + state.set(`${COOKIE_PREFIX_KEY}_ep_credentials`, initialCookieValue); + } + + return { + set: (key: string, value: string): void => { + state.set(key, value); + }, + get: (key: string): any => { + return state.get(key); + }, + delete: (key: string) => { + state.delete(key); + }, + }; +} diff --git a/examples/simple/src/lib/form-url-encode-body.ts b/examples/simple/src/lib/form-url-encode-body.ts new file mode 100644 index 00000000..b05c4711 --- /dev/null +++ b/examples/simple/src/lib/form-url-encode-body.ts @@ -0,0 +1,10 @@ +export function formUrlEncodeBody(body: Record): string { + return Object.keys(body) + .map( + (k) => + `${encodeURIComponent(k)}=${encodeURIComponent( + body[k as keyof typeof body], + )}`, + ) + .join("&"); +} diff --git a/examples/simple/src/lib/get-store-context.ts b/examples/simple/src/lib/get-store-context.ts new file mode 100644 index 00000000..fa6b4860 --- /dev/null +++ b/examples/simple/src/lib/get-store-context.ts @@ -0,0 +1,19 @@ +import { Moltin } from "@moltin/sdk"; +import { StoreContext } from "@elasticpath/react-shopper-hooks"; +import { buildSiteNavigation } from "./build-site-navigation"; +import { getCartCookieServer } from "./cart-cookie-server"; +import { getCart } from "../services/cart"; + +export async function getStoreContext(client: Moltin): Promise { + const nav = await buildSiteNavigation(client); + + const cartCookie = getCartCookieServer(); + + const cart = await getCart(cartCookie, client); + + return { + cart, + nav, + type: "store-context-ssr", + }; +} diff --git a/examples/simple/src/lib/is-empty-object.ts b/examples/simple/src/lib/is-empty-object.ts new file mode 100644 index 00000000..200e3eb9 --- /dev/null +++ b/examples/simple/src/lib/is-empty-object.ts @@ -0,0 +1,2 @@ +export const isEmptyObj = (obj: object): boolean => + Object.keys(obj).length === 0; diff --git a/examples/simple/src/lib/is-supported-extension.ts b/examples/simple/src/lib/is-supported-extension.ts new file mode 100644 index 00000000..a7b1adaf --- /dev/null +++ b/examples/simple/src/lib/is-supported-extension.ts @@ -0,0 +1,9 @@ +export function isSupportedExtension(value: unknown): boolean { + return ( + typeof value === "boolean" || + typeof value === "number" || + typeof value === "string" || + typeof value === "undefined" || + value === null + ); +} diff --git a/examples/simple/src/lib/middleware/apply-set-cookie.ts b/examples/simple/src/lib/middleware/apply-set-cookie.ts new file mode 100644 index 00000000..f6a1a400 --- /dev/null +++ b/examples/simple/src/lib/middleware/apply-set-cookie.ts @@ -0,0 +1,31 @@ +import { NextResponse, type NextRequest } from "next/server"; +import { + ResponseCookies, + RequestCookies, +} from "next/dist/server/web/spec-extension/cookies"; + +/** + * Copy cookies from the Set-Cookie header of the response to the Cookie header of the request, + * so that it will appear to SSR/RSC as if the user already has the new cookies. + * + * Workaround for - https://github.com/vercel/next.js/issues/49442#issuecomment-1679807704 + */ +export function applySetCookie(req: NextRequest, res: NextResponse): void { + // parse the outgoing Set-Cookie header + const setCookies = new ResponseCookies(res.headers); + // Build a new Cookie header for the request by adding the setCookies + const newReqHeaders = new Headers(req.headers); + const newReqCookies = new RequestCookies(newReqHeaders); + setCookies.getAll().forEach((cookie) => newReqCookies.set(cookie)); + // set “request header overrides” on the outgoing response + NextResponse.next({ + request: { headers: newReqHeaders }, + }).headers.forEach((value, key) => { + if ( + key === "x-middleware-override-headers" || + key.startsWith("x-middleware-request-") + ) { + res.headers.set(key, value); + } + }); +} diff --git a/examples/simple/src/lib/middleware/cart-cookie-middleware.ts b/examples/simple/src/lib/middleware/cart-cookie-middleware.ts new file mode 100644 index 00000000..f0faf431 --- /dev/null +++ b/examples/simple/src/lib/middleware/cart-cookie-middleware.ts @@ -0,0 +1,121 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + createAuthenticationErrorUrl, + createMissingEnvironmentVariableUrl, +} from "./create-missing-environment-variable-url"; +import { epccEndpoint } from "./implicit-auth-middleware"; +import { NextResponseFlowResult } from "./middleware-runner"; +import { tokenExpired } from "../token-expired"; +import { applySetCookie } from "./apply-set-cookie"; + +const cookiePrefixKey = process.env.NEXT_PUBLIC_COOKIE_PREFIX_KEY; + +export async function cartCookieMiddleware( + req: NextRequest, + previousResponse: NextResponse, +): Promise { + if (typeof cookiePrefixKey !== "string") { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createMissingEnvironmentVariableUrl( + "NEXT_PUBLIC_COOKIE_PREFIX_KEY", + req.nextUrl.basePath, + req.url, + ), + ), + }; + } + + if (req.cookies.get(`${cookiePrefixKey}_ep_cart`)) { + return { + shouldReturn: false, + resultingResponse: previousResponse, + }; + } + + if (typeof epccEndpoint !== "string") { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createMissingEnvironmentVariableUrl( + "NEXT_PUBLIC_EPCC_ENDPOINT_URL", + req.nextUrl.basePath, + req.url, + ), + ), + }; + } + + const authToken = retrieveAuthToken(req, previousResponse); + + if (!authToken) { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createAuthenticationErrorUrl( + `Cart cookie creation failed in middleware because credentials \"${cookiePrefixKey}_ep_credentials\" cookie was missing.`, + req.nextUrl.origin, + req.url, + ), + ), + }; + } + + if (!authToken.access_token) { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createAuthenticationErrorUrl( + `Cart cookie creation failed in middleware because credentials \"access_token\" was undefined.`, + req.nextUrl.origin, + req.url, + ), + ), + }; + } + + const createdCart = await fetch(`https://${epccEndpoint}/v2/carts`, { + method: "POST", + headers: { + Authorization: `Bearer ${authToken.access_token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ data: { name: "Cart" } }), + }); + + const parsedCartJSON = await createdCart.json(); + + previousResponse.cookies.set( + `${cookiePrefixKey}_ep_cart`, + parsedCartJSON.data.id, + { + sameSite: "strict", + expires: new Date(parsedCartJSON.data.meta.timestamps.expires_at), + }, + ); + + // Apply those cookies to the request + // Workaround for - https://github.com/vercel/next.js/issues/49442#issuecomment-1679807704 + applySetCookie(req, previousResponse); + + return { + shouldReturn: false, + resultingResponse: previousResponse, + }; +} + +function retrieveAuthToken( + req: NextRequest, + resp: NextResponse, +): { access_token: string; expires: number } | undefined { + const authCookie = + req.cookies.get(`${cookiePrefixKey}_ep_credentials`) ?? + resp.cookies.get(`${cookiePrefixKey}_ep_credentials`); + + const possiblyParsedCookie = authCookie && JSON.parse(authCookie.value); + + return possiblyParsedCookie && tokenExpired(possiblyParsedCookie.expires) + ? undefined + : possiblyParsedCookie; +} diff --git a/examples/simple/src/lib/middleware/create-missing-environment-variable-url.ts b/examples/simple/src/lib/middleware/create-missing-environment-variable-url.ts new file mode 100644 index 00000000..f6f347e5 --- /dev/null +++ b/examples/simple/src/lib/middleware/create-missing-environment-variable-url.ts @@ -0,0 +1,36 @@ +import { NonEmptyArray } from "../types/non-empty-array"; + +export function createMissingEnvironmentVariableUrl( + name: string | NonEmptyArray, + reqUrl: string, + from?: string, +): URL { + const configErrorUrl = createBaseErrorUrl(reqUrl, from); + + (Array.isArray(name) ? name : [name]).forEach((n) => { + configErrorUrl.searchParams.append("missing-env-variable", n); + }); + + return configErrorUrl; +} + +export function createAuthenticationErrorUrl( + message: string, + reqUrl: string, + from?: string, +): URL { + const configErrorUrl = createBaseErrorUrl(reqUrl, from); + configErrorUrl.searchParams.append( + "authentication", + encodeURIComponent(message), + ); + return configErrorUrl; +} + +function createBaseErrorUrl(reqUrl: string, from?: string): URL { + const configErrorUrl = new URL("/configuration-error", reqUrl); + if (from) { + configErrorUrl.searchParams.set("from", from); + } + return configErrorUrl; +} diff --git a/examples/simple/src/lib/middleware/implicit-auth-middleware.ts b/examples/simple/src/lib/middleware/implicit-auth-middleware.ts new file mode 100644 index 00000000..22f38d5d --- /dev/null +++ b/examples/simple/src/lib/middleware/implicit-auth-middleware.ts @@ -0,0 +1,102 @@ +import { NextRequest, NextResponse } from "next/server"; +import { NextResponseFlowResult } from "./middleware-runner"; +import { formUrlEncodeBody } from "../form-url-encode-body"; +import { + createAuthenticationErrorUrl, + createMissingEnvironmentVariableUrl, +} from "./create-missing-environment-variable-url"; +import { tokenExpired } from "../token-expired"; +import { applySetCookie } from "./apply-set-cookie"; + +export const epccEndpoint = process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL; +const clientId = process.env.NEXT_PUBLIC_EPCC_CLIENT_ID; +const cookiePrefixKey = process.env.NEXT_PUBLIC_COOKIE_PREFIX_KEY; + +export async function implicitAuthMiddleware( + req: NextRequest, + previousResponse: NextResponse, +): Promise { + if (typeof clientId !== "string" || typeof cookiePrefixKey !== "string") { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createMissingEnvironmentVariableUrl( + ["NEXT_PUBLIC_EPCC_CLIENT_ID", "NEXT_PUBLIC_COOKIE_PREFIX_KEY"], + req.nextUrl.basePath, + req.url, + ), + ), + }; + } + + const possibleImplicitCookie = req.cookies.get( + `${cookiePrefixKey}_ep_credentials`, + ); + + if ( + possibleImplicitCookie && + !tokenExpired(JSON.parse(possibleImplicitCookie.value).expires) + ) { + return { + shouldReturn: false, + resultingResponse: previousResponse, + }; + } + + const authResponse = await getTokenImplicitToken({ + grant_type: "implicit", + client_id: clientId, + }); + + const token = await authResponse.json(); + + /** + * Check response did not fail + */ + if (token && "errors" in token) { + return { + shouldReturn: true, + resultingResponse: NextResponse.redirect( + createAuthenticationErrorUrl( + `Implicit auth middleware failed to get access token.`, + req.nextUrl.origin, + req.url, + ), + ), + }; + } + + previousResponse.cookies.set( + `${cookiePrefixKey}_ep_credentials`, + JSON.stringify({ + ...token, + client_id: clientId, + }), + { + sameSite: "strict", + expires: new Date(token.expires * 1000), + }, + ); + + // Apply those cookies to the request + // Workaround for - https://github.com/vercel/next.js/issues/49442#issuecomment-1679807704 + applySetCookie(req, previousResponse); + + return { + shouldReturn: false, + resultingResponse: previousResponse, + }; +} + +async function getTokenImplicitToken(body: { + grant_type: "implicit"; + client_id: string; +}): Promise { + return fetch(`https://${epccEndpoint}/oauth/access_token`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + body: formUrlEncodeBody(body), + }); +} diff --git a/examples/simple/src/lib/middleware/middleware-runner.ts b/examples/simple/src/lib/middleware/middleware-runner.ts new file mode 100644 index 00000000..d6b8df45 --- /dev/null +++ b/examples/simple/src/lib/middleware/middleware-runner.ts @@ -0,0 +1,63 @@ +import { NonEmptyArray } from "../types/non-empty-array"; +import { NextRequest, NextResponse } from "next/server"; + +export interface NextResponseFlowResult { + shouldReturn: boolean; + resultingResponse: NextResponse; +} + +interface RunnableMiddlewareEntryOptions { + exclude?: NonEmptyArray; +} + +interface RunnableMiddlewareEntry { + runnable: RunnableMiddleware; + options?: RunnableMiddlewareEntryOptions; +} + +export function middlewareRunner( + ...middleware: NonEmptyArray +) { + return async (req: NextRequest): Promise => { + let lastResult: NextResponseFlowResult = { + shouldReturn: false, + resultingResponse: NextResponse.next(), + }; + + for (const m of middleware) { + const toRun: RunnableMiddlewareEntry = + "runnable" in m ? m : { runnable: m }; + + const { runnable, options } = toRun; + + if (shouldRun(req.nextUrl.pathname, options?.exclude)) { + lastResult = await runnable(req, lastResult.resultingResponse); + } + + if (lastResult.shouldReturn) { + return lastResult.resultingResponse; + } + } + return lastResult.resultingResponse; + }; +} + +function shouldRun( + pathname: string, + excluded?: NonEmptyArray, +): boolean { + if (excluded) { + for (const path of excluded) { + if (pathname.startsWith(path)) { + return false; + } + } + } + + return true; +} + +type RunnableMiddleware = ( + req: NextRequest, + previousResponse: NextResponse, +) => Promise; diff --git a/examples/simple/src/lib/parse-cookie.ts b/examples/simple/src/lib/parse-cookie.ts new file mode 100644 index 00000000..392db5d4 --- /dev/null +++ b/examples/simple/src/lib/parse-cookie.ts @@ -0,0 +1,32 @@ +const parseCookie = (str: string): Record => + str + .split(";") + .map((v) => v.split("=")) + .reduce( + (acc, v, index) => { + return { + ...acc, + ...(index == 0 + ? { + value: decodeURIComponent(v[1].trim()), + name: decodeURIComponent(v[0].trim()), + } + : { + [decodeURIComponent(v[0].trim())]: decodeURIComponent( + v[1].trim(), + ), + }), + }; + }, + {} as Record, + ); + +export const parseCookies = ( + values: string[], +): Record> => { + return values.reduce((acc, val) => { + const parsed = parseCookie(val); + + return { ...acc, [parsed["name"]]: parsed }; + }, {}); +}; diff --git a/examples/simple/src/lib/product-context.ts b/examples/simple/src/lib/product-context.ts new file mode 100644 index 00000000..7be48e43 --- /dev/null +++ b/examples/simple/src/lib/product-context.ts @@ -0,0 +1,10 @@ +import { createContext } from "react"; +import { + ProductContextState, + ProductModalContextState, +} from "./types/product-types"; + +export const ProductContext = createContext(null); + +export const ProductModalContext = + createContext(null); diff --git a/examples/simple/src/lib/product-helper.test.ts b/examples/simple/src/lib/product-helper.test.ts new file mode 100644 index 00000000..a9270fd0 --- /dev/null +++ b/examples/simple/src/lib/product-helper.test.ts @@ -0,0 +1,273 @@ +import type { ProductResponse, Variation } from "@moltin/sdk"; +import { describe, test, expect } from "vitest"; +import { + allVariationsHaveSelectedOption, + getOptionsFromSkuId, + getSkuIdFromOptions, + isChildProductResource, + isSimpleProductResource, + mapOptionsToVariation, +} from "./product-helper"; + +describe("product-helpers", () => { + test("isChildProductResource should return false if it's a base product", () => { + const sampleProduct = { + attributes: { + base_product: true, + }, + } as ProductResponse; + expect(isChildProductResource(sampleProduct)).toEqual(false); + }); + + test("isChildProductResource should return false if it is a simple product", () => { + const sampleProduct = { + attributes: { + base_product: false, + }, + } as ProductResponse; + expect(isChildProductResource(sampleProduct)).toEqual(false); + }); + test("isChildProductResource should return true if it is a child product", () => { + const sampleProduct = { + attributes: { + base_product: false, + base_product_id: "123", + }, + } as ProductResponse; + expect(isChildProductResource(sampleProduct)).toEqual(true); + }); + + test("isSimpleProductResource should return true if it is a simple product", () => { + const sampleProduct = { + attributes: { + base_product: false, + }, + } as ProductResponse; + expect(isSimpleProductResource(sampleProduct)).toEqual(true); + }); + + test("isSimpleProductResource should return false if it is a base product", () => { + const sampleProduct = { + attributes: { + base_product: true, + }, + } as ProductResponse; + expect(isSimpleProductResource(sampleProduct)).toEqual(false); + }); + + test("isSimpleProductResource should return false if it is a child product", () => { + const sampleProduct = { + attributes: { + base_product: true, + base_product_id: "123", + }, + } as ProductResponse; + expect(isSimpleProductResource(sampleProduct)).toEqual(false); + }); + + test("getSkuIDFromOptions should return the id of the sku for the provided options.", () => { + const variationMatrixSample = { + "4252d475-2d0e-4cd2-99d3-19fba34ef211": { + "217883ce-55f1-4c34-8e00-e86c743f4dff": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "709e6cc6-a40c-4833-9469-b4abd0e7f67f", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "c05839f5-3eac-48f2-9d36-1bc2a481a213", + }, + "37b5bcf7-0b65-4e12-ad31-3052e27c107f": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "9e07495c-caf1-4f11-93c5-16cfeb63d492", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "b9bb984a-7a6d-4433-a445-1cde0383bece", + }, + }, + "693b16b8-a3b3-4419-ad03-61007a381c56": { + "217883ce-55f1-4c34-8e00-e86c743f4dff": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "2d864c10-146f-4905-859f-86e63c18abf4", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "42aef769-c97e-48a8-a3c4-2af8ad504ebb", + }, + }, + }; + + const options = [ + "693b16b8-a3b3-4419-ad03-61007a381c56", + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89", + "217883ce-55f1-4c34-8e00-e86c743f4dff", + ]; + + expect(getSkuIdFromOptions(options, variationMatrixSample)).toEqual( + "2d864c10-146f-4905-859f-86e63c18abf4", + ); + }); + test("getSkuIDFromOptions should return undefined when proveded valid but not found options.", () => { + const variationMatrixSample = { + "4252d475-2d0e-4cd2-99d3-19fba34ef211": { + "217883ce-55f1-4c34-8e00-e86c743f4dff": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "709e6cc6-a40c-4833-9469-b4abd0e7f67f", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "c05839f5-3eac-48f2-9d36-1bc2a481a213", + }, + "37b5bcf7-0b65-4e12-ad31-3052e27c107f": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "9e07495c-caf1-4f11-93c5-16cfeb63d492", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "b9bb984a-7a6d-4433-a445-1cde0383bece", + }, + }, + "693b16b8-a3b3-4419-ad03-61007a381c56": { + "217883ce-55f1-4c34-8e00-e86c743f4dff": { + "45e2612f-6bbf-4bc9-8803-80c5cf78ed89": + "2d864c10-146f-4905-859f-86e63c18abf4", + "8b6dfc96-11e6-455d-b042-e4137df3f13a": + "42aef769-c97e-48a8-a3c4-2af8ad504ebb", + }, + }, + }; + + const options = ["4252d475-2d0e-4cd2-99d3-19fba34ef211", "456", "789"]; + + expect(getSkuIdFromOptions(options, variationMatrixSample)).toEqual( + undefined, + ); + }); + test("getSkuIDFromOptions should return undefined when proveded empty options.", () => { + const variationMatrixSample = {}; + + expect(getSkuIdFromOptions([], variationMatrixSample)).toEqual(undefined); + }); + + test("getOptionsFromSkuId should return a list of options for given sku id and matrix.", () => { + const variationMatrixSample = { + "option-1": { + "option-3": { + "option-5": "709e6cc6-a40c-4833-9469-b4abd0e7f67f", + "option-6": "c05839f5-3eac-48f2-9d36-1bc2a481a213", + }, + "option-4": { + "option-5": "9e07495c-caf1-4f11-93c5-16cfeb63d492", + "option-6": "b9bb984a-7a6d-4433-a445-1cde0383bece", + }, + }, + "option-2": { + "option-3": { + "option-5": "2d864c10-146f-4905-859f-86e63c18abf4", + "option-6": "42aef769-c97e-48a8-a3c4-2af8ad504ebb", + }, + }, + }; + + const expectedOutput = ["option-2", "option-3", "option-6"]; + + expect( + getOptionsFromSkuId( + "42aef769-c97e-48a8-a3c4-2af8ad504ebb", + variationMatrixSample, + ), + ).toEqual(expectedOutput); + }); + + test("mapOptionsToVariation should return the object mapping varitions to the selected option.", () => { + const variations: Partial[] = [ + { + id: "variation-1", + name: "Generic Sizes", + options: [ + { + id: "option-1", + description: "Small size", + name: "SM", + modifiers: [], + }, + { + id: "option-2", + description: "Medium size", + name: "MD", + modifiers: [], + }, + ], + }, + { + id: "variation-2", + name: "Simple T-Shirt Sleeve Length", + options: [ + { + id: "option-3", + description: "Simple T-Shirt with short sleeves", + name: "Short", + modifiers: [], + }, + { + id: "option-4", + description: "Simple T-Shirt with long sleeves", + name: "Long", + modifiers: [], + }, + ], + }, + ]; + + const selectedOptions = ["option-2", "option-3"]; + + const expectedOutput = { + "variation-1": "option-2", + "variation-2": "option-3", + }; + + expect( + mapOptionsToVariation(selectedOptions, variations as Variation[]), + ).toEqual(expectedOutput); + }); + + test("allVariationsHaveSelectedOption should return true if all variations keys have a defined value for their key value pair.", () => { + const variations: Partial[] = [ + { + id: "variation-1", + name: "Generic Sizes", + options: [ + { + id: "option-1", + description: "Small size", + name: "SM", + modifiers: [], + }, + { + id: "option-2", + description: "Medium size", + name: "MD", + modifiers: [], + }, + ], + }, + { + id: "variation-2", + name: "Simple T-Shirt Sleeve Length", + options: [ + { + id: "option-3", + description: "Simple T-Shirt with short sleeves", + name: "Short", + modifiers: [], + }, + { + id: "option-4", + description: "Simple T-Shirt with long sleeves", + name: "Long", + modifiers: [], + }, + ], + }, + ]; + + const optionDict = { + "variation-1": "option-2", + "variation-2": "option-3", + }; + + expect( + allVariationsHaveSelectedOption(optionDict, variations as Variation[]), + ).toEqual(true); + }); +}); diff --git a/examples/simple/src/lib/product-helper.ts b/examples/simple/src/lib/product-helper.ts new file mode 100644 index 00000000..1842a8e9 --- /dev/null +++ b/examples/simple/src/lib/product-helper.ts @@ -0,0 +1,81 @@ +import { CatalogsProductVariation, ProductResponse } from "@moltin/sdk"; +import { OptionDict } from "./types/product-types"; +import { MatrixObjectEntry, MatrixValue } from "./types/matrix-object-entry"; + +export const getSkuIdFromOptions = ( + options: string[], + matrix: MatrixObjectEntry | MatrixValue, +): string | undefined => { + if (typeof matrix === "string") { + return matrix; + } + + for (const currOption in options) { + const nestedMatrix = matrix[options[currOption]]; + if (nestedMatrix) { + return getSkuIdFromOptions(options, nestedMatrix); + } + } + + return undefined; +}; + +export const getOptionsFromSkuId = ( + skuId: string, + entry: MatrixObjectEntry | MatrixValue, + options: string[] = [], +): string[] | undefined => { + if (typeof entry === "string") { + return entry === skuId ? options : undefined; + } + + let acc: string[] | undefined; + Object.keys(entry).every((key) => { + const result = getOptionsFromSkuId(skuId, entry[key], [...options, key]); + if (result) { + acc = result; + return false; + } + return true; + }); + return acc; +}; + +// TODO refactor +export const mapOptionsToVariation = ( + options: string[], + variations: CatalogsProductVariation[], +): OptionDict => { + return variations.reduce( + (acc: OptionDict, variation: CatalogsProductVariation) => { + const x = variation.options.find((varOption) => + options.some((selectedOption) => varOption.id === selectedOption), + )?.id; + return { ...acc, [variation.id]: x ? x : "" }; + }, + {}, + ); +}; + +export function allVariationsHaveSelectedOption( + optionsDict: OptionDict, + variations: CatalogsProductVariation[], +): boolean { + return !variations.some((variation) => !optionsDict[variation.id]); +} + +export const isChildProductResource = (product: ProductResponse): boolean => + !product.attributes.base_product && !!product.attributes.base_product_id; + +export const isSimpleProductResource = (product: ProductResponse): boolean => + !product.attributes.base_product && !product.attributes.base_product_id; + +/** + * promise will resolve after 300ms. + */ +export const wait300 = new Promise((resolve) => { + const wait = setTimeout(() => { + clearTimeout(wait); + resolve(); + }, 300); +}); diff --git a/examples/simple/src/lib/product-util.test.ts b/examples/simple/src/lib/product-util.test.ts new file mode 100644 index 00000000..d1882706 --- /dev/null +++ b/examples/simple/src/lib/product-util.test.ts @@ -0,0 +1,315 @@ +import type { + File, + ProductResponse, + ShopperCatalogResource, + Variation, +} from "@moltin/sdk"; +import { describe, test, expect } from "vitest"; +import { + createEmptyOptionDict, + excludeChildProducts, + filterBaseProducts, + getProductMainImage, + processImageFiles, +} from "./product-util"; + +describe("product util", () => { + describe("unit tests", () => { + test("processImageFiles should return only supported images without the main image", () => { + const files: Partial[] = [ + { + type: "file", + id: "123", + mime_type: "image/jpeg", + }, + { + type: "file", + id: "456", + mime_type: "image/gif", + }, + { + type: "file", + id: "789", + mime_type: "image/jpeg", + }, + { + type: "file", + id: "101112", + mime_type: "image/png", + }, + { + type: "file", + id: "131415", + mime_type: "image/svg+xml", + }, + { + type: "file", + id: "161718", + mime_type: "image/webp", + }, + { + type: "file", + id: "192021", + mime_type: "video/mp4", + }, + { + type: "file", + id: "222324", + mime_type: "application/pdf", + }, + { + type: "file", + id: "252627", + mime_type: "application/vnd.ms-excel", + }, + { + type: "file", + id: "282930", + mime_type: "application/vnd.ms-powerpoint", + }, + { + type: "file", + id: "313233", + mime_type: "application/msword", + }, + ]; + + const expected: Partial[] = [ + { + type: "file", + id: "456", + mime_type: "image/gif", + }, + { + type: "file", + id: "789", + mime_type: "image/jpeg", + }, + { + type: "file", + id: "101112", + mime_type: "image/png", + }, + { + type: "file", + id: "131415", + mime_type: "image/svg+xml", + }, + { + type: "file", + id: "161718", + mime_type: "image/webp", + }, + ]; + expect(processImageFiles(files as File[], "123")).toEqual(expected); + }); + + test("processImageFiles should support an undefined main image id", () => { + const files: Partial[] = [ + { + type: "file", + id: "123", + mime_type: "image/jpeg", + }, + { + type: "file", + id: "456", + mime_type: "image/gif", + }, + ]; + + const expected: Partial[] = [ + { + type: "file", + id: "123", + mime_type: "image/jpeg", + }, + { + type: "file", + id: "456", + mime_type: "image/gif", + }, + ]; + expect(processImageFiles(files as File[])).toEqual(expected); + }); + + test("getProductMainImage should return a products main image file", () => { + const mainImageFile: Partial = { + type: "file", + id: "123", + mime_type: "image/jpeg", + }; + + const productResp: Partial> = { + included: { + main_images: [mainImageFile] as File[], + }, + }; + + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + mainImageFile, + ); + }); + + test("getProductMainImage should return null when product does not have main image included", () => { + const productResp: Partial> = { + included: {}, + }; + + expect(getProductMainImage(productResp.included?.main_images)).toEqual( + null, + ); + }); + + test("createEmptyOptionDict should return an OptionDict with all with variation keys assigned undefined values", () => { + const variations: Partial[] = [ + { + id: "variation-1", + name: "Generic Sizes", + options: [ + { + id: "option-1", + description: "Small size", + name: "SM", + modifiers: [], + }, + { + id: "option-2", + description: "Medium size", + name: "MD", + modifiers: [], + }, + ], + }, + { + id: "variation-2", + name: "Simple T-Shirt Sleeve Length", + options: [ + { + id: "option-3", + description: "Simple T-Shirt with short sleeves", + name: "Short", + modifiers: [], + }, + { + id: "option-4", + description: "Simple T-Shirt with long sleeves", + name: "Long", + modifiers: [], + }, + ], + }, + ]; + + const optionDict = { + "variation-1": undefined, + "variation-2": undefined, + }; + + expect(createEmptyOptionDict(variations as Variation[])).toEqual( + optionDict, + ); + }); + + test("filterBaseProducts should return only the base products from a list of ProductResponse", () => { + const products: any = [ + { + id: "123", + attributes: { + base_product: false, + base_product_id: "789", + }, + relationships: { + parent: { + data: { + id: "parent-id", + type: "product", + }, + }, + }, + }, + { + id: "456", + attributes: { + base_product: false, + }, + relationships: {}, + }, + { + id: "789", + attributes: { + base_product: true, + }, + relationships: {}, + }, + ]; + + const expected = [ + { + id: "789", + attributes: { + base_product: true, + }, + relationships: {}, + }, + ]; + const actual = filterBaseProducts(products as ProductResponse[]); + expect(actual).toEqual(expected); + }); + + test("excludeChildProducts should return only the products that are not child products", () => { + const products: any = [ + { + id: "123", + attributes: { + base_product: false, + base_product_id: "789", + }, + relationships: { + parent: { + data: { + id: "parent-id", + type: "product", + }, + }, + }, + }, + { + id: "456", + attributes: { + base_product: false, + }, + relationships: {}, + }, + { + id: "789", + attributes: { + base_product: true, + }, + relationships: {}, + }, + ]; + + const expected = [ + { + id: "456", + attributes: { + base_product: false, + }, + relationships: {}, + }, + { + id: "789", + attributes: { + base_product: true, + }, + relationships: {}, + }, + ]; + + expect(excludeChildProducts(products as ProductResponse[])).toEqual( + expected, + ); + }); + }); +}); diff --git a/examples/simple/src/lib/product-util.ts b/examples/simple/src/lib/product-util.ts new file mode 100644 index 00000000..5d6eab18 --- /dev/null +++ b/examples/simple/src/lib/product-util.ts @@ -0,0 +1,54 @@ +import type { + CatalogsProductVariation, + File, + ProductResponse, +} from "@moltin/sdk"; +import type { + IdentifiableBaseProduct, + OptionDict, +} from "./types/product-types"; + +export function processImageFiles(files: File[], mainImageId?: string) { + // filters out main image and keeps server order + const supportedMimeTypes = [ + "image/gif", + "image/jpeg", + "image/png", + "image/webp", + "image/svg+xml", + ]; + return files.filter( + (fileEntry) => + fileEntry.id !== mainImageId && + supportedMimeTypes.some((type) => fileEntry.mime_type === type), + ); +} + +export function getProductMainImage( + mainImages: File[] | undefined, +): File | null { + return mainImages?.[0] || null; +} + +// Using existance of parent relationship property to filter because only child products seem to have this property. +export const filterBaseProducts = ( + products: ProductResponse[], +): IdentifiableBaseProduct[] => + products.filter( + (product: ProductResponse): product is IdentifiableBaseProduct => + product.attributes.base_product, + ); + +// Using existance of parent relationship property to filter because only child products seem to have this property. +export const excludeChildProducts = ( + products: ProductResponse[], +): IdentifiableBaseProduct[] => + products.filter( + (product: ProductResponse): product is IdentifiableBaseProduct => + !product?.relationships?.parent, + ); + +export const createEmptyOptionDict = ( + variations: CatalogsProductVariation[], +): OptionDict => + variations.reduce((acc, c) => ({ ...acc, [c.id]: undefined }), {}); diff --git a/examples/simple/src/lib/providers/store-provider.tsx b/examples/simple/src/lib/providers/store-provider.tsx new file mode 100644 index 00000000..e9eb75dd --- /dev/null +++ b/examples/simple/src/lib/providers/store-provider.tsx @@ -0,0 +1,19 @@ +import { StoreProviderProps } from "@elasticpath/react-shopper-hooks"; +import { StoreProvider } from "@elasticpath/react-shopper-hooks"; +import { getEpccImplicitClient } from "../epcc-implicit-client"; +import { getCartCookieClient } from "../cart-cookie-client"; + +const StoreNextJSProvider = ( + props: Omit, +) => { + const client = getEpccImplicitClient(); + return ( + + ); +}; + +export default StoreNextJSProvider; diff --git a/examples/simple/src/lib/resolve-cart-env.ts b/examples/simple/src/lib/resolve-cart-env.ts new file mode 100644 index 00000000..d4f29eba --- /dev/null +++ b/examples/simple/src/lib/resolve-cart-env.ts @@ -0,0 +1,11 @@ +export const COOKIE_PREFIX_KEY = cartEnv(); + +function cartEnv(): string { + const cookiePrefixKey = process.env.NEXT_PUBLIC_COOKIE_PREFIX_KEY; + if (!cookiePrefixKey) { + throw new Error( + `Failed to get cart cookie key environment variables cookiePrefixKey. \n Make sure you have set NEXT_PUBLIC_COOKIE_PREFIX_KEY`, + ); + } + return cookiePrefixKey; +} diff --git a/examples/simple/src/lib/resolve-ep-currency-code.ts b/examples/simple/src/lib/resolve-ep-currency-code.ts new file mode 100644 index 00000000..999cdaf3 --- /dev/null +++ b/examples/simple/src/lib/resolve-ep-currency-code.ts @@ -0,0 +1,13 @@ +import { getCookie } from "cookies-next"; +import { COOKIE_PREFIX_KEY } from "./resolve-cart-env"; + +export const EP_CURRENCY_CODE = retrieveCurrency(); + +function retrieveCurrency(): string { + const currencyInCookie = getCookie(`${COOKIE_PREFIX_KEY}_ep_currency`); + return ( + (typeof currencyInCookie === "string" + ? currencyInCookie + : process.env.NEXT_PUBLIC_DEFAULT_CURRENCY_CODE) || "USD" + ); +} diff --git a/examples/simple/src/lib/resolve-epcc-env.ts b/examples/simple/src/lib/resolve-epcc-env.ts new file mode 100644 index 00000000..7852a5c9 --- /dev/null +++ b/examples/simple/src/lib/resolve-epcc-env.ts @@ -0,0 +1,21 @@ +export const epccEnv = resolveEpccEnv(); + +function resolveEpccEnv(): { + client_id: string; + host?: string; + client_secret?: string; +} { + const { host, client_id, client_secret } = { + host: process.env.NEXT_PUBLIC_EPCC_ENDPOINT_URL, + client_id: process.env.NEXT_PUBLIC_EPCC_CLIENT_ID, + client_secret: process.env.EPCC_CLIENT_SECRET, + }; + + if (!client_id) { + throw new Error( + `Failed to get Elasticpath Commerce Cloud client_id environment variables client_id: \n Make sure you have set NEXT_PUBLIC_EPCC_CLIENT_ID`, + ); + } + + return { host, client_id, client_secret }; +} diff --git a/examples/simple/src/lib/resolve-shopping-cart-props.ts b/examples/simple/src/lib/resolve-shopping-cart-props.ts new file mode 100644 index 00000000..9afd4ef9 --- /dev/null +++ b/examples/simple/src/lib/resolve-shopping-cart-props.ts @@ -0,0 +1,29 @@ +import { CartState, PresentCartState } from "@elasticpath/react-shopper-hooks"; +import { ICart } from "../components/cart/Cart"; +import { getPresentCartState } from "@elasticpath/react-shopper-hooks"; + +export function resolveShoppingCartProps( + state: CartState, + removeCartItem: (itemId: string) => Promise, +): ICart | undefined { + /** + * Checking if the current cart state is a present cart or updating with a previous state of present cart + * as in both cases we want to show cart items + */ + const resolvePresentCartState: PresentCartState | undefined = + getPresentCartState(state); + + if (resolvePresentCartState) { + const { id, withTax, withoutTax, groupedItems, items } = + resolvePresentCartState; + return { + id, + totalPrice: withTax, + subtotal: withoutTax, + items, + groupedItems: groupedItems, + removeCartItem: removeCartItem, + }; + } + return; +} diff --git a/examples/simple/src/lib/sort-alphabetically.ts b/examples/simple/src/lib/sort-alphabetically.ts new file mode 100644 index 00000000..a69ce476 --- /dev/null +++ b/examples/simple/src/lib/sort-alphabetically.ts @@ -0,0 +1,4 @@ +export const sortAlphabetically = ( + a: { name: string }, + b: { name: string }, +): number => a.name.localeCompare(b.name); diff --git a/examples/simple/src/lib/to-base-64.ts b/examples/simple/src/lib/to-base-64.ts new file mode 100644 index 00000000..87b9edc2 --- /dev/null +++ b/examples/simple/src/lib/to-base-64.ts @@ -0,0 +1,4 @@ +export const toBase64 = (str: string): string => + typeof window === "undefined" + ? Buffer.from(str).toString("base64") + : window.btoa(str); diff --git a/examples/simple/src/lib/token-expired.ts b/examples/simple/src/lib/token-expired.ts new file mode 100644 index 00000000..694a760f --- /dev/null +++ b/examples/simple/src/lib/token-expired.ts @@ -0,0 +1,3 @@ +export function tokenExpired(expires: number): boolean { + return Math.floor(Date.now() / 1000) >= expires; +} diff --git a/examples/simple/src/lib/types/deep-partial.ts b/examples/simple/src/lib/types/deep-partial.ts new file mode 100644 index 00000000..e422dd51 --- /dev/null +++ b/examples/simple/src/lib/types/deep-partial.ts @@ -0,0 +1,3 @@ +export type DeepPartial = { + [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; +}; diff --git a/examples/simple/src/lib/types/matrix-object-entry.ts b/examples/simple/src/lib/types/matrix-object-entry.ts new file mode 100644 index 00000000..a214bbc1 --- /dev/null +++ b/examples/simple/src/lib/types/matrix-object-entry.ts @@ -0,0 +1,5 @@ +export type MatrixValue = string; + +export interface MatrixObjectEntry { + [key: string]: MatrixObjectEntry | MatrixValue; +} diff --git a/examples/simple/src/lib/types/non-empty-array.ts b/examples/simple/src/lib/types/non-empty-array.ts new file mode 100644 index 00000000..af797df1 --- /dev/null +++ b/examples/simple/src/lib/types/non-empty-array.ts @@ -0,0 +1,3 @@ +export interface NonEmptyArray extends Array { + 0: A; +} diff --git a/examples/simple/src/lib/types/product-types.ts b/examples/simple/src/lib/types/product-types.ts new file mode 100644 index 00000000..a18a260d --- /dev/null +++ b/examples/simple/src/lib/types/product-types.ts @@ -0,0 +1,31 @@ +import type { ProductResponse, File } from "@moltin/sdk"; +import type { Dispatch, SetStateAction } from "react"; + +export type IdentifiableBaseProduct = ProductResponse & { + id: string; + attributes: { slug: string; sku: string; base_product: true }; +}; + +export interface ProductContextState { + isChangingSku: boolean; + setIsChangingSku: Dispatch>; +} + +export interface ProductModalContextState { + isChangingSku: boolean; + setIsChangingSku: Dispatch>; + changedSkuId: string; + setChangedSkuId: Dispatch>; +} + +export interface OptionDict { + [key: string]: string; +} + +export interface ProductResponseWithImage extends ProductResponse { + main_image?: File; +} + +export interface ProductImageObject { + [key: string]: File; +} diff --git a/examples/simple/src/lib/types/read-only-non-empty-array.ts b/examples/simple/src/lib/types/read-only-non-empty-array.ts new file mode 100644 index 00000000..9877640a --- /dev/null +++ b/examples/simple/src/lib/types/read-only-non-empty-array.ts @@ -0,0 +1,7 @@ +export type ReadonlyNonEmptyArray = ReadonlyArray & { + readonly 0: A; +}; + +export const isNonEmpty = ( + as: ReadonlyArray, +): as is ReadonlyNonEmptyArray => as.length > 0; diff --git a/examples/simple/src/lib/types/store-context.ts b/examples/simple/src/lib/types/store-context.ts new file mode 100644 index 00000000..877d788a --- /dev/null +++ b/examples/simple/src/lib/types/store-context.ts @@ -0,0 +1,17 @@ +import { NavigationNode } from "../build-site-navigation"; +import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk"; + +interface StoreContextBase { + nav: NavigationNode[]; +} + +export interface StoreContextSSR extends StoreContextBase { + type: "store-context-ssr"; + cart?: ResourceIncluded; +} + +export interface StoreContextSSG extends StoreContextBase { + type: "store-context-ssg"; +} + +export type StoreContext = StoreContextSSR | StoreContextSSG; diff --git a/examples/simple/src/middleware.ts b/examples/simple/src/middleware.ts new file mode 100644 index 00000000..a06a5712 --- /dev/null +++ b/examples/simple/src/middleware.ts @@ -0,0 +1,21 @@ +import { NextRequest } from "next/server"; +import { middlewareRunner } from "./lib/middleware/middleware-runner"; +import { implicitAuthMiddleware } from "./lib/middleware/implicit-auth-middleware"; +import { cartCookieMiddleware } from "./lib/middleware/cart-cookie-middleware"; + +export async function middleware(req: NextRequest) { + return middlewareRunner( + { + runnable: implicitAuthMiddleware, + options: { + exclude: ["/_next", "/configuration-error"], + }, + }, + { + runnable: cartCookieMiddleware, + options: { + exclude: ["/_next", "/configuration-error"], + }, + }, + )(req); +} diff --git a/examples/simple/src/services/cart.ts b/examples/simple/src/services/cart.ts new file mode 100644 index 00000000..fcf06377 --- /dev/null +++ b/examples/simple/src/services/cart.ts @@ -0,0 +1,22 @@ +import type { Moltin as EPCCClient } from "@moltin/sdk"; +import { Cart, CartIncluded, ResourceIncluded } from "@moltin/sdk"; + +export interface CustomItemRequest { + type: "custom_item"; + name: string; + quantity: number; + price: { + amount: number; + includes_tax?: boolean; + }; + sku?: string; + description?: string; + custom_inputs?: Record; +} + +export async function getCart( + cartId: string, + client: EPCCClient, +): Promise> { + return client.Cart(cartId).With("items").Get(); +} diff --git a/examples/simple/src/services/checkout.ts b/examples/simple/src/services/checkout.ts new file mode 100644 index 00000000..43e12740 --- /dev/null +++ b/examples/simple/src/services/checkout.ts @@ -0,0 +1,34 @@ +import type { + Order, + PaymentRequestBody, + Address, + CheckoutCustomerObject, + Moltin as EPCCClient, + ConfirmPaymentResponse, +} from "@moltin/sdk"; + +export function checkout( + id: string, + customer: CheckoutCustomerObject, + billing: Partial
, + shipping: Partial
, + client: EPCCClient, +): Promise<{ data: Order }> { + return client.Cart(id).Checkout(customer, billing, shipping); +} + +export function makePayment( + payment: PaymentRequestBody, + orderId: string, + client: EPCCClient, +): Promise { + return client.Orders.Payment(orderId, payment); +} + +export function confirmOrder( + orderId: string, + transactionId: string, + client: EPCCClient, +): Promise { + return client.Orders.Confirm(orderId, transactionId, { data: {} }); +} diff --git a/examples/simple/src/services/hierarchy.ts b/examples/simple/src/services/hierarchy.ts new file mode 100644 index 00000000..62eab71d --- /dev/null +++ b/examples/simple/src/services/hierarchy.ts @@ -0,0 +1,28 @@ +import type { Node, Hierarchy } from "@moltin/sdk"; +import { Moltin as EPCCClient } from "@moltin/sdk"; + +export async function getHierarchies(client: EPCCClient): Promise { + const result = await client.ShopperCatalog.Hierarchies.All(); + return result.data; +} + +export async function getHierarchyChildren( + hierarchyId: string, + client: EPCCClient, +): Promise { + const result = await client.ShopperCatalog.Hierarchies.GetHierarchyChildren({ + hierarchyId, + }); + return result.data; +} + +export async function getHierarchyNodes( + hierarchyId: string, + client: EPCCClient, +): Promise { + const result = await client.ShopperCatalog.Hierarchies.GetHierarchyNodes({ + hierarchyId, + }); + + return result.data; +} diff --git a/examples/simple/src/services/products.ts b/examples/simple/src/services/products.ts new file mode 100644 index 00000000..037ba757 --- /dev/null +++ b/examples/simple/src/services/products.ts @@ -0,0 +1,68 @@ +import type { + ProductResponse, + ResourcePage, + ShopperCatalogResource, +} from "@moltin/sdk"; +import { wait300 } from "../lib/product-helper"; +import { Moltin as EPCCClient } from "@moltin/sdk"; + +export async function getProductById( + productId: string, + client: EPCCClient, +): Promise> { + return client.ShopperCatalog.Products.With([ + "main_image", + "files", + "component_products", + ]).Get({ + productId, + }); +} + +export function getAllProducts(client: EPCCClient): Promise { + return _getAllProductPages(client)(); +} + +export function getProducts(client: EPCCClient, offset = 0, limit = 100) { + return client.ShopperCatalog.Products.With(["main_image"]) + .Limit(limit) + .Offset(offset) + .All(); +} + +const _getAllPages = + ( + nextPageRequestFn: ( + limit: number, + offset: number, + client?: EPCCClient, + ) => Promise>, + ) => + async ( + offset: number = 0, + limit: number = 25, + accdata: T[] = [], + ): Promise => { + const requestResp = await nextPageRequestFn(limit, offset); + const { + meta: { + page: newPage, + results: { total }, + }, + data: newData, + } = requestResp; + + const updatedOffset = offset + newPage.total; + const combinedData = [...accdata, ...newData]; + if (updatedOffset < total) { + return wait300.then(() => + _getAllPages(nextPageRequestFn)(updatedOffset, limit, combinedData), + ); + } + return Promise.resolve(combinedData); + }; + +const _getAllProductPages = (client: EPCCClient) => + _getAllPages((limit = 25, offset = 0) => + client.ShopperCatalog.Products.Limit(limit).Offset(offset).All(), + ); diff --git a/examples/simple/src/styles/globals.css b/examples/simple/src/styles/globals.css new file mode 100644 index 00000000..290750fe --- /dev/null +++ b/examples/simple/src/styles/globals.css @@ -0,0 +1,64 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + .nav-button-container { + @apply rounded-md px-4 py-2 transition-all duration-200 hover:bg-slate-200/70; + } + + .link-hover { + @apply hover:text-brand-primary hover:underline; + } + + .primary-btn { + @apply flex w-full items-center justify-center rounded-md bg-brand-primary px-4 py-2 font-semibold text-white transition-all duration-200 hover:bg-brand-highlight; + } + + .secondary-btn { + @apply flex w-full items-center justify-center rounded-md border border-black bg-transparent px-4 py-2 font-semibold text-black transition-all duration-200 hover:border-brand-highlight hover:text-brand-primary; + } +} + +html, +body { + padding: 0; + margin: 0; + height: 100%; + font-family: + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Roboto, + Oxygen, + Ubuntu, + Cantarell, + Fira Sans, + Droid Sans, + Helvetica Neue, + sans-serif; +} + +#__next { + display: flex; + flex-direction: column; + height: 100%; +} + +main { + flex: 1; +} + +a { + color: inherit; + text-decoration: none; +} + +* { + box-sizing: border-box; +} + +.carousel__slide-focus-ring { + display: none !important; + outline-width: 0 !important; +} diff --git a/examples/simple/tailwind.config.ts b/examples/simple/tailwind.config.ts new file mode 100644 index 00000000..5ca516bc --- /dev/null +++ b/examples/simple/tailwind.config.ts @@ -0,0 +1,42 @@ +import type { Config } from "tailwindcss"; + +export default { + content: ["./src/**/*.{js,jsx,ts,tsx}"], + theme: { + extend: { + maxWidth: { + "base-max-width": "80rem", + }, + colors: { + brand: { + primary: "#2BCC7E", + secondary: "#144E31", + highlight: "#56DC9B", + primaryAlt: "#EA7317", + secondaryAlt: "#ffcb47", + }, + }, + keyframes: { + fadeIn: { + from: { opacity: "0" }, + to: { opacity: "1" }, + }, + marquee: { + "0%": { transform: "translateX(0%)" }, + "100%": { transform: "translateX(-100%)" }, + }, + blink: { + "0%": { opacity: "0.2" }, + "20%": { opacity: "1" }, + "100% ": { opacity: "0.2" }, + }, + }, + animation: { + fadeIn: "fadeIn .3s ease-in-out", + carousel: "marquee 60s linear infinite", + blink: "blink 1.4s both infinite", + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/examples/simple/tsconfig.json b/examples/simple/tsconfig.json new file mode 100644 index 00000000..1780e064 --- /dev/null +++ b/examples/simple/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "es6", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ] + }, + "include": [ + "next-env.d.ts", + "src/**/*.ts", + "src/**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"], + "types": ["global.d.ts"] +} diff --git a/examples/simple/vite.config.ts b/examples/simple/vite.config.ts new file mode 100644 index 00000000..9da935db --- /dev/null +++ b/examples/simple/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig, defaultExclude } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: ["e2e/**/*", ...defaultExclude], + coverage: { + provider: "istanbul", + }, + }, +}); diff --git a/scripts/example-specs/configuration.ts b/scripts/example-specs/configuration.ts index 20e029f2..e1b1b666 100644 --- a/scripts/example-specs/configuration.ts +++ b/scripts/example-specs/configuration.ts @@ -39,7 +39,24 @@ interface Configuration> { export const configuration: Configuration = { specs: [ { - name: "basic", + name: "simple", + args: { + epccClientId: process.env.EPCC_CLIENT_ID, + epccClientSecret: process.env.EPCC_CLIENT_SECRET, + epccEndpointUrl: process.env.EPCC_ENDPOINT, + skipGit: true, + skipInstall: true, + skipConfig: true, + plpType: "None" as PlpType.None, + name: "simple", + dryRun: false, + interactive: false, + paymentGatewayType: "Manual" as PaymentGatewayType.Manual, + packageManager: "pnpm", + }, + }, + { + name: "payments", args: { epccClientId: process.env.EPCC_CLIENT_ID, epccClientSecret: process.env.EPCC_CLIENT_SECRET, @@ -51,7 +68,7 @@ export const configuration: Configuration = { epPaymentsStripeAccountId: process.env.EP_PAYMENTS_STRIPE_ACCOUNT_ID, epPaymentsStripePublishableKey: process.env.EP_PAYMENTS_STRIPE_PUBLISHABLE_KEY, - name: "basic", + name: "payments", dryRun: false, interactive: false, paymentGatewayType: "EP Payments" as PaymentGatewayType.EpPayments, From 5e401a215f826c04bfe9daa4d7ed3bfe4c05065d Mon Sep 17 00:00:00 2001 From: Robert Field Date: Thu, 16 Nov 2023 17:35:52 +0100 Subject: [PATCH 3/4] chore: lock file --- pnpm-lock.yaml | 253 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 242 insertions(+), 11 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2bdbf302..290b5a6b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,11 +97,11 @@ importers: specifier: ^1.4.7 version: 1.4.7(classnames@2.3.2)(react-instantsearch-dom@6.40.4)(react@18.2.0) '@elasticpath/react-shopper-hooks': - specifier: workspace:^0.6.0 - version: link:../../packages/react-shopper-hooks + specifier: ^0.5.1 + version: 0.5.4(@moltin/sdk@25.1.1)(encoding@0.1.13)(react-dom@18.2.0)(react@18.2.0) '@elasticpath/shopper-common': - specifier: workspace:^0.2.0 - version: link:../../packages/shopper-common + specifier: ^0.1.1 + version: 0.1.3(encoding@0.1.13) '@headlessui/react': specifier: ^1.7.17 version: 1.7.17(react-dom@18.2.0)(react@18.2.0) @@ -251,14 +251,14 @@ importers: specifier: ^0.34.5 version: 0.34.6 - examples/basic: + examples/payments: dependencies: '@elasticpath/react-shopper-hooks': - specifier: workspace:^0.6.0 - version: link:../../packages/react-shopper-hooks + specifier: ^0.5.1 + version: 0.5.4(@moltin/sdk@25.1.1)(encoding@0.1.13)(react-dom@18.2.0)(react@18.2.0) '@elasticpath/shopper-common': - specifier: workspace:^0.2.0 - version: link:../../packages/shopper-common + specifier: ^0.1.1 + version: 0.1.3(encoding@0.1.13) '@headlessui/react': specifier: ^1.7.17 version: 1.7.17(react-dom@18.2.0)(react@18.2.0) @@ -379,7 +379,7 @@ importers: version: 8.4.31 prettier: specifier: ^3.0.3 - version: 3.0.3 + version: 3.1.0 prettier-eslint: specifier: ^15.0.1 version: 15.0.1 @@ -388,7 +388,152 @@ importers: version: 7.1.0(prettier-eslint@15.0.1) prettier-plugin-tailwindcss: specifier: ^0.5.4 - version: 0.5.6(prettier@3.0.3) + version: 0.5.6(prettier@3.1.0) + tailwindcss: + specifier: ^3.3.3 + version: 3.3.4(ts-node@10.9.1) + typescript: + specifier: ^5.2.2 + version: 5.2.2 + vite: + specifier: ^4.2.1 + version: 4.5.0(@types/node@18.7.3) + vitest: + specifier: ^0.34.5 + version: 0.34.6 + + examples/simple: + dependencies: + '@elasticpath/react-shopper-hooks': + specifier: ^0.5.1 + version: 0.5.4(@moltin/sdk@25.1.1)(encoding@0.1.13)(react-dom@18.2.0)(react@18.2.0) + '@elasticpath/shopper-common': + specifier: ^0.1.1 + version: 0.1.3(encoding@0.1.13) + '@headlessui/react': + specifier: ^1.7.17 + version: 1.7.17(react-dom@18.2.0)(react@18.2.0) + '@heroicons/react': + specifier: ^2.0.18 + version: 2.0.18(react@18.2.0) + '@moltin/sdk': + specifier: ^25.0.2 + version: 25.1.1(encoding@0.1.13) + clsx: + specifier: ^1.2.1 + version: 1.2.1 + cookies-next: + specifier: ^4.0.0 + version: 4.0.0 + focus-visible: + specifier: ^5.2.0 + version: 5.2.0 + formik: + specifier: ^2.2.9 + version: 2.4.5(react@18.2.0) + next: + specifier: ^14.0.0 + version: 14.0.0(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0) + pure-react-carousel: + specifier: ^1.29.0 + version: 1.30.1(react-dom@18.2.0)(react@18.2.0) + rc-slider: + specifier: ^10.3.0 + version: 10.3.1(react-dom@18.2.0)(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-device-detect: + specifier: ^2.2.2 + version: 2.2.3(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-toastify: + specifier: ^9.1.3 + version: 9.1.3(react-dom@18.2.0)(react@18.2.0) + server-only: + specifier: ^0.0.1 + version: 0.0.1 + zod: + specifier: ^3.22.4 + version: 3.22.4 + zod-formik-adapter: + specifier: ^1.2.0 + version: 1.2.0(formik@2.4.5)(zod@3.22.4) + devDependencies: + '@babel/core': + specifier: ^7.18.10 + version: 7.23.2 + '@next/bundle-analyzer': + specifier: ^14.0.0 + version: 14.0.0 + '@next/env': + specifier: ^14.0.0 + version: 14.0.0 + '@playwright/test': + specifier: ^1.28.1 + version: 1.39.0 + '@svgr/webpack': + specifier: ^6.3.1 + version: 6.5.1 + '@testing-library/jest-dom': + specifier: ^6.1.3 + version: 6.1.4(@types/jest@29.5.6)(vitest@0.34.6) + '@testing-library/react': + specifier: ^14.0.0 + version: 14.0.0(react-dom@18.2.0)(react@18.2.0) + '@types/node': + specifier: 18.7.3 + version: 18.7.3 + '@types/react': + specifier: ^18.2.33 + version: 18.2.33 + '@types/react-dom': + specifier: ^18.2.14 + version: 18.2.14 + '@vitest/coverage-istanbul': + specifier: ^0.34.5 + version: 0.34.6(vitest@0.34.6) + autoprefixer: + specifier: ^10.4.14 + version: 10.4.16(postcss@8.4.31) + babel-loader: + specifier: ^8.2.5 + version: 8.3.0(@babel/core@7.23.2)(webpack@5.89.0) + encoding: + specifier: ^0.1.13 + version: 0.1.13 + eslint: + specifier: ^8.49.0 + version: 8.52.0 + eslint-config-next: + specifier: ^14.0.0 + version: 14.0.0(eslint@8.52.0)(typescript@5.2.2) + eslint-config-prettier: + specifier: ^9.0.0 + version: 9.0.0(eslint@8.52.0) + eslint-plugin-react: + specifier: ^7.33.2 + version: 7.33.2(eslint@8.52.0) + lint-staged: + specifier: ^13.0.3 + version: 13.3.0 + postcss: + specifier: ^8.4.30 + version: 8.4.31 + prettier: + specifier: ^3.0.3 + version: 3.1.0 + prettier-eslint: + specifier: ^15.0.1 + version: 15.0.1 + prettier-eslint-cli: + specifier: ^7.1.0 + version: 7.1.0(prettier-eslint@15.0.1) + prettier-plugin-tailwindcss: + specifier: ^0.5.4 + version: 0.5.6(prettier@3.1.0) tailwindcss: specifier: ^3.3.3 version: 3.3.4(ts-node@10.9.1) @@ -2731,6 +2876,31 @@ packages: node-source-walk: 6.0.2 dev: true + /@elasticpath/react-shopper-hooks@0.5.4(@moltin/sdk@25.1.1)(encoding@0.1.13)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-JnQRsPF4oCiKGcaNiR9/cy+RzDUF/2Ng4wnkDLgoX8VQoAQa/uZtcL+5H9buYxfqOW3R47P8uXcM5KDrK9fLZQ==} + peerDependencies: + '@moltin/sdk': ^25.0.2 + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + '@elasticpath/shopper-common': 0.1.3(encoding@0.1.13) + '@moltin/sdk': 25.1.1(encoding@0.1.13) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + rxjs: 7.5.7 + transitivePeerDependencies: + - encoding + dev: false + + /@elasticpath/shopper-common@0.1.3(encoding@0.1.13): + resolution: {integrity: sha512-+jmas9qoiX63BWNyQMLtGHu+oH1d8lFeLZVE+T9VMVq35d+FhGVl+rsbsHZCA2B5CE2KE9a+quEiVJzK5+0TYA==} + dependencies: + '@moltin/sdk': 25.1.1(encoding@0.1.13) + tslib: 2.6.2 + transitivePeerDependencies: + - encoding + dev: false + /@esbuild/android-arm64@0.17.19: resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==} engines: {node: '>=12'} @@ -17715,6 +17885,61 @@ packages: prettier: 3.0.3 dev: true + /prettier-plugin-tailwindcss@0.5.6(prettier@3.1.0): + resolution: {integrity: sha512-2Xgb+GQlkPAUCFi3sV+NOYcSI5XgduvDBL2Zt/hwJudeKXkyvRS65c38SB0yb9UB40+1rL83I6m0RtlOQ8eHdg==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@shufo/prettier-plugin-blade': '*' + '@trivago/prettier-plugin-sort-imports': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + prettier-plugin-twig-melody: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@shufo/prettier-plugin-blade': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier-plugin-twig-melody: + optional: true + dependencies: + prettier: 3.1.0 + dev: true + /prettier@2.8.8: resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} engines: {node: '>=10.13.0'} @@ -17727,6 +17952,12 @@ packages: hasBin: true dev: true + /prettier@3.1.0: + resolution: {integrity: sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==} + engines: {node: '>=14'} + hasBin: true + dev: true + /pretty-format@23.6.0: resolution: {integrity: sha512-zf9NV1NSlDLDjycnwm6hpFATCGl/K1lt0R/GdkAK2O5LN/rwJoB+Mh93gGJjut4YbmecbfgLWVGSTCr0Ewvvbw==} dependencies: From 6409b7088005ba75ea60ed0063b276524415303c Mon Sep 17 00:00:00 2001 From: Robert Field Date: Thu, 16 Nov 2023 17:43:21 +0100 Subject: [PATCH 4/4] chore: update basic references to simple --- .github/workflows/generate-examples.yml | 198 +----------------------- package.json | 4 +- 2 files changed, 5 insertions(+), 197 deletions(-) diff --git a/.github/workflows/generate-examples.yml b/.github/workflows/generate-examples.yml index 4ec603d4..3515e80e 100644 --- a/.github/workflows/generate-examples.yml +++ b/.github/workflows/generate-examples.yml @@ -141,7 +141,7 @@ jobs: run: | echo "${{ secrets.TEST_ENV_FILE }}" > .env.test echo NEXT_PUBLIC_CI=true >> .env.test - echo NEXT_PUBLIC_CI=true >> ./examples/basic/.env.test + echo NEXT_PUBLIC_CI=true >> ./examples/simple/.env.test - name: Build everything run: pnpm build:e2e @@ -166,7 +166,7 @@ jobs: uses: actions/upload-artifact@v3 with: name: test-results - path: examples/basic/test-results + path: examples/simple/test-results continue-on-error: true - name: Report "Run examples e2e tests" conclusion @@ -176,196 +176,4 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} id: examples-e2e-tests conclusion: ${{ steps.examples-e2e-tests.outcome }} - fail-on-error: true - -# commit-latest-examples: -# name: Commit latest examples -# needs: generate-examples -# runs-on: ubuntu-latest -# outputs: -# commit_sha: ${{ steps.commit_changes.outputs.commit_long_sha }} -# steps: -# - name: Checkout Repo -# uses: actions/checkout@v3 -# -# - name: Install rimraf -# run: npm i -g rimraf@3.0.0 -# -# - name: Clear all previous examples versions -# run: mkdir -p examples && rimraf ./examples/* -# -# - uses: actions/download-artifact@v3 -# with: -# path: temp-artifact-workspace -# -# -# -# - name: Display structure of downloaded files -# run: ls -R - - -# - name: Commit changes -# id: commit_changes -# uses: EndBug/add-and-commit@v9 -# with: -# message: 'chore: generated latest ${{ matrix.example }} example' -# committer_name: GitHub Actions -# committer_email: 41898282+github-actions[bot]@users.noreply.github.com -# default_author: github_actions -# add: '*' - -# build: -# name: Create latest examples -# runs-on: ubuntu-latest -# outputs: -# commit_sha: ${{ steps.commit_changes.outputs.commit_long_sha }} -# steps: -# - name: Checkout Repo -# uses: actions/checkout@v3 -# -# - name: Setup Node.js 16.x -# uses: actions/setup-node@v3 -# with: -# node-version: 16.x -# -# - name: Install Dependencies -# run: yarn -# -# - name: Make .env.examples file -# run: | -# echo "${{ secrets.EXAMPLES_ENV_FILE }}" > .env.examples -# -# - name: Build Latest CLI and generate examples -# run: yarn examples -# -# - name: Commit changes -# id: commit_changes -# uses: EndBug/add-and-commit@v9 -# with: -# message: 'chore: generate latest examples' -# committer_name: GitHub Actions -# committer_email: 41898282+github-actions[bot]@users.noreply.github.com -# default_author: github_actions -# add: '*' -# -# - name: Display structure of directory pre zip -# run: ls -Ra -# -# - name: Zip build artifact -# run: zip app-build.zip ./.next -r -# -# - name: Display structure of directory post zip -# run: ls -Ra -# -# - name: Upload test results -# if: always() -# uses: actions/upload-artifact@v3 -# with: -# name: app-build -# path: ./app-build.zip -# -# run-unit-basic: -# name: Run unit tests for examples -# needs: build -# defaults: -# run: -# working-directory: examples/basic -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@v3 -# -# - uses: actions/download-artifact@v3 -# with: -# name: app-build -# -# - name: Display structure of downloaded files pre unzip -# run: ls -R -# -# - name: unzip build artifact -# run: unzip app-build.zip -# -# - name: Display structure of downloaded files post unzip -# run: ls -R -# -# - name: Run unit tests for latest examples -# run: yarn test:coverage -# - name: Upload test results -# if: always() -# uses: actions/upload-artifact@v3 -# with: -# name: coverage -# path: examples/basic/coverage -# -# - name: "Create a check run" -# if: ${{ needs.build.outputs.commit_sha }} -# uses: actions/github-script@v6 -# env: -# parameter_url: '${{ github.event.workflow_run.html_url }}' -# with: -# debug: ${{ secrets.ACTIONS_STEP_DEBUG || false }} -# script: | -# await github.rest.checks.create({ -# owner: context.repo.owner, -# repo: context.repo.repo, -# head_sha: "${{ needs.build.outputs.commit_sha }}", -# name: "Unit Tests", -# status: "completed", -# conclusion: "success", -# output: { -# title: "Unit Tests", -# summary: "my *check* summary", -# text: "my text", -# }, -# }); -# -# run-e2e-basic: -# name: Run e2e tests for examples -# needs: build -# defaults: -# run: -# working-directory: examples/basic -# runs-on: ubuntu-latest -# steps: -# - name: Checkout files -# uses: Bhacaz/checkout-files@v2 -# with: -# files: examples/basic -# branch: ${{ needs.build.outputs.commit_sha || github.sha }} -# -# - name: Make .env.test file -# run: | -# echo "${{ secrets.TEST_ENV_FILE }}" > .env.test -# -# - name: Install Dependencies -# run: yarn --ignore-scripts -# -# - name: Run e2e tests for latest examples -# run: yarn test:ci:e2e -# - name: Upload test results -# if: always() -# uses: actions/upload-artifact@v3 -# with: -# name: test-results -# path: examples/basic/test-results -# -# - name: "Create a check run" -# if: ${{ needs.build.outputs.commit_sha }} -# uses: actions/github-script@v6 -# env: -# parameter_url: '${{ github.event.workflow_run.html_url }}' -# with: -# debug: ${{ secrets.ACTIONS_STEP_DEBUG || false }} -# script: | -# await github.rest.checks.create({ -# owner: context.repo.owner, -# repo: context.repo.repo, -# head_sha: "${{ needs.build.outputs.commit_sha }}", -# name: "Generate Examples", -# status: "completed", -# conclusion: "success", -# output: { -# title: "Generate Examples", -# summary: "my *check* summary", -# text: "my text", -# }, -# }); + fail-on-error: true \ No newline at end of file diff --git a/package.json b/package.json index 453f33b6..d884bde6 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ "test": "turbo run test", "test:packages": "turbo run test --filter='./packages/*'", "test:watch": "turbo run test:watch", - "start:e2e": "turbo run start:e2e --filter='./examples/basic'", - "test:e2e": "NODE_ENV=test pnpm start:e2e & (sleep 5 && turbo run test:e2e --filter='./examples/basic' && kill $(lsof -t -i tcp:3000))", + "start:e2e": "turbo run start:e2e --filter='./examples/simple'", + "test:e2e": "NODE_ENV=test pnpm start:e2e & (sleep 5 && turbo run test:e2e --filter='./examples/simple' && kill $(lsof -t -i tcp:3000))", "build:cli": "turbo run build --filter=composable-cli...", "build:packages": "turbo run build --filter='./packages/*'", "ci:version": "changeset version && pnpm install --no-frozen-lockfile",