diff --git a/composable-ui/src/components/cms/commerce-connector.tsx b/composable-ui/src/components/cms/commerce-connector.tsx index de89109..c2bf978 100644 --- a/composable-ui/src/components/cms/commerce-connector.tsx +++ b/composable-ui/src/components/cms/commerce-connector.tsx @@ -1,34 +1,9 @@ import { Button, Container, Flex, HStack, Text } from '@chakra-ui/react' +import { CommerceConnectorProps, PriceProps } from '@composable/types' import { ProductCard } from '@composable/ui' import { useRouter } from 'next/router' -export interface GenericConnectorProps { - title?: string - ctaLabel?: string - ctaHref?: string - ctaHeight?: string - ctaMaxWidth?: string - ctaMinWidth?: string - products?: { - name: string - slug: string - brand?: string - img?: { - url?: string - alt?: string - } - price?: PriceProps - }[] -} - -export interface PriceProps { - current: number - currentFormatted: string - regular?: number - regularFormatted?: string -} - -export const CommerceConnector = (props: GenericConnectorProps) => { +export const CommerceConnector = (props: CommerceConnectorProps) => { const { title, ctaLabel, diff --git a/composable-ui/src/server/api/routers/cms.ts b/composable-ui/src/server/api/routers/cms.ts index ff17183..7a552e1 100644 --- a/composable-ui/src/server/api/routers/cms.ts +++ b/composable-ui/src/server/api/routers/cms.ts @@ -1,12 +1,18 @@ -import { createTRPCRouter, publicProcedure } from '../trpc' import { getPage } from '@composable/cms-generic' -import { PageProps } from '@composable/types' +import { PageSchema } from '@composable/types' import { z } from 'zod' +import { createTRPCRouter, publicProcedure } from '../trpc' export const cmsRouter = createTRPCRouter({ getPage: publicProcedure .input(z.object({ slug: z.string() })) .query(async ({ input }) => { - return getPage({ pageSlug: input.slug }) as PageProps + try { + const page = await getPage({ pageSlug: input.slug }) + return PageSchema.parse(page) + } catch (err) { + console.log(err) + return null + } }), }) diff --git a/docs/docs/integrations/orchestration/_category_.json b/docs/docs/integrations/orchestration/_category_.json new file mode 100644 index 0000000..c749d8b --- /dev/null +++ b/docs/docs/integrations/orchestration/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Orchestration", + "position": 7, + "link": { + "type": "generated-index", + "description": "Orchestration" + } +} diff --git a/docs/docs/integrations/orchestration/conscia-visualizer.png b/docs/docs/integrations/orchestration/conscia-visualizer.png new file mode 100644 index 0000000..57ad724 Binary files /dev/null and b/docs/docs/integrations/orchestration/conscia-visualizer.png differ diff --git a/docs/docs/integrations/orchestration/conscia.md b/docs/docs/integrations/orchestration/conscia.md new file mode 100644 index 0000000..3920ad7 --- /dev/null +++ b/docs/docs/integrations/orchestration/conscia.md @@ -0,0 +1,46 @@ +# Conscia.ai Integration + +Conscia’s zero-code Digital Experience Orchestration (DXO) platform enables brands and organizations to fast-track the adoption of MACH and composable architecture into their existing tech stacks. For marketing, it offers centralized omnichannel control over the composable experience, with Personalization and A/B testing built in. For engineering teams, it offers zero code API and data orchestration, offloading the point to point integrations to the orchestration layer, simplifying the frontend code, and eliminating the need to build custom BFFs (Backend for Frontends). Conscia’s revolutionary approach embraces both legacy and modern backends, allowing it to act as the bridge between any backend and any frontend, and for this reason, justifiably claims the role of the ‘Brain’ of the Composable stack. + +This integration demonstrates Conscia's ability to orchestrate multiple data sources, stitch together data, transform data models, and enable switching out data sources by the business user without the front end being concerned. Using Salesforce Commere Cloud, Contentstack and Contentful as data sources, we have a created a Home Page Model template that recreates the Composable UI's homepage. + +## Integration architecture + +To extend the storefront to pull in content from Conscia's DXO, a package called `conscia` has been created. This package contains a `getPage` data fetching function that queries the Conscia API and then extracts the data required from the response to drive the content components on the Composable UI homepage. + +## Integrating Conscia.ai with Composable UI + +### Set Up + +1. Gain access to a Conscia sandbox. (These are currently only available to SIs. Please talk to your partner manager about getting access to a sandbox.) +1. Refer to the Conscia package [README](https://github.com/composable-com/composable-ui/blob/main/packages/conscia/README.md) to complete set up. + +### Exploring the Conscia Sandbox + +The sandbox is pre-populated with the set of orchestration Components required to reproduce the Composable UI homepage. These Components are connected to Contentstack, Contentful, and Salesforce Commerce Cloud sandboxes. + +Each Composable UI component data source is modeled with the same pattern of selector, mapper, and model. +- Selector Components pull data directly from a data source. +- Mapper Components transform the data as required. This could include remapping fields, stripping out extraneous data, and stitching together multiple data sources. +- Model Components provide the final response to the consumer. + +For example: +- `selector-contentstack-banner` connects to Contentstack's Banner content model. It also defines a number of Design Attributes that the business user is expected to supply values for via Experience Rules. +- `mapper-contentstack-banner` takes the selector Component's response and maps only the necessary fields to the schema expected by Composable UI. It combines these fields with the Design Attributes into a single schema response. +- `model-banner` provides a clean, agnostic final response, and includes logic to select either the Contenstack or Contentful banner mapper Component response based on the selection made in the `banner-picker` Component. + +![alt_text](conscia-visualizer.png "Conscia Home Page Model Template") + +### Try this + +1. Go into Experience Rules +2. Under Composable UI Storefront -> Home Page find the Hero Banner Source component +3. Click Edit on the `**Default**` rule +4. Change tabs to the Experience page +5. Under `Attributes` -> `CMS Banner Selection`, select Contentstack (or Contentful, depending on which is already selected, choose the other one) +6. Click Submit +7. Reload the homepage and see a different hero banner has loaded + +## Related Resources + +- [Conscia.ai DXO Documentation](https://docs.conscia.ai/platform-overview) \ No newline at end of file diff --git a/packages/conscia/.eslintrc.js b/packages/conscia/.eslintrc.js new file mode 100644 index 0000000..b56159e --- /dev/null +++ b/packages/conscia/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ['custom'], +} diff --git a/packages/conscia/README.md b/packages/conscia/README.md new file mode 100644 index 0000000..235752b --- /dev/null +++ b/packages/conscia/README.md @@ -0,0 +1,38 @@ +# Integrating Conscia Components into Composable-UI + +Read more about the Conscia.ai composable orchestration integration with Composable UI and what it accomplishes [here](https://docs.composable.com/docs/integrations/orchestration/conscia). + +# Installation and Setup + +1. **Navigating to the Project Directory:** Open your terminal and navigate to your local `composable-ui` project directory and then move to `composable-ui` subfolder. + ```bash + cd path/to/composable-ui/composable-ui + ``` +1. **Install the @composable/conscia package**: + ```bash + pnpm install @composable/conscia + ``` +1. **Set the required environment variables:** + + ```shell + NEXT_PUBLIC_CONSCIA_BASE_URL=https://...conscia.io/api + NEXT_PUBLIC_CONSCIA_TOKEN= + NEXT_PUBLIC_CONSCIA_CUSTOMER_CODE= + ``` +1. **Update your storefront to use the Conscia data fetching service:** + +The `cmsRouter`, defined in `composable-ui/src/server/api/routers/cms.ts`, provides your storefront with a data fetching function called `getPage`, which retrieves the data that is used to populate the content on your storefront. By default this function returns data retrieved from a local file. + +In order to use data from Conscia, the `cmsRouter` needs to use the `getPage` data fetching service defined in `@composable/conscia`, instead of from `@composable/cms-generic`. Change the code as follows: + + + ```javascript + // cms.ts - before changes + import { getPage } from '@composable/cms-generic' + +... + + // cms.ts - after changes + import { getPage } from '@composable/conscia' + ``` + diff --git a/packages/conscia/index.ts b/packages/conscia/index.ts new file mode 100644 index 0000000..6f39cd4 --- /dev/null +++ b/packages/conscia/index.ts @@ -0,0 +1 @@ +export * from './src' diff --git a/packages/conscia/package.json b/packages/conscia/package.json new file mode 100644 index 0000000..6bf25a4 --- /dev/null +++ b/packages/conscia/package.json @@ -0,0 +1,22 @@ +{ + "name": "@composable/conscia", + "version": "0.0.0", + "main": "./index.ts", + "types": "./index.ts", + "sideEffects": "false", + "scripts": { + "build": "echo \"Build script for @composable/conscia ...\"", + "lint": "eslint \"**/*.{js,ts,tsx}\" --max-warnings 0", + "ts": "tsc --noEmit --incremental" + }, + "dependencies": { + "@composable/types": "workspace:*", + "axios": "0.26.1" + }, + "devDependencies": { + "@types/node": "^18.6.3", + "eslint-config-custom": "workspace:*", + "tsconfig": "workspace:*", + "typescript": "^4.5.5" + } +} diff --git a/packages/conscia/src/index.ts b/packages/conscia/src/index.ts new file mode 100644 index 0000000..7b68f3c --- /dev/null +++ b/packages/conscia/src/index.ts @@ -0,0 +1,3 @@ +export * from './service' +export * from './utils' +export * from './types' diff --git a/packages/conscia/src/service/client.ts b/packages/conscia/src/service/client.ts new file mode 100644 index 0000000..0a162b3 --- /dev/null +++ b/packages/conscia/src/service/client.ts @@ -0,0 +1,9 @@ +import axios from 'axios' + +export const consciaClient = axios.create({ + baseURL: process.env.NEXT_PUBLIC_CONSCIA_BASE_URL, + headers: { + Authorization: `Bearer ${process.env.NEXT_PUBLIC_CONSCIA_TOKEN}`, + 'X-Customer-Code': process.env.NEXT_PUBLIC_CONSCIA_CUSTOMER_CODE ?? '', + }, +}) diff --git a/packages/conscia/src/service/index.ts b/packages/conscia/src/service/index.ts new file mode 100644 index 0000000..a34885e --- /dev/null +++ b/packages/conscia/src/service/index.ts @@ -0,0 +1,3 @@ +export * from './client' +export * from './page' +export * from './templates' diff --git a/packages/conscia/src/service/page.ts b/packages/conscia/src/service/page.ts new file mode 100644 index 0000000..7064ace --- /dev/null +++ b/packages/conscia/src/service/page.ts @@ -0,0 +1,18 @@ +import { PageProps } from '@composable/types' +import { ConsciaPageTemplateComponents } from '../types' +import { getTemplateData } from './templates' +import { transformPage } from '../utils' + +export const getPage = async ({ + pageSlug, +}: { + pageSlug: string +}): Promise => { + if (!pageSlug) { + return null + } + const consciaPage = await getTemplateData({ + templateCode: pageSlug, + }) + return consciaPage ? transformPage({ consciaPage, pageSlug }) : null +} diff --git a/packages/conscia/src/service/templates.ts b/packages/conscia/src/service/templates.ts new file mode 100644 index 0000000..5bec3f9 --- /dev/null +++ b/packages/conscia/src/service/templates.ts @@ -0,0 +1,18 @@ +import { ConsciaComponent, ConsciaTemplate } from '../types' +import { consciaClient } from './client' + +export const getTemplateData = async < + Components extends ConsciaComponent +>({ + templateCode, +}: { + templateCode: string +}) => { + const response = await consciaClient.post>( + '/experience/template/_query', + { + templateCode, + } + ) + return response.data +} diff --git a/packages/conscia/src/types.ts b/packages/conscia/src/types.ts new file mode 100644 index 0000000..f399e9b --- /dev/null +++ b/packages/conscia/src/types.ts @@ -0,0 +1,126 @@ +import { + BannerFullProps, + BannerSplitProps, + BannerTextOnlyProps, + CommerceConnectorProps, + CommerceProduct, + GridProps, + TextCardProps, +} from '@composable/types' + +export interface ConsciaTemplate> { + duration: number + components: Record + errors: any[] +} + +export interface ConsciaComponent { + '@extras': { + rule: { + metadata: any[] + attributes: Record + } + } + status: 'VALID' | 'INVALID' + response: ComponentData +} + +export type ConsciaHeroBanner = { + __typename: BannerSplitProps['__typename'] + containerMarginBottom?: number + containerMarginTop?: number + containerSize: BannerSplitProps['containerSize'] + textPosition: string + theme: string + id: BannerSplitProps['id'] + content: BannerSplitProps['content'] + title: BannerSplitProps['title'] + ctaAlphaHref: BannerSplitProps['ctaAlphaHref'] + ctaAlphaLabel: BannerSplitProps['ctaAlphaLabel'] + image: { + title: string + description: string + url: string + } +} + +export type ConsciaCTABanner = { + __typename: BannerFullProps['__typename'] + containerMarginBottom?: number + containerMarginTop?: number + containerSize: BannerFullProps['containerSize'] + overlayBackground: BannerFullProps['overlayBackground'] + textPosition: BannerFullProps['textPosition'] + theme: BannerFullProps['theme'] + id: BannerFullProps['id'] + content: BannerFullProps['content'] + title: BannerFullProps['title'] + ctaAlphaLabel: BannerFullProps['ctaAlphaLabel'] + ctaAlphaHref: BannerFullProps['ctaAlphaHref'] + image: { + url: string + title: string + } + linkHref1: BannerFullProps['linkHref1'] + linkLabel1: BannerFullProps['linkLabel1'] +} + +export type ConsciaFeatureCardsHeader = { + __typename: BannerTextOnlyProps['__typename'] + centered: BannerTextOnlyProps['centered'] + id: BannerTextOnlyProps['id'] + content: BannerTextOnlyProps['content'] + title: BannerTextOnlyProps['title'] + ctaAlphaLabel: BannerTextOnlyProps['ctaAlphaLabel'] + ctaAlphaHref: BannerTextOnlyProps['ctaAlphaHref'] +} + +export interface ConsciaFeaturedProducts { + __typename: CommerceConnectorProps['__typename'] + id: CommerceConnectorProps['id'] + title: CommerceConnectorProps['title'] + containerMarginBottom?: number + containerMarginTop?: number + containerSize: CommerceConnectorProps['containerSize'] + ctaMaxWidth: string + ctaMinWidth: string + ctaLabel: CommerceConnectorProps['ctaLabel'] + ctaHref: CommerceConnectorProps['ctaHref'] + products: (Omit & { image: CommerceProduct['img'] })[] +} + +export type ConsciaGrid = { + __typename: GridProps['__typename'] + id: GridProps['id'] + columns: GridProps['columns'] + containerMarginBottom?: number + containerMarginTop?: number + gridGap: GridProps['gridGap'] + containerSize: GridProps['containerSize'] + items: ConsciaTextCard[] +} + +export type ConsciaTextCard = { + __typename: TextCardProps['__typename'] + id: TextCardProps['id'] + title: TextCardProps['title'] + image: { + url: string + title: string + } + content: TextCardProps['content'] + ctaLabel: TextCardProps['ctaLabel'] + ctaHref: TextCardProps['ctaHref'] + theme: TextCardProps['theme'] + textAlign: TextCardProps['textAlign'] +} + +export type ConsciaPageTemplateComponents = ConsciaComponent< + | ConsciaHeroBanner + | ConsciaCTABanner + | ConsciaFeatureCardsHeader + | ConsciaFeaturedProducts + | ConsciaGrid +> +export type ConsciaPageTemplateResponse = + ConsciaTemplate diff --git a/packages/conscia/src/utils/images.ts b/packages/conscia/src/utils/images.ts new file mode 100644 index 0000000..e84f5e8 --- /dev/null +++ b/packages/conscia/src/utils/images.ts @@ -0,0 +1,3 @@ +export const parseImageUrl = (imageUrl: string) => { + return imageUrl.startsWith('//') ? `https:${imageUrl}` : imageUrl +} diff --git a/packages/conscia/src/utils/index.ts b/packages/conscia/src/utils/index.ts new file mode 100644 index 0000000..a9ea2d6 --- /dev/null +++ b/packages/conscia/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './images' +export * from './page' diff --git a/packages/conscia/src/utils/page.ts b/packages/conscia/src/utils/page.ts new file mode 100644 index 0000000..a75dea9 --- /dev/null +++ b/packages/conscia/src/utils/page.ts @@ -0,0 +1,185 @@ +import { + BannerFullProps, + BannerSplitProps, + BannerTextOnlyProps, + CommerceConnectorProps, + GridProps, + PageItem, + PageProps, + TextCardProps, +} from '@composable/types' +import { + ConsciaCTABanner, + ConsciaFeatureCardsHeader, + ConsciaFeaturedProducts, + ConsciaGrid, + ConsciaHeroBanner, + ConsciaPageTemplateComponents, + ConsciaPageTemplateResponse, + ConsciaTextCard, +} from '../types' +import { parseImageUrl } from './images' + +export const transformPage = ({ + consciaPage, + pageSlug, +}: { + consciaPage: ConsciaPageTemplateResponse + pageSlug: string +}): PageProps => { + return { + __typename: 'pageSlug', + id: pageSlug, + items: Object.values(consciaPage.components).map(transformPageComponent), + metaDescription: '', + metaKeywords: [], + metaTitle: '', + slug: pageSlug, + } +} + +const transformPageComponent = ( + component: ConsciaPageTemplateComponents +): PageItem => { + switch (component.response.__typename) { + case 'BannerSplit': + return transformHeroBanner(component.response) + case 'BannerFull': + return transformCTABanner(component.response) + case 'BannerTextOnly': + return transformFeatureCardsHeader(component.response) + case 'CommerceConnector': + return transformFeaturedProducts(component.response) + case 'Grid': + return transformGrid(component.response) + } + throw new Error( + `Unknown component ${component.response.__typename} found in template` + ) +} + +const transformHeroBanner = ( + heroBanner: ConsciaHeroBanner +): BannerSplitProps => { + const image = heroBanner.image + ? { + description: heroBanner.image.description, + title: heroBanner.image.title, + url: parseImageUrl(heroBanner.image.url), + } + : undefined + return { + __typename: heroBanner.__typename, + containerMarginBottom: heroBanner.containerMarginBottom?.toString(), + containerMarginTop: heroBanner.containerMarginTop?.toString(), + containerSize: heroBanner.containerSize, + content: heroBanner.content, + ctaAlphaHref: heroBanner.ctaAlphaHref, + ctaAlphaLabel: heroBanner.ctaAlphaLabel, + id: heroBanner.id, + imageDesktop: image, + imageMobile: image, + inverted: false, + isFullScreen: false, + isLazy: false, + title: heroBanner.title, + } +} + +const transformCTABanner = (ctaBanner: ConsciaCTABanner): BannerFullProps => { + const image = ctaBanner.image + ? { + title: ctaBanner.image.title ?? '', + url: parseImageUrl(ctaBanner.image.url), + description: '', + } + : undefined + return { + __typename: ctaBanner.__typename, + containerMarginBottom: ctaBanner.containerMarginBottom?.toString(), + containerMarginTop: ctaBanner.containerMarginTop?.toString(), + containerSize: ctaBanner.containerSize, + overlayBackground: ctaBanner.overlayBackground, + textPosition: ctaBanner.textPosition, + theme: ctaBanner.theme, + id: ctaBanner.id, + content: ctaBanner.content, + title: ctaBanner.title, + ctaAlphaLabel: ctaBanner.ctaAlphaLabel, + ctaAlphaHref: ctaBanner.ctaAlphaHref, + imageDesktop: image, + imageMobile: image, + linkHref1: ctaBanner.linkHref1, + linkLabel1: ctaBanner.linkLabel1, + } +} + +const transformFeatureCardsHeader = ( + featureCardsHeader: ConsciaFeatureCardsHeader +): BannerTextOnlyProps => { + return { + __typename: featureCardsHeader.__typename, + centered: featureCardsHeader.centered, + id: featureCardsHeader.id, + content: featureCardsHeader.content, + title: featureCardsHeader.title, + ctaAlphaLabel: featureCardsHeader.ctaAlphaLabel, + ctaAlphaHref: featureCardsHeader.ctaAlphaHref, + } +} + +const transformFeaturedProducts = ( + featuredProducts: ConsciaFeaturedProducts +): CommerceConnectorProps => { + return { + __typename: featuredProducts.__typename, + id: featuredProducts.id, + title: featuredProducts.title, + containerMarginBottom: featuredProducts.containerMarginBottom?.toString(), + containerMarginTop: featuredProducts.containerMarginTop?.toString(), + containerSize: featuredProducts.containerSize, + ctaLabel: featuredProducts.ctaLabel, + ctaHref: featuredProducts.ctaHref, + products: featuredProducts.products.map((product) => ({ + ...product, + img: product.image, + })), + productListType: 'id', + } +} + +const transformGrid = (grid: ConsciaGrid): GridProps => { + return { + __typename: grid.__typename, + id: grid.id, + columns: grid.columns, + containerMarginBottom: grid.containerMarginBottom?.toString(), + containerMarginTop: grid.containerMarginTop?.toString(), + gridGap: grid.gridGap, + containerSize: grid.containerSize, + items: grid.items.map((item) => { + switch (item.__typename) { + case 'TextCard': + return transformTextCard(item) + } + }), + } +} + +const transformTextCard = (textCard: ConsciaTextCard): TextCardProps => { + return { + __typename: textCard.__typename, + id: textCard.id, + title: textCard.title, + image: { + title: textCard.image.title, + url: parseImageUrl(textCard.image.url), + description: '', + }, + content: textCard.content, + ctaLabel: textCard.ctaLabel, + ctaHref: textCard.ctaHref, + theme: textCard.theme, + textAlign: textCard.textAlign, + } +} diff --git a/packages/conscia/tsconfig.json b/packages/conscia/tsconfig.json new file mode 100644 index 0000000..fd9038d --- /dev/null +++ b/packages/conscia/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "tsconfig/react-library.json", + "include": ["."], + "exclude": [".turbo", "dist", "tmp", "node_modules", "tsconfig.tsbuildinfo"] +} diff --git a/packages/types/package.json b/packages/types/package.json index fadaa60..cffad06 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -8,6 +8,9 @@ "lint": "eslint \"**/*.{js,ts,tsx}\" --max-warnings 0", "ts": "tsc --noEmit --incremental" }, + "dependencies": { + "zod": "^3.20.2" + }, "devDependencies": { "eslint-config-custom": "workspace:*", "tsconfig": "workspace:*", diff --git a/packages/types/src/cms/components/article-card.ts b/packages/types/src/cms/components/article-card.ts new file mode 100644 index 0000000..c3533e2 --- /dev/null +++ b/packages/types/src/cms/components/article-card.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' +import { ImageSchema } from './fields' + +export const ArticleCardSchema = z.object({ + __typename: z.literal('ArticleCard'), + containerMarginBottom: z.string().optional(), + containerMarginTop: z.string().optional(), + containerSize: z.string().optional(), + content: z.string(), + eyebrow: z.string().optional(), + href: z.string(), + id: z.string(), + image: ImageSchema, + textAlign: z.string(), + title: z.string(), +}) + +export type ArticleCardProps = z.infer diff --git a/packages/types/src/cms/components/banner-full.ts b/packages/types/src/cms/components/banner-full.ts new file mode 100644 index 0000000..0e27224 --- /dev/null +++ b/packages/types/src/cms/components/banner-full.ts @@ -0,0 +1,36 @@ +import { z } from 'zod' +import { ImageSchema } from './fields' + +export const BannerFullSchema = z.object({ + __typename: z.literal('BannerFull').optional(), + containerMarginBottom: z.string().nullish(), + containerMarginTop: z.string().nullish(), + containerSize: z.string().nullish(), + content: z.string(), + ctaAlphaHref: z.string().nullish(), + ctaAlphaLabel: z.string(), + ctaBetaHref: z.string().nullish(), + ctaBetaLabel: z.string().nullish(), + eyebrow: z.string().nullish(), + id: z.string(), + imageDesktop: ImageSchema.nullish(), + imageMobile: ImageSchema.nullish(), + linkHref1: z.string().nullish(), + linkHref2: z.string().nullish(), + linkHref3: z.string().nullish(), + linkHref4: z.string().nullish(), + linkHref5: z.string().nullish(), + linkHref6: z.string().nullish(), + linkLabel1: z.string().nullish(), + linkLabel2: z.string().nullish(), + linkLabel3: z.string().nullish(), + linkLabel4: z.string().nullish(), + linkLabel5: z.string().nullish(), + linkLabel6: z.string().nullish(), + overlayBackground: z.string().nullish(), + textPosition: z.string(), + theme: z.string(), + title: z.string(), +}) + +export type BannerFullProps = z.infer diff --git a/packages/types/src/cms/components/banner-split.ts b/packages/types/src/cms/components/banner-split.ts new file mode 100644 index 0000000..60cb698 --- /dev/null +++ b/packages/types/src/cms/components/banner-split.ts @@ -0,0 +1,22 @@ +import { z } from 'zod' +import { ImageSchema } from './fields' + +export const BannerSplitSchema = z.object({ + __typename: z.literal('BannerSplit').optional(), + containerMarginBottom: z.string().nullish(), + containerMarginTop: z.string().nullish(), + containerSize: z.string().nullish(), + content: z.string(), + ctaAlphaHref: z.string().nullish(), + ctaAlphaLabel: z.string(), + eyebrow: z.string().nullish(), + id: z.string(), + imageDesktop: ImageSchema.nullish(), + imageMobile: ImageSchema.nullish(), + inverted: z.boolean(), + isFullScreen: z.boolean(), + isLazy: z.boolean(), + title: z.string(), +}) + +export type BannerSplitProps = z.infer diff --git a/packages/types/src/cms/components/banner-text-only.ts b/packages/types/src/cms/components/banner-text-only.ts new file mode 100644 index 0000000..8963875 --- /dev/null +++ b/packages/types/src/cms/components/banner-text-only.ts @@ -0,0 +1,18 @@ +import { z } from 'zod' + +export const BannerTextOnlySchema = z.object({ + __typename: z.literal('BannerTextOnly').optional(), + centered: z.boolean().optional(), + containerMarginBottom: z.string().nullish(), + containerMarginTop: z.string().nullish(), + containerSize: z.string().nullish(), + content: z.string(), + ctaAlphaHref: z.string().nullish(), + ctaAlphaLabel: z.string(), + eyebrow: z.string().nullish(), + id: z.string(), + minHeight: z.string().nullish(), + title: z.string(), +}) + +export type BannerTextOnlyProps = z.infer diff --git a/packages/types/src/cms/components/commerce-connector.ts b/packages/types/src/cms/components/commerce-connector.ts new file mode 100644 index 0000000..cc3eb56 --- /dev/null +++ b/packages/types/src/cms/components/commerce-connector.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' + +const PriceSchema = z.object({ + current: z.number(), + currentFormatted: z.string(), + regular: z.number().optional(), + regularFormatted: z.string().optional(), +}) + +export type PriceProps = z.infer + +const CommerceProductsSchema = z.object({ + brand: z.string().nullish(), + id: z.string(), + img: z + .object({ + url: z.string().optional(), + alt: z.string().optional(), + }) + .optional(), + name: z.string(), + price: PriceSchema.optional(), + slug: z.string(), +}) + +export type CommerceProduct = z.infer + +export const CommerceConnectorSchema = z.object({ + __typename: z.literal('CommerceConnector'), + containerMarginBottom: z.string().optional(), + containerMarginTop: z.string().optional(), + containerSize: z.string().optional(), + ctaHref: z.string().nullish(), + ctaLabel: z.string().nullish(), + ctaHeight: z.string().nullish(), + ctaMaxWidth: z.string().nullish(), + ctaMinWidth: z.string().nullish(), + id: z.string(), + productListType: z.union([z.literal('id'), z.literal('sku')]).optional(), + products: z.array(CommerceProductsSchema), + title: z.string().nullish(), +}) + +export type CommerceConnectorProps = z.infer diff --git a/packages/types/src/cms/components/cover-card.ts b/packages/types/src/cms/components/cover-card.ts new file mode 100644 index 0000000..43f0932 --- /dev/null +++ b/packages/types/src/cms/components/cover-card.ts @@ -0,0 +1,19 @@ +import { z } from 'zod' +import { ImageSchema } from './fields' + +export const CoverCardSchema = z.object({ + __typename: z.literal('CoverCard'), + containerMarginBottom: z.string().nullish(), + containerMarginTop: z.string().nullish(), + containerSize: z.string().nullish(), + content: z.string(), + eyebrow: z.string().nullish(), + href: z.string(), + id: z.string(), + image: ImageSchema, + textAlign: z.string(), + theme: z.string(), + title: z.string(), +}) + +export type CoverCardProps = z.infer diff --git a/packages/types/src/cms/components/fields.ts b/packages/types/src/cms/components/fields.ts new file mode 100644 index 0000000..67f865f --- /dev/null +++ b/packages/types/src/cms/components/fields.ts @@ -0,0 +1,10 @@ +import { z } from 'zod' + +export const ImageSchema = z.object({ + description: z.string(), + title: z.string(), + url: z.string(), + minHeight: z.string().optional(), +}) + +export type ImageProps = z.infer diff --git a/packages/types/src/cms/components/grid.ts b/packages/types/src/cms/components/grid.ts new file mode 100644 index 0000000..c3c557a --- /dev/null +++ b/packages/types/src/cms/components/grid.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' +import { ArticleCardSchema } from './article-card' +import { CoverCardSchema } from './cover-card' +import { TextCardSchema } from './text-card' + +export const GridItemSchema = z.union([ + ArticleCardSchema, + CoverCardSchema, + TextCardSchema, +]) + +export type GridItem = z.infer + +export const GridSchema = z.object({ + __typename: z.literal('Grid'), + columns: z.number(), + containerMarginBottom: z.string().or(z.number()).nullable().optional(), + containerMarginTop: z.string().or(z.number()).nullable().optional(), + containerSize: z.string().nullable().optional(), + gridGap: z.number().optional(), + id: z.string(), + items: z.array(GridItemSchema), +}) + +export type GridProps = z.infer diff --git a/packages/types/src/cms/components/index.ts b/packages/types/src/cms/components/index.ts new file mode 100644 index 0000000..1b5d592 --- /dev/null +++ b/packages/types/src/cms/components/index.ts @@ -0,0 +1,9 @@ +export * from './article-card' +export * from './banner-full' +export * from './banner-split' +export * from './banner-text-only' +export * from './commerce-connector' +export * from './cover-card' +export * from './fields' +export * from './grid' +export * from './text-card' diff --git a/packages/types/src/cms/components/text-card.ts b/packages/types/src/cms/components/text-card.ts new file mode 100644 index 0000000..4a8d197 --- /dev/null +++ b/packages/types/src/cms/components/text-card.ts @@ -0,0 +1,16 @@ +import { z } from 'zod' +import { ImageSchema } from './fields' + +export const TextCardSchema = z.object({ + __typename: z.literal('TextCard'), + content: z.string(), + ctaHref: z.string().nullish(), + ctaLabel: z.string().nullish(), + id: z.string(), + image: ImageSchema, + textAlign: z.string(), + theme: z.string(), + title: z.string(), +}) + +export type TextCardProps = z.infer diff --git a/packages/types/src/cms/index.ts b/packages/types/src/cms/index.ts index 71c50f4..c1dd753 100644 --- a/packages/types/src/cms/index.ts +++ b/packages/types/src/cms/index.ts @@ -1,174 +1,2 @@ -export interface ImageProps { - description: string - title: string - url: string - minHeight?: string -} - -export interface BannerSplitProps { - __typename?: 'BannerSplit' - containerMarginBottom?: string | null - containerMarginTop?: string | null - containerSize?: string | null - content: string - ctaAlphaHref?: string | null - ctaAlphaLabel: string - eyebrow?: string | null - id: string - imageDesktop?: ImageProps | null - imageMobile?: ImageProps | null - inverted: boolean - isFullScreen: boolean - isLazy: boolean - title: string -} - -export interface BannerFullProps { - __typename?: 'BannerFull' - containerMarginBottom?: string | null - containerMarginTop?: string | null - containerSize?: string | null - content: string - ctaAlphaHref?: string | null - ctaAlphaLabel: string - ctaBetaHref?: string | null - ctaBetaLabel?: string | null - eyebrow?: string | null - id: string - imageDesktop: ImageProps - imageMobile: ImageProps - linkHref1?: string | null - linkHref2?: string | null - linkHref3?: string | null - linkHref4?: string | null - linkHref5?: string | null - linkHref6?: string | null - linkLabel1?: string | null - linkLabel2?: string | null - linkLabel3?: string | null - linkLabel4?: string | null - linkLabel5?: string | null - linkLabel6?: string | null - overlayBackground?: string | null - textPosition: string - theme: string - title: string -} - -export interface BannerTextOnlyProps { - __typename?: 'BannerTextOnly' - centered: boolean - containerMarginBottom?: string | null - containerMarginTop?: string | null - containerSize?: string | null - content: string - ctaAlphaHref?: string | null - ctaAlphaLabel: string - eyebrow?: string | null - id: string - minHeight?: string | null - title: string -} - -export interface CoverCardProps { - __typename: 'CoverCard' - containerMarginBottom?: string | null - containerMarginTop?: string | null - containerSize?: string | null - content: string - eyebrow?: string | null - href: string - id: string - image: ImageProps - textAlign: string - theme: string - title: string -} - -export interface TextCardProps { - __typename: 'TextCard' - content: string - ctaHref?: string | null - ctaLabel: string - id: string - image: ImageProps - textAlign: string - theme: string - title: string -} - -export interface ArticleCardProps { - __typename: 'ArticleCard' - containerMarginBottom?: string - containerMarginTop?: string - containerSize?: string - content: string - eyebrow?: string - href: string - id: string - image: ImageProps - textAlign: string - title: string -} - -export interface PriceProps { - current: number - currentFormatted: string - regular?: number - regularFormatted?: string -} - -interface CommerceProducts { - brand?: string - id: string - img?: { - url?: string - alt?: string - } - name: string - price?: PriceProps - slug: string -} - -interface CommerceConnectorProps { - __typename: 'CommerceConnector' - containerMarginBottom?: string - containerMarginTop?: string - containerSize?: string - ctaHref: string - ctaLabel: string - id: string - productListType: 'id' | 'sku' - products: CommerceProducts[] - title: string -} - -export type PageItem = - | BannerFullProps - | BannerSplitProps - | BannerTextOnlyProps - | CommerceConnectorProps - | GridProps - -export interface PageProps { - __typename: string - id: string - items: PageItem[] - metaDescription: string - metaKeywords: string[] - metaTitle: string - slug: string -} - -type GridItem = ArticleCardProps | CoverCardProps | TextCardProps - -export interface GridProps { - __typename: 'Grid' - columns: number - containerMarginBottom?: string - containerMarginTop?: string - containerSize?: string - gridGap?: string - id: string - items: GridItem[] -} +export * from './components' +export * from './page' diff --git a/packages/types/src/cms/page.ts b/packages/types/src/cms/page.ts new file mode 100644 index 0000000..4346ea3 --- /dev/null +++ b/packages/types/src/cms/page.ts @@ -0,0 +1,30 @@ +import { z } from 'zod' +import { + BannerFullSchema, + BannerSplitSchema, + BannerTextOnlySchema, + CommerceConnectorSchema, + GridSchema, +} from './components' + +export const PageItemSchema = z.union([ + BannerFullSchema, + BannerSplitSchema, + BannerTextOnlySchema, + CommerceConnectorSchema, + GridSchema, +]) + +export type PageItem = z.infer + +export const PageSchema = z.object({ + __typename: z.string(), + id: z.string(), + items: z.array(PageItemSchema), + metaDescription: z.string(), + metaKeywords: z.array(z.string()), + metaTitle: z.string(), + slug: z.string(), +}) + +export type PageProps = z.infer diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24f87e9..dca57dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -225,6 +225,28 @@ importers: specifier: ^4.5.5 version: 4.9.5 + packages/conscia: + dependencies: + '@composable/types': + specifier: workspace:* + version: link:../types + axios: + specifier: 0.26.1 + version: 0.26.1 + devDependencies: + '@types/node': + specifier: ^18.6.3 + version: 18.15.11 + eslint-config-custom: + specifier: workspace:* + version: link:../eslint-config-custom + tsconfig: + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: ^4.5.5 + version: 4.9.5 + packages/eslint-config-custom: dependencies: eslint: @@ -317,6 +339,10 @@ importers: packages/tsconfig: {} packages/types: + dependencies: + zod: + specifier: ^3.20.2 + version: 3.21.4 devDependencies: eslint-config-custom: specifier: workspace:* @@ -7407,7 +7433,7 @@ packages: /axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.5 transitivePeerDependencies: - debug dev: false @@ -7415,7 +7441,7 @@ packages: /axios@0.27.2: resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} dependencies: - follow-redirects: 1.15.2 + follow-redirects: 1.15.5 form-data: 4.0.0 transitivePeerDependencies: - debug @@ -10206,6 +10232,7 @@ packages: peerDependenciesMeta: debug: optional: true + dev: true /follow-redirects@1.15.5: resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} @@ -10215,7 +10242,6 @@ packages: peerDependenciesMeta: debug: optional: true - dev: false /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}