diff --git a/cms/content-types.json b/cms/content-types.json index 1bfad828..42ef0c7c 100644 --- a/cms/content-types.json +++ b/cms/content-types.json @@ -48,5 +48,10 @@ ] } ] + }, + { + "id": "pdp", + "name": "Product Page", + "configurationSchemaSets": [] } ] diff --git a/cms/sections.json b/cms/sections.json index 8c672e55..489e67a5 100644 --- a/cms/sections.json +++ b/cms/sections.json @@ -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": { @@ -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": {} + } } ] diff --git a/src/components/cms/RenderPageSections.tsx b/src/components/cms/RenderPageSections.tsx index e9a72b63..d21d5cf6 100644 --- a/src/components/cms/RenderPageSections.tsx +++ b/src/components/cms/RenderPageSections.tsx @@ -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> = { - Hero, - BannerText, - IncentivesHeader, - ProductShelf, - ProductTiles, - Newsletter, -} - interface Props { - sections?: Array<{ name: string; data: any }> + components: Record> + 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( @@ -42,7 +24,7 @@ const RenderPageSections = ({ sections }: Props) => ( return ( - + ) })} diff --git a/src/components/sections/CrossSellingShelf/CrossSellingShelf.tsx b/src/components/sections/CrossSellingShelf/CrossSellingShelf.tsx new file mode 100644 index 00000000..f5834e21 --- /dev/null +++ b/src/components/sections/CrossSellingShelf/CrossSellingShelf.tsx @@ -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 ( + + ) +} + +export default CrossSellingShelf diff --git a/src/components/sections/CrossSellingShelf/index.tsx b/src/components/sections/CrossSellingShelf/index.tsx new file mode 100644 index 00000000..adfb9091 --- /dev/null +++ b/src/components/sections/CrossSellingShelf/index.tsx @@ -0,0 +1 @@ +export { default } from './CrossSellingShelf' diff --git a/src/components/sections/ProductDetails/ProductDetails.tsx b/src/components/sections/ProductDetails/ProductDetails.tsx index 1cf5cb4f..5859715b 100644 --- a/src/components/sections/ProductDetails/ProductDetails.tsx +++ b/src/components/sections/ProductDetails/ProductDetails.tsx @@ -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) diff --git a/src/pages/[slug]/p.tsx b/src/pages/[slug]/p.tsx index 22c5a8eb..1e5d82fa 100644 --- a/src/pages/[slug]/p.tsx +++ b/src/pages/[slug]/p.tsx @@ -2,14 +2,18 @@ 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, @@ -17,9 +21,19 @@ import type { 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> = { + 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 @@ -84,27 +98,11 @@ function Page({ product }: Props) { If needed, wrap your component in a
component (not the HTML tag) before rendering it here. */} - - - - - - - - ) } @@ -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({ + ...(previewData?.contentType === 'pdp' ? previewData : null), + contentType: 'pdp', + }), + execute({ + variables: { slug }, + operationName: query, + }), + ]) + + const { data, errors = [] } = searchResult const notFound = errors.find(isNotFoundError) @@ -193,7 +198,10 @@ export const getStaticProps: GetStaticProps< } return { - props: data, + props: { + ...data, + ...cmsPage, + }, } } diff --git a/src/pages/index.tsx b/src/pages/index.tsx index f9c88a07..26924e71 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -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> = { + Hero, + BannerText, + IncentivesHeader, + ProductShelf, + ProductTiles, + Newsletter, +} + type Props = PageContentType function Page({ sections, settings }: Props) { @@ -48,7 +68,7 @@ function Page({ sections, settings }: Props) { If needed, wrap your component in a
component (not the HTML tag) before rendering it here. */} - + ) } @@ -59,7 +79,9 @@ export const getStaticProps: GetStaticProps< Locator > = async (context) => { const page = await getPage({ - ...(context.previewData ?? { filters: { 'settings.seo.slug': '/' } }), + ...(context.previewData?.contentType === 'page' + ? context.previewData + : { filters: { 'settings.seo.slug': '/' } }), contentType: 'page', }) diff --git a/src/server/cms.ts b/src/server/cms.ts index 6a6c02bd..1cd06d30 100644 --- a/src/server/cms.ts +++ b/src/server/cms.ts @@ -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' @@ -54,6 +54,8 @@ export const getPage = async (options: Options) => { return pages[0] as T } +export type PDPContentType = ContentData + export type PageContentType = ContentData & { settings: { seo: {