Skip to content

Commit

Permalink
feat: Integrate CMS with PDP (#283)
Browse files Browse the repository at this point in the history
  • Loading branch information
tlgimenes authored Oct 25, 2022
1 parent 8b4add4 commit e3c3ec8
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 68 deletions.
5 changes: 5 additions & 0 deletions cms/content-types.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@
]
}
]
},
{
"id": "pdp",
"name": "Product Page",
"configurationSchemaSets": []
}
]
47 changes: 47 additions & 0 deletions cms/sections.json
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,34 @@
}
}
},
{
"name": "CrossSellingShelf",
"schema": {
"title": "Cross Selling Shelf",
"description": "Add cross selling product data to your users",
"type": "object",
"required": ["title", "items", "kind"],
"properties": {
"title": {
"type": "string",
"title": "Title"
},
"items": {
"type": "integer",
"title": "First",
"default": 5,
"description": "Number of items to display"
},
"kind": {
"title": "Kind",
"description": "Change cross selling types",
"default": "buy",
"enum": ["buy", "view"],
"enumNames": ["Who bought also bought", "Who saw also saw"]
}
}
}
},
{
"name": "ProductTiles",
"schema": {
Expand Down Expand Up @@ -346,5 +374,24 @@
}
}
}
},
{
"name": "BannerNewsletter",
"schema": {
"title": "Banner Newsletter",
"description": "Add newsletter with a banner",
"type": "object",
"required": [],
"properties": {}
}
},
{
"name": "ProductDetails",
"schema": {
"title": "Product Details",
"description": "Display product gallery with buy button and shipping",
"type": "object",
"properties": {}
}
}
]
32 changes: 7 additions & 25 deletions src/components/cms/RenderPageSections.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,18 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType } from 'react'

import BannerText from 'src/components/sections/BannerText'
import Hero from 'src/components/sections/Hero'
import IncentivesHeader from 'src/components/sections/Incentives/IncentivesHeader'
import ProductShelf from 'src/components/sections/ProductShelf'
import ProductTiles from 'src/components/sections/ProductTiles'
import Newsletter from 'src/components/sections/Newsletter'

import SectionBoundary from './SectionBoundary'

/**
* Sections: Components imported from '../components/sections' only.
* Do not import or render components from any other folder in here.
*/
const COMPONENTS: Record<string, ComponentType<any>> = {
Hero,
BannerText,
IncentivesHeader,
ProductShelf,
ProductTiles,
Newsletter,
}

interface Props {
sections?: Array<{ name: string; data: any }>
components: Record<string, ComponentType<any>>
sections: Array<{ name: string; data: any }>
context?: unknown
}

const RenderPageSections = ({ sections }: Props) => (
const RenderPageSections = ({ sections = [], context, components }: Props) => (
<>
{sections?.map(({ name, data }, index) => {
const Component = COMPONENTS[name]
{sections.map(({ name, data }, index) => {
const Component = components[name]

if (!Component) {
console.info(
Expand All @@ -42,7 +24,7 @@ const RenderPageSections = ({ sections }: Props) => (

return (
<SectionBoundary key={`cms-section-${index}`} name={name}>
<Component {...data} />
<Component {...data} context={context} />
</SectionBoundary>
)
})}
Expand Down
25 changes: 25 additions & 0 deletions src/components/sections/CrossSellingShelf/CrossSellingShelf.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useMemo } from 'react'

import type { ProductDetailsFragment_ProductFragment } from '@generated/graphql'

import ProductShelf from '../ProductShelf'

interface Props {
items: number
title: string
context: ProductDetailsFragment_ProductFragment
kind: 'buy' | 'view'
}

const CrossSellingShelf = ({ items, title, context, kind }: Props) => {
const selectedFacets = useMemo(
() => [{ key: kind, value: context.isVariantOf.productGroupID }],
[kind, context.isVariantOf.productGroupID]
)

return (
<ProductShelf first={items} title={title} selectedFacets={selectedFacets} />
)
}

export default CrossSellingShelf
1 change: 1 addition & 0 deletions src/components/sections/CrossSellingShelf/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CrossSellingShelf'
4 changes: 2 additions & 2 deletions src/components/sections/ProductDetails/ProductDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import Section from '../Section'
import ProductDetailsContent from '../ProducDetailsContent'

interface Props {
product: ProductDetailsFragment_ProductFragment
context: ProductDetailsFragment_ProductFragment
}

function ProductDetails({ product: staleProduct }: Props) {
function ProductDetails({ context: staleProduct }: Props) {
const { currency } = useSession()
const [addQuantity, setAddQuantity] = useState(1)

Expand Down
80 changes: 44 additions & 36 deletions src/pages/[slug]/p.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,38 @@ import { isNotFoundError } from '@faststore/api'
import { gql } from '@faststore/graphql-utils'
import { BreadcrumbJsonLd, NextSeo, ProductJsonLd } from 'next-seo'
import type { GetStaticPaths, GetStaticProps } from 'next'
import type { ComponentType } from 'react'
import type { Locator } from '@vtex/client-cms'

import ProductDetails from 'src/components/sections/ProductDetails'
import ProductShelf from 'src/components/sections/ProductShelf'
import RenderPageSections from 'src/components/cms/RenderPageSections'
import BannerNewsletter from 'src/components/sections/BannerNewsletter/BannerNewsletter'
import { ITEMS_PER_SECTION } from 'src/constants'
import CrossSellingShelf from 'src/components/sections/CrossSellingShelf'
import ProductDetails from 'src/components/sections/ProductDetails'
import { useSession } from 'src/sdk/session'
import { mark } from 'src/sdk/tests/mark'
import { execute } from 'src/server'
import { getPage } from 'src/server/cms'
import type { PDPContentType } from 'src/server/cms'
import type {
ServerProductPageQueryQuery,
ServerProductPageQueryQueryVariables,
} from '@generated/graphql'

import storeConfig from '../../../store.config'

type Props = ServerProductPageQueryQuery
/**
* Sections: Components imported from '../components/sections' only.
* Do not import or render components from any other folder in here.
*/
const COMPONENTS: Record<string, ComponentType<any>> = {
ProductDetails,
BannerNewsletter,
CrossSellingShelf,
}

type Props = ServerProductPageQueryQuery & PDPContentType

function Page({ product }: Props) {
function Page({ product, sections }: Props) {
const { currency } = useSession()
const { seo } = product
const title = seo.title || storeConfig.seo.title
Expand Down Expand Up @@ -84,27 +98,11 @@ function Page({ product }: Props) {
If needed, wrap your component in a <Section /> component
(not the HTML tag) before rendering it here.
*/}

<ProductDetails product={product} />

<ProductShelf
first={ITEMS_PER_SECTION}
selectedFacets={[
{ key: 'buy', value: product.isVariantOf.productGroupID },
]}
title="People also bought"
withDivisor
/>

<ProductShelf
first={ITEMS_PER_SECTION}
selectedFacets={[
{ key: 'view', value: product.isVariantOf.productGroupID },
]}
title="People also view"
<RenderPageSections
context={product}
sections={sections}
components={COMPONENTS}
/>

<BannerNewsletter />
</>
)
}
Expand Down Expand Up @@ -169,16 +167,23 @@ const query = gql`
`

export const getStaticProps: GetStaticProps<
ServerProductPageQueryQuery,
{ slug: string }
> = async ({ params }) => {
const { data, errors = [] } = await execute<
ServerProductPageQueryQueryVariables,
ServerProductPageQueryQuery
>({
variables: { slug: params?.slug ?? '' },
operationName: query,
})
Props,
{ slug: string },
Locator
> = async ({ params, previewData }) => {
const slug = params?.slug ?? ''
const [cmsPage, searchResult] = await Promise.all([
getPage<PDPContentType>({
...(previewData?.contentType === 'pdp' ? previewData : null),
contentType: 'pdp',
}),
execute<ServerProductPageQueryQueryVariables, ServerProductPageQueryQuery>({
variables: { slug },
operationName: query,
}),
])

const { data, errors = [] } = searchResult

const notFound = errors.find(isNotFoundError)

Expand All @@ -193,7 +198,10 @@ export const getStaticProps: GetStaticProps<
}

return {
props: data,
props: {
...data,
...cmsPage,
},
}
}

Expand Down
30 changes: 26 additions & 4 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,34 @@
import { NextSeo, SiteLinksSearchBoxJsonLd } from 'next-seo'
import type { ComponentType } from 'react'
import type { GetStaticProps } from 'next'
import type { Locator } from '@vtex/client-cms'

import RenderPageSections from 'src/components/cms/RenderPageSections'
import type { PageContentType } from 'src/server/cms'
import { getPage } from 'src/server/cms'
import BannerText from 'src/components/sections/BannerText'
import Hero from 'src/components/sections/Hero'
import IncentivesHeader from 'src/components/sections/Incentives/IncentivesHeader'
import Newsletter from 'src/components/sections/Newsletter'
import ProductShelf from 'src/components/sections/ProductShelf'
import ProductTiles from 'src/components/sections/ProductTiles'
import { mark } from 'src/sdk/tests/mark'
import { getPage } from 'src/server/cms'
import type { PageContentType } from 'src/server/cms'

import storeConfig from '../../store.config'

/**
* Sections: Components imported from '../components/sections' only.
* Do not import or render components from any other folder in here.
*/
const COMPONENTS: Record<string, ComponentType<any>> = {
Hero,
BannerText,
IncentivesHeader,
ProductShelf,
ProductTiles,
Newsletter,
}

type Props = PageContentType

function Page({ sections, settings }: Props) {
Expand Down Expand Up @@ -48,7 +68,7 @@ function Page({ sections, settings }: Props) {
If needed, wrap your component in a <Section /> component
(not the HTML tag) before rendering it here.
*/}
<RenderPageSections sections={sections} />
<RenderPageSections sections={sections} components={COMPONENTS} />
</>
)
}
Expand All @@ -59,7 +79,9 @@ export const getStaticProps: GetStaticProps<
Locator
> = async (context) => {
const page = await getPage<PageContentType>({
...(context.previewData ?? { filters: { 'settings.seo.slug': '/' } }),
...(context.previewData?.contentType === 'page'
? context.previewData
: { filters: { 'settings.seo.slug': '/' } }),
contentType: 'page',
})

Expand Down
4 changes: 3 additions & 1 deletion src/server/cms.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ClientCMS from '@vtex/client-cms'
import type { Locator, ContentData } from '@vtex/client-cms'
import type { ContentData, Locator } from '@vtex/client-cms'

import config from '../../store.config'

Expand Down Expand Up @@ -54,6 +54,8 @@ export const getPage = async <T extends ContentData>(options: Options) => {
return pages[0] as T
}

export type PDPContentType = ContentData

export type PageContentType = ContentData & {
settings: {
seo: {
Expand Down

1 comment on commit e3c3ec8

@vercel
Copy link

@vercel vercel bot commented on e3c3ec8 Oct 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.