@faststore/core 2.1.17 → 2.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/BUILD_ID +1 -0
- package/.next/build-manifest.json +129 -0
- package/.next/cache/.tsbuildinfo +1 -0
- package/.next/cache/config.json +7 -0
- package/.next/cache/eslint/.cache_1gneedd +1 -0
- package/.next/cache/next-server.js.nft.json +1 -0
- package/.next/cache/webpack/client-production/0.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +44 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/chunks/123.js +58 -0
- package/.next/server/chunks/143.js +106 -0
- package/.next/server/chunks/144.js +1338 -0
- package/.next/server/chunks/183.js +90 -0
- package/.next/server/chunks/184.js +61 -0
- package/.next/server/chunks/186.js +136 -0
- package/.next/server/chunks/247.js +61 -0
- package/.next/server/chunks/253.js +535 -0
- package/.next/server/chunks/269.js +517 -0
- package/.next/server/chunks/287.js +58 -0
- package/.next/server/chunks/289.js +242 -0
- package/.next/server/chunks/312.js +697 -0
- package/.next/server/chunks/350.js +143 -0
- package/.next/server/chunks/487.js +9142 -0
- package/.next/server/chunks/502.js +626 -0
- package/.next/server/chunks/576.js +90 -0
- package/.next/server/chunks/597.js +211 -0
- package/.next/server/chunks/650.js +9142 -0
- package/.next/server/chunks/676.js +32 -0
- package/.next/server/chunks/721.js +679 -0
- package/.next/server/chunks/74.js +4100 -0
- package/.next/server/chunks/825.js +4039 -0
- package/.next/server/chunks/854.js +72 -0
- package/.next/server/chunks/859.js +959 -0
- package/.next/server/chunks/886.js +120 -0
- package/.next/server/chunks/907.js +1956 -0
- package/.next/server/chunks/98.js +124 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/pages/404.js +386 -0
- package/.next/server/pages/404.js.nft.json +1 -0
- package/.next/server/pages/500.js +388 -0
- package/.next/server/pages/500.js.nft.json +1 -0
- package/.next/server/pages/[...slug].js +1005 -0
- package/.next/server/pages/[...slug].js.nft.json +1 -0
- package/.next/server/pages/[slug]/p.js +2390 -0
- package/.next/server/pages/[slug]/p.js.nft.json +1 -0
- package/.next/server/pages/_app.js +281 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +340 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +164 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages/account.js +363 -0
- package/.next/server/pages/account.js.nft.json +1 -0
- package/.next/server/pages/api/graphql.js +365 -0
- package/.next/server/pages/api/graphql.js.nft.json +1 -0
- package/.next/server/pages/api/preview.js +148 -0
- package/.next/server/pages/api/preview.js.nft.json +1 -0
- package/.next/server/pages/checkout.js +363 -0
- package/.next/server/pages/checkout.js.nft.json +1 -0
- package/.next/server/pages/en-US/404.html +81 -0
- package/.next/server/pages/en-US/404.json +1 -0
- package/.next/server/pages/en-US/500.html +81 -0
- package/.next/server/pages/en-US/500.json +1 -0
- package/.next/server/pages/en-US/account.html +81 -0
- package/.next/server/pages/en-US/account.json +1 -0
- package/.next/server/pages/en-US/checkout.html +81 -0
- package/.next/server/pages/en-US/checkout.json +1 -0
- package/.next/server/pages/en-US/login.html +81 -0
- package/.next/server/pages/en-US/login.json +1 -0
- package/.next/server/pages/en-US/s.html +81 -0
- package/.next/server/pages/en-US/s.json +1 -0
- package/.next/server/pages/en-US.html +81 -0
- package/.next/server/pages/en-US.json +1 -0
- package/.next/server/pages/index.js +439 -0
- package/.next/server/pages/index.js.nft.json +1 -0
- package/.next/server/pages/login.js +368 -0
- package/.next/server/pages/login.js.nft.json +1 -0
- package/.next/server/pages/s.js +466 -0
- package/.next/server/pages/s.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +16 -0
- package/.next/server/webpack-api-runtime.js +229 -0
- package/.next/server/webpack-runtime.js +229 -0
- package/.next/static/HWyxnnh37qprVz28Rroho/_buildManifest.js +1 -0
- package/.next/static/HWyxnnh37qprVz28Rroho/_ssgManifest.js +1 -0
- package/.next/static/chunks/130-15325805e3c8d5f4.js +1 -0
- package/.next/static/chunks/143.dd8a556e6957baa1.js +1 -0
- package/.next/static/chunks/495.0ecd099878b2a36d.js +1 -0
- package/.next/static/chunks/502.b14533723651e5a1.js +1 -0
- package/.next/static/chunks/548-ab84e9e8b49413ab.js +1 -0
- package/.next/static/chunks/597.f8d0595b113c70af.js +1 -0
- package/.next/static/chunks/64.7ea3677ac3a10e00.js +1 -0
- package/.next/static/chunks/651.7142f31ce1e052b3.js +1 -0
- package/.next/static/chunks/706-5aea41dd07970d2d.js +1 -0
- package/.next/static/chunks/738-67a288ca3569cdbb.js +1 -0
- package/.next/static/chunks/741.52f7fb873418346f.js +1 -0
- package/.next/static/chunks/791-63b1ccf9964b6517.js +1 -0
- package/.next/static/chunks/98.97381d2021f86cd9.js +1 -0
- package/.next/static/chunks/framework-dfd14d7ce6600b03.js +1 -0
- package/.next/static/chunks/main-fd466221927468fd.js +1 -0
- package/.next/static/chunks/pages/404-d5f20744ecd83121.js +1 -0
- package/.next/static/chunks/pages/500-3911549ab88d0378.js +1 -0
- package/.next/static/chunks/pages/[...slug]-3a37fd4d13cb2ba8.js +1 -0
- package/.next/static/chunks/pages/[slug]/p-bf47c90571846f86.js +1 -0
- package/.next/static/chunks/pages/_app-79d333aa6001a806.js +1 -0
- package/.next/static/chunks/pages/_error-a7a0c1d9bfbb4f38.js +1 -0
- package/.next/static/chunks/pages/account-d248acc931146694.js +1 -0
- package/.next/static/chunks/pages/checkout-97f6d6f36f041a6f.js +1 -0
- package/.next/static/chunks/pages/index-e9727cb06d9ae961.js +1 -0
- package/.next/static/chunks/pages/login-4cc4b8e52608f076.js +1 -0
- package/.next/static/chunks/pages/s-aabfa5c08338974a.js +1 -0
- package/.next/static/chunks/polyfills-c67a75d1b6f99dc8.js +1 -0
- package/.next/static/chunks/webpack-cbb5ca873d13aa82.js +1 -0
- package/.next/static/css/08e3fc80f9bad95d.css +1 -0
- package/.next/static/css/0e00026896a2ee3e.css +1 -0
- package/.next/static/css/1b23beb5af203ffb.css +1 -0
- package/.next/static/css/584640ffee46aa49.css +1 -0
- package/.next/static/css/7d822a137c54a781.css +1 -0
- package/.next/static/css/a49f71ae6bb528e0.css +1 -0
- package/.next/static/css/ccbb6df6cae68586.css +1 -0
- package/.next/static/css/e02cdad8fc000339.css +1 -0
- package/.next/static/css/f7ed956d370744ea.css +1 -0
- package/.next/static/css/f9d59f597a4d8f82.css +1 -0
- package/.next/trace +80 -0
- package/.turbo/turbo-build.log +13 -14
- package/README.md +1 -2
- package/cms/faststore/content-types.json +2 -2
- package/package.json +4 -4
- package/public/~partytown/debug/partytown-atomics.js +556 -0
- package/public/~partytown/debug/partytown-media.js +374 -0
- package/public/~partytown/debug/partytown-sandbox-sw.js +543 -0
- package/public/~partytown/debug/partytown-sw.js +59 -0
- package/public/~partytown/debug/partytown-ww-atomics.js +1789 -0
- package/public/~partytown/debug/partytown-ww-sw.js +1781 -0
- package/public/~partytown/debug/partytown.js +72 -0
- package/public/~partytown/partytown-atomics.js +2 -0
- package/public/~partytown/partytown-media.js +2 -0
- package/public/~partytown/partytown-sw.js +2 -0
- package/public/~partytown/partytown.js +2 -0
- package/src/components/cms/GlobalSections.tsx +2 -3
- package/src/components/common/Alert/section.module.scss +2 -0
- package/src/components/common/Footer/Footer.tsx +5 -14
- package/src/components/navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/navigation/NavbarLinks/NavbarLinks.tsx +1 -1
- package/src/components/product/ProductCard/ProductCard.tsx +1 -1
- package/src/components/sections/BannerNewsletter/BannerNewsletter.tsx +2 -4
- package/src/components/sections/BannerText/BannerText.tsx +0 -2
- package/src/components/sections/Breadcrumb/Breadcrumb.tsx +1 -1
- package/src/components/sections/Incentives/Incentives.tsx +1 -1
- package/src/components/sections/Incentives/section.module.scss +2 -0
- package/src/components/sections/Navbar/section.module.scss +4 -0
- package/src/components/sections/Newsletter/section.module.scss +2 -0
- package/src/components/sections/ProductDetails/ProductDetails.tsx +2 -4
- package/src/components/sections/ProductGallery/ProductGallery.tsx +2 -4
- package/src/components/sections/ProductGallery/section.module.scss +13 -0
- package/src/components/sections/ProductShelf/section.module.scss +5 -0
- package/src/components/sections/ProductTiles/ProductTiles.tsx +18 -20
- package/src/components/sections/Section/section.scss +8 -0
- package/src/components/skeletons/ProductShelfSkeleton/ProductShelfSkeleton.tsx +1 -1
- package/src/components/templates/LandingPage/LandingPage.tsx +102 -0
- package/src/components/templates/LandingPage/index.ts +2 -0
- package/src/components/templates/ProductListingPage/ProductListingPage.tsx +138 -0
- package/src/components/templates/ProductListingPage/index.ts +2 -0
- package/src/components/ui/Incentives/Incentives.tsx +1 -1
- package/src/components/ui/Newsletter/Newsletter.tsx +1 -1
- package/src/components/ui/ProductGallery/ProductGallery.tsx +5 -2
- package/src/pages/[...slug].tsx +45 -130
- package/src/pages/api/preview.ts +18 -2
- package/src/pages/index.tsx +1 -3
- package/src/sdk/error/MissingContentError/MissingContentError.ts +15 -0
- package/src/sdk/error/MissingContentError/index.ts +1 -0
- package/src/sdk/error/MultipleContentError/MultipleContentError.ts +15 -0
- package/src/sdk/error/MultipleContentError/index.ts +1 -0
- package/src/server/cms.ts +7 -21
- package/src/styles/global/components.scss +0 -6
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { NextSeo, SiteLinksSearchBoxJsonLd } from 'next-seo'
|
|
2
|
+
import type { ComponentType } from 'react'
|
|
3
|
+
import type { Locator } from '@vtex/client-cms'
|
|
4
|
+
|
|
5
|
+
import MissingContentError from 'src/sdk/error/MissingContentError/MissingContentError'
|
|
6
|
+
import RenderSections from 'src/components/cms/RenderSections'
|
|
7
|
+
import Hero from 'src/components/sections/Hero'
|
|
8
|
+
import Incentives from 'src/components/sections/Incentives'
|
|
9
|
+
import ProductShelf from 'src/components/sections/ProductShelf'
|
|
10
|
+
import ProductTiles from 'src/components/sections/ProductTiles'
|
|
11
|
+
import BannerText from 'src/components/sections/BannerText'
|
|
12
|
+
import Newsletter from 'src/components/sections/Newsletter'
|
|
13
|
+
import { getPage } from 'src/server/cms'
|
|
14
|
+
import type { PageContentType } from 'src/server/cms'
|
|
15
|
+
import CUSTOM_COMPONENTS from 'src/customizations/components'
|
|
16
|
+
|
|
17
|
+
import storeConfig from '../../../../faststore.config'
|
|
18
|
+
|
|
19
|
+
/* A list of components that can be used in the CMS. */
|
|
20
|
+
const COMPONENTS: Record<string, ComponentType<any>> = {
|
|
21
|
+
Hero,
|
|
22
|
+
Incentives,
|
|
23
|
+
ProductShelf,
|
|
24
|
+
ProductTiles,
|
|
25
|
+
BannerText,
|
|
26
|
+
Newsletter,
|
|
27
|
+
...CUSTOM_COMPONENTS,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type LandingPageProps = {
|
|
31
|
+
page: PageContentType
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function LandingPage({
|
|
35
|
+
page: { sections, settings },
|
|
36
|
+
}: LandingPageProps) {
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
{/* SEO */}
|
|
40
|
+
<NextSeo
|
|
41
|
+
title={settings?.seo?.title ?? storeConfig.seo.title}
|
|
42
|
+
description={settings?.seo?.description ?? storeConfig.seo?.description}
|
|
43
|
+
titleTemplate={storeConfig.seo?.titleTemplate ?? storeConfig.seo?.title}
|
|
44
|
+
canonical={settings?.seo?.canonical ?? storeConfig.storeUrl}
|
|
45
|
+
openGraph={{
|
|
46
|
+
type: 'website',
|
|
47
|
+
url: storeConfig.storeUrl,
|
|
48
|
+
title: settings?.seo?.title ?? storeConfig.seo.title,
|
|
49
|
+
description:
|
|
50
|
+
settings?.seo?.description ?? storeConfig.seo.description,
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
<SiteLinksSearchBoxJsonLd
|
|
54
|
+
url={storeConfig.storeUrl}
|
|
55
|
+
potentialActions={[
|
|
56
|
+
{
|
|
57
|
+
target: `${storeConfig.storeUrl}/s/?q`,
|
|
58
|
+
queryInput: 'search_term_string',
|
|
59
|
+
},
|
|
60
|
+
]}
|
|
61
|
+
/>
|
|
62
|
+
|
|
63
|
+
{/*
|
|
64
|
+
WARNING: Do not import or render components from any
|
|
65
|
+
other folder than '../components/sections' in here.
|
|
66
|
+
|
|
67
|
+
This is necessary to keep the integration with the CMS
|
|
68
|
+
easy and consistent, enabling the change and reorder
|
|
69
|
+
of elements on this page.
|
|
70
|
+
|
|
71
|
+
If needed, wrap your component in a <Section /> component
|
|
72
|
+
(not the HTML tag) before rendering it here.
|
|
73
|
+
*/}
|
|
74
|
+
<RenderSections sections={sections} components={COMPONENTS} />
|
|
75
|
+
</>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const getLandingPageBySlug = async (
|
|
80
|
+
slug: string,
|
|
81
|
+
previewData: Locator
|
|
82
|
+
) => {
|
|
83
|
+
try {
|
|
84
|
+
const landingPageData = await getPage<PageContentType>({
|
|
85
|
+
...(previewData?.contentType === 'landingPage'
|
|
86
|
+
? previewData
|
|
87
|
+
: {
|
|
88
|
+
filters: {
|
|
89
|
+
filters: { 'settings.seo.slug': `/${slug}` },
|
|
90
|
+
},
|
|
91
|
+
}),
|
|
92
|
+
contentType: 'landingPage',
|
|
93
|
+
})
|
|
94
|
+
return landingPageData
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof MissingContentError) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
throw error
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { SearchState } from '@faststore/sdk'
|
|
2
|
+
import {
|
|
3
|
+
formatSearchState,
|
|
4
|
+
parseSearchState,
|
|
5
|
+
SearchProvider,
|
|
6
|
+
} from '@faststore/sdk'
|
|
7
|
+
import { BreadcrumbJsonLd, NextSeo } from 'next-seo'
|
|
8
|
+
import { useRouter } from 'next/router'
|
|
9
|
+
import { useMemo } from 'react'
|
|
10
|
+
|
|
11
|
+
import type { ServerCollectionPageQueryQuery } from '@generated/graphql'
|
|
12
|
+
import Breadcrumb from 'src/components/sections/Breadcrumb'
|
|
13
|
+
import Hero from 'src/components/sections/Hero'
|
|
14
|
+
import ProductGallery from 'src/components/sections/ProductGallery'
|
|
15
|
+
import ProductShelf from 'src/components/sections/ProductShelf'
|
|
16
|
+
import ScrollToTopButton from 'src/components/sections/ScrollToTopButton'
|
|
17
|
+
import { ITEMS_PER_PAGE } from 'src/constants'
|
|
18
|
+
import { useApplySearchState } from 'src/sdk/search/state'
|
|
19
|
+
|
|
20
|
+
import type { ComponentType } from 'react'
|
|
21
|
+
import RenderSections from 'src/components/cms/RenderSections'
|
|
22
|
+
import CUSTOM_COMPONENTS from 'src/customizations/components'
|
|
23
|
+
import { PLPContentType } from 'src/server/cms'
|
|
24
|
+
|
|
25
|
+
import storeConfig from '../../../../faststore.config'
|
|
26
|
+
|
|
27
|
+
export type ProductListingPageProps = ServerCollectionPageQueryQuery & {
|
|
28
|
+
page: PLPContentType
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sections: Components imported from each store's custom components and '../components/sections' only.
|
|
33
|
+
* Do not import or render components from any other folder in here.
|
|
34
|
+
*/
|
|
35
|
+
const COMPONENTS: Record<string, ComponentType<any>> = {
|
|
36
|
+
Breadcrumb,
|
|
37
|
+
Hero,
|
|
38
|
+
ProductGallery,
|
|
39
|
+
ProductShelf,
|
|
40
|
+
...CUSTOM_COMPONENTS,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type UseSearchParams = ServerCollectionPageQueryQuery & {
|
|
44
|
+
sort: SearchState['sort']
|
|
45
|
+
}
|
|
46
|
+
const useSearchParams = ({
|
|
47
|
+
collection,
|
|
48
|
+
sort,
|
|
49
|
+
}: UseSearchParams): SearchState => {
|
|
50
|
+
const selectedFacets = collection?.meta.selectedFacets
|
|
51
|
+
const { asPath } = useRouter()
|
|
52
|
+
|
|
53
|
+
const hrefState = useMemo(() => {
|
|
54
|
+
const url = new URL(asPath, 'http://localhost')
|
|
55
|
+
|
|
56
|
+
const shouldUpdateDefaultSort = sort && !url.searchParams.has('sort')
|
|
57
|
+
if (shouldUpdateDefaultSort) {
|
|
58
|
+
url.searchParams.set('sort', sort)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const newState = parseSearchState(url)
|
|
62
|
+
// In case we are in an incomplete url
|
|
63
|
+
if (newState.selectedFacets.length === 0) {
|
|
64
|
+
newState.selectedFacets = selectedFacets
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return formatSearchState(newState).href
|
|
68
|
+
}, [asPath, selectedFacets, sort])
|
|
69
|
+
|
|
70
|
+
return useMemo(() => parseSearchState(new URL(hrefState)), [hrefState])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default function ProductListingPage({
|
|
74
|
+
page: { sections, settings },
|
|
75
|
+
...otherProps
|
|
76
|
+
}: ProductListingPageProps) {
|
|
77
|
+
const { collection } = otherProps
|
|
78
|
+
const router = useRouter()
|
|
79
|
+
const applySearchState = useApplySearchState()
|
|
80
|
+
const searchParams = useSearchParams({
|
|
81
|
+
...otherProps,
|
|
82
|
+
sort: settings?.productGallery?.sortBySelection as SearchState['sort'],
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
const { page, sort } = searchParams
|
|
86
|
+
const title = collection?.seo.title ?? storeConfig.seo.title
|
|
87
|
+
const description = collection?.seo.description ?? storeConfig.seo.title
|
|
88
|
+
const pageQuery = page !== 0 ? `?page=${page}` : ''
|
|
89
|
+
const separator = pageQuery !== '' ? '&' : '?'
|
|
90
|
+
const sortQuery = !!sort ? `${separator}sort=${sort}` : ''
|
|
91
|
+
const [pathname] = router.asPath.split('?')
|
|
92
|
+
const canonical = `${storeConfig.storeUrl}${pathname}${pageQuery}${sortQuery}`
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<SearchProvider
|
|
97
|
+
onChange={applySearchState}
|
|
98
|
+
itemsPerPage={settings?.productGallery?.itemsPerPage ?? ITEMS_PER_PAGE}
|
|
99
|
+
{...searchParams}
|
|
100
|
+
>
|
|
101
|
+
{/* SEO */}
|
|
102
|
+
<NextSeo
|
|
103
|
+
title={title}
|
|
104
|
+
description={description}
|
|
105
|
+
titleTemplate={storeConfig.seo.titleTemplate}
|
|
106
|
+
canonical={canonical}
|
|
107
|
+
openGraph={{
|
|
108
|
+
type: 'website',
|
|
109
|
+
title,
|
|
110
|
+
description,
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<BreadcrumbJsonLd
|
|
114
|
+
itemListElements={collection?.breadcrumbList.itemListElement ?? []}
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
{/*
|
|
118
|
+
WARNING: Do not import or render components from any
|
|
119
|
+
other folder than '../components/sections' in here.
|
|
120
|
+
|
|
121
|
+
This is necessary to keep the integration with the CMS
|
|
122
|
+
easy and consistent, enabling the change and reorder
|
|
123
|
+
of elements on this page.
|
|
124
|
+
|
|
125
|
+
If needed, wrap your component in a <Section /> component
|
|
126
|
+
(not the HTML tag) before rendering it here.
|
|
127
|
+
*/}
|
|
128
|
+
<RenderSections
|
|
129
|
+
context={collection}
|
|
130
|
+
sections={sections}
|
|
131
|
+
components={COMPONENTS}
|
|
132
|
+
/>
|
|
133
|
+
|
|
134
|
+
<ScrollToTopButton />
|
|
135
|
+
</SearchProvider>
|
|
136
|
+
</>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -35,7 +35,7 @@ function Incentives({
|
|
|
35
35
|
data-fs-incentives-colored={colored}
|
|
36
36
|
data-fs-incentives-variant={variant}
|
|
37
37
|
>
|
|
38
|
-
<UIList
|
|
38
|
+
<UIList data-fs-content="incentives">
|
|
39
39
|
{incentives.map((incentive, index) => (
|
|
40
40
|
<li role="listitem" key={String(index)}>
|
|
41
41
|
<UIIncentive tabIndex={0}>
|
|
@@ -179,8 +179,8 @@ const Newsletter = forwardRef<HTMLFormElement, NewsletterProps>(
|
|
|
179
179
|
<form
|
|
180
180
|
ref={ref}
|
|
181
181
|
data-fs-newsletter-form
|
|
182
|
+
data-fs-content="newsletter"
|
|
182
183
|
onSubmit={handleSubmit}
|
|
183
|
-
className="layout__content"
|
|
184
184
|
>
|
|
185
185
|
<header data-fs-newsletter-header>
|
|
186
186
|
<h3>
|
|
@@ -95,13 +95,16 @@ function ProductGallery({
|
|
|
95
95
|
return (
|
|
96
96
|
<section data-testid="product-gallery" data-fs-product-listing>
|
|
97
97
|
{searchTerm && (
|
|
98
|
-
<header data-fs-product-listing-search-term
|
|
98
|
+
<header data-fs-product-listing-search-term>
|
|
99
99
|
<h1>
|
|
100
100
|
{searchTermLabel} <span>{searchTerm}</span>
|
|
101
101
|
</h1>
|
|
102
102
|
</header>
|
|
103
103
|
)}
|
|
104
|
-
<div
|
|
104
|
+
<div
|
|
105
|
+
data-fs-product-listing-content-grid
|
|
106
|
+
data-fs-content="product-gallery"
|
|
107
|
+
>
|
|
105
108
|
<div data-fs-product-listing-filters>
|
|
106
109
|
<FilterSkeleton loading={facets?.length === 0}>
|
|
107
110
|
<Filter facets={facets} filter={filter} />
|
package/src/pages/[...slug].tsx
CHANGED
|
@@ -1,152 +1,51 @@
|
|
|
1
1
|
import { isNotFoundError } from '@faststore/api'
|
|
2
2
|
import { gql } from '@faststore/graphql-utils'
|
|
3
|
-
import type { SearchState } from '@faststore/sdk'
|
|
4
|
-
import {
|
|
5
|
-
formatSearchState,
|
|
6
|
-
parseSearchState,
|
|
7
|
-
SearchProvider,
|
|
8
|
-
} from '@faststore/sdk'
|
|
9
3
|
import type { GetStaticPaths, GetStaticProps } from 'next'
|
|
10
|
-
import { BreadcrumbJsonLd, NextSeo } from 'next-seo'
|
|
11
|
-
import { useRouter } from 'next/router'
|
|
12
|
-
import { useMemo } from 'react'
|
|
13
4
|
|
|
14
5
|
import type {
|
|
15
6
|
ServerCollectionPageQueryQuery,
|
|
16
7
|
ServerCollectionPageQueryQueryVariables,
|
|
17
8
|
} from '@generated/graphql'
|
|
18
|
-
import Breadcrumb from 'src/components/sections/Breadcrumb'
|
|
19
|
-
import Hero from 'src/components/sections/Hero'
|
|
20
|
-
import ProductGallery from 'src/components/sections/ProductGallery'
|
|
21
|
-
import ProductShelf from 'src/components/sections/ProductShelf'
|
|
22
|
-
import ScrollToTopButton from 'src/components/sections/ScrollToTopButton'
|
|
23
|
-
import { ITEMS_PER_PAGE, ITEMS_PER_SECTION } from 'src/constants'
|
|
24
|
-
import { useApplySearchState } from 'src/sdk/search/state'
|
|
25
9
|
import { mark } from 'src/sdk/tests/mark'
|
|
26
10
|
import { execute } from 'src/server'
|
|
27
11
|
|
|
28
12
|
import { Locator } from '@vtex/client-cms'
|
|
29
|
-
import type { ComponentType } from 'react'
|
|
30
13
|
import GlobalSections, {
|
|
31
14
|
getGlobalSectionsData,
|
|
32
15
|
GlobalSectionsData,
|
|
33
16
|
} from 'src/components/cms/GlobalSections'
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
17
|
+
import { getPage, PageContentType, PLPContentType } from 'src/server/cms'
|
|
18
|
+
import ProductListingPage, {
|
|
19
|
+
ProductListingPageProps,
|
|
20
|
+
} from 'src/components/templates/ProductListingPage'
|
|
21
|
+
import LandingPage, {
|
|
22
|
+
getLandingPageBySlug,
|
|
23
|
+
LandingPageProps,
|
|
24
|
+
} from 'src/components/templates/LandingPage'
|
|
25
|
+
|
|
26
|
+
type BaseProps = {
|
|
41
27
|
globalSections: GlobalSectionsData
|
|
42
28
|
}
|
|
43
29
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
type UseSearchParams = ServerCollectionPageQueryQuery & {
|
|
57
|
-
sort: SearchState['sort']
|
|
58
|
-
}
|
|
59
|
-
const useSearchParams = ({
|
|
60
|
-
collection,
|
|
61
|
-
sort,
|
|
62
|
-
}: UseSearchParams): SearchState => {
|
|
63
|
-
const selectedFacets = collection?.meta.selectedFacets
|
|
64
|
-
const { asPath } = useRouter()
|
|
65
|
-
|
|
66
|
-
const hrefState = useMemo(() => {
|
|
67
|
-
const url = new URL(asPath, 'http://localhost')
|
|
68
|
-
|
|
69
|
-
const shouldUpdateDefaultSort = sort && !url.searchParams.has('sort')
|
|
70
|
-
if (shouldUpdateDefaultSort) {
|
|
71
|
-
url.searchParams.set('sort', sort)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const newState = parseSearchState(url)
|
|
75
|
-
// In case we are in an incomplete url
|
|
76
|
-
if (newState.selectedFacets.length === 0) {
|
|
77
|
-
newState.selectedFacets = selectedFacets
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return formatSearchState(newState).href
|
|
81
|
-
}, [asPath, selectedFacets, sort])
|
|
82
|
-
|
|
83
|
-
return useMemo(() => parseSearchState(new URL(hrefState)), [hrefState])
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function Page({
|
|
87
|
-
page: { sections, settings },
|
|
88
|
-
globalSections,
|
|
89
|
-
...otherProps
|
|
90
|
-
}: Props) {
|
|
91
|
-
const { collection } = otherProps
|
|
92
|
-
const router = useRouter()
|
|
93
|
-
const applySearchState = useApplySearchState()
|
|
94
|
-
const searchParams = useSearchParams({
|
|
95
|
-
...otherProps,
|
|
96
|
-
sort: settings?.productGallery?.sortBySelection as SearchState['sort'],
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
const { page, sort } = searchParams
|
|
100
|
-
const title = collection?.seo.title ?? storeConfig.seo.title
|
|
101
|
-
const description = collection?.seo.description ?? storeConfig.seo.title
|
|
102
|
-
const pageQuery = page !== 0 ? `?page=${page}` : ''
|
|
103
|
-
const separator = pageQuery !== '' ? '&' : '?'
|
|
104
|
-
const sortQuery = !!sort ? `${separator}sort=${sort}` : ''
|
|
105
|
-
const [pathname] = router.asPath.split('?')
|
|
106
|
-
const canonical = `${storeConfig.storeUrl}${pathname}${pageQuery}${sortQuery}`
|
|
30
|
+
type Props = BaseProps &
|
|
31
|
+
(
|
|
32
|
+
| ({
|
|
33
|
+
type: 'plp'
|
|
34
|
+
page: PLPContentType
|
|
35
|
+
} & ServerCollectionPageQueryQuery)
|
|
36
|
+
| {
|
|
37
|
+
type: 'page'
|
|
38
|
+
page: PageContentType
|
|
39
|
+
}
|
|
40
|
+
)
|
|
107
41
|
|
|
42
|
+
function Page({ globalSections, type, ...otherProps }: Props) {
|
|
108
43
|
return (
|
|
109
44
|
<GlobalSections {...globalSections}>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
>
|
|
115
|
-
{/* SEO */}
|
|
116
|
-
<NextSeo
|
|
117
|
-
title={title}
|
|
118
|
-
description={description}
|
|
119
|
-
titleTemplate={storeConfig.seo.titleTemplate}
|
|
120
|
-
canonical={canonical}
|
|
121
|
-
openGraph={{
|
|
122
|
-
type: 'website',
|
|
123
|
-
title,
|
|
124
|
-
description,
|
|
125
|
-
}}
|
|
126
|
-
/>
|
|
127
|
-
<BreadcrumbJsonLd
|
|
128
|
-
itemListElements={collection?.breadcrumbList.itemListElement ?? []}
|
|
129
|
-
/>
|
|
130
|
-
|
|
131
|
-
{/*
|
|
132
|
-
WARNING: Do not import or render components from any
|
|
133
|
-
other folder than '../components/sections' in here.
|
|
134
|
-
|
|
135
|
-
This is necessary to keep the integration with the CMS
|
|
136
|
-
easy and consistent, enabling the change and reorder
|
|
137
|
-
of elements on this page.
|
|
138
|
-
|
|
139
|
-
If needed, wrap your component in a <Section /> component
|
|
140
|
-
(not the HTML tag) before rendering it here.
|
|
141
|
-
*/}
|
|
142
|
-
<RenderSections
|
|
143
|
-
context={collection}
|
|
144
|
-
sections={sections}
|
|
145
|
-
components={COMPONENTS}
|
|
146
|
-
/>
|
|
147
|
-
|
|
148
|
-
<ScrollToTopButton />
|
|
149
|
-
</SearchProvider>
|
|
45
|
+
{type === 'plp' && (
|
|
46
|
+
<ProductListingPage {...(otherProps as ProductListingPageProps)} />
|
|
47
|
+
)}
|
|
48
|
+
{type === 'page' && <LandingPage {...(otherProps as LandingPageProps)} />}
|
|
150
49
|
</GlobalSections>
|
|
151
50
|
)
|
|
152
51
|
}
|
|
@@ -180,7 +79,22 @@ export const getStaticProps: GetStaticProps<
|
|
|
180
79
|
{ slug: string[] },
|
|
181
80
|
Locator
|
|
182
81
|
> = async ({ params, previewData }) => {
|
|
183
|
-
const [
|
|
82
|
+
const [landingPagePromise, globalSectionsPromise] = [
|
|
83
|
+
getLandingPageBySlug(params?.slug[0], previewData),
|
|
84
|
+
getGlobalSectionsData(previewData),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
if (await landingPagePromise) {
|
|
88
|
+
return {
|
|
89
|
+
props: {
|
|
90
|
+
page: await landingPagePromise,
|
|
91
|
+
globalSections: await globalSectionsPromise,
|
|
92
|
+
type: 'page',
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [{ data, errors = [] }, page] = await Promise.all([
|
|
184
98
|
execute<
|
|
185
99
|
ServerCollectionPageQueryQueryVariables,
|
|
186
100
|
ServerCollectionPageQueryQuery
|
|
@@ -192,7 +106,6 @@ export const getStaticProps: GetStaticProps<
|
|
|
192
106
|
...(previewData?.contentType === 'plp' ? previewData : null),
|
|
193
107
|
contentType: 'plp',
|
|
194
108
|
}),
|
|
195
|
-
getGlobalSectionsData(previewData),
|
|
196
109
|
])
|
|
197
110
|
|
|
198
111
|
const notFound = errors.find(isNotFoundError)
|
|
@@ -204,6 +117,7 @@ export const getStaticProps: GetStaticProps<
|
|
|
204
117
|
}
|
|
205
118
|
|
|
206
119
|
if (errors.length > 0) {
|
|
120
|
+
console.error(...errors)
|
|
207
121
|
throw errors[0]
|
|
208
122
|
}
|
|
209
123
|
|
|
@@ -211,7 +125,8 @@ export const getStaticProps: GetStaticProps<
|
|
|
211
125
|
props: {
|
|
212
126
|
...data,
|
|
213
127
|
page,
|
|
214
|
-
globalSections,
|
|
128
|
+
globalSections: await globalSectionsPromise,
|
|
129
|
+
type: 'plp',
|
|
215
130
|
},
|
|
216
131
|
}
|
|
217
132
|
}
|
package/src/pages/api/preview.ts
CHANGED
|
@@ -3,6 +3,13 @@ import type { NextApiHandler, NextApiRequest } from 'next'
|
|
|
3
3
|
import { clientCMS } from 'src/server/cms'
|
|
4
4
|
import { previewRedirects } from '../../../faststore.config'
|
|
5
5
|
|
|
6
|
+
type Settings = {
|
|
7
|
+
seo: {
|
|
8
|
+
slug: string
|
|
9
|
+
title: string
|
|
10
|
+
description: string
|
|
11
|
+
}
|
|
12
|
+
}
|
|
6
13
|
class StatusError extends Error {
|
|
7
14
|
constructor(message: string, public status: number) {
|
|
8
15
|
super(message)
|
|
@@ -46,8 +53,17 @@ const handler: NextApiHandler = async (req, res) => {
|
|
|
46
53
|
res.setPreviewData(locator, { maxAge: 3600 })
|
|
47
54
|
|
|
48
55
|
// Redirect to the path from the fetched locator
|
|
49
|
-
|
|
50
|
-
|
|
56
|
+
if (previewRedirects[locator.contentType]) {
|
|
57
|
+
res.redirect(previewRedirects[locator.contentType])
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (locator.contentType === 'landingPage') {
|
|
62
|
+
res.redirect(`${(page.settings as Settings)?.seo.slug}`)
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
res.redirect('/')
|
|
51
67
|
} catch (error) {
|
|
52
68
|
if (error instanceof StatusError) {
|
|
53
69
|
res.status(error.status).end(error.message)
|
package/src/pages/index.tsx
CHANGED
|
@@ -87,9 +87,7 @@ export const getStaticProps: GetStaticProps<
|
|
|
87
87
|
> = async ({ previewData }) => {
|
|
88
88
|
const [page, globalSections] = await Promise.all([
|
|
89
89
|
getPage<PageContentType>({
|
|
90
|
-
...(previewData?.contentType === 'home'
|
|
91
|
-
? previewData
|
|
92
|
-
: { filters: { 'settings.seo.slug': '/' } }),
|
|
90
|
+
...(previewData?.contentType === 'home' && previewData),
|
|
93
91
|
contentType: 'home',
|
|
94
92
|
}),
|
|
95
93
|
getGlobalSectionsData(previewData),
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Options } from 'src/server/cms'
|
|
2
|
+
|
|
3
|
+
export default class MissingContentError extends Error {
|
|
4
|
+
constructor(options: Options) {
|
|
5
|
+
super(
|
|
6
|
+
`Missing content on the CMS for content type ${
|
|
7
|
+
options.contentType
|
|
8
|
+
}. Add content before proceeding. Context: ${JSON.stringify(
|
|
9
|
+
options,
|
|
10
|
+
null,
|
|
11
|
+
2
|
|
12
|
+
)}`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MissingContentError'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Options } from 'src/server/cms'
|
|
2
|
+
|
|
3
|
+
export default class MultipleContentError extends Error {
|
|
4
|
+
constructor(options: Options) {
|
|
5
|
+
super(
|
|
6
|
+
`Multiple content defined on the CMS for content type ${
|
|
7
|
+
options.contentType
|
|
8
|
+
}. Remove duplicated content before proceeding. Context: ${JSON.stringify(
|
|
9
|
+
options,
|
|
10
|
+
null,
|
|
11
|
+
2
|
|
12
|
+
)}`
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './MultipleContentError'
|
package/src/server/cms.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import type { ContentData, Locator } from '@vtex/client-cms'
|
|
1
|
+
import type { ContentData, ContentTypeOptions, Locator } from '@vtex/client-cms'
|
|
2
2
|
import ClientCMS from '@vtex/client-cms'
|
|
3
3
|
|
|
4
4
|
import config from '../../faststore.config'
|
|
5
|
+
import MultipleContentError from 'src/sdk/error/MultipleContentError'
|
|
6
|
+
import MissingContentError from 'src/sdk/error/MissingContentError'
|
|
5
7
|
|
|
6
8
|
export const clientCMS = new ClientCMS({
|
|
7
9
|
workspace: config.api.workspace,
|
|
8
10
|
tenant: config.api.storeId,
|
|
9
11
|
})
|
|
10
12
|
|
|
11
|
-
type Options =
|
|
13
|
+
export type Options =
|
|
12
14
|
| Locator
|
|
13
15
|
| {
|
|
14
16
|
contentType: string
|
|
15
|
-
filters?:
|
|
17
|
+
filters?: Partial<ContentTypeOptions>
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const isLocator = (x: any): x is Locator =>
|
|
@@ -27,27 +29,11 @@ export const getPage = async <T extends ContentData>(options: Options) => {
|
|
|
27
29
|
const pages = result.data
|
|
28
30
|
|
|
29
31
|
if (!pages[0]) {
|
|
30
|
-
throw new
|
|
31
|
-
`Missing content on the CMS for content type ${
|
|
32
|
-
options.contentType
|
|
33
|
-
}. Add content before proceeding. Context: ${JSON.stringify(
|
|
34
|
-
options,
|
|
35
|
-
null,
|
|
36
|
-
2
|
|
37
|
-
)}`
|
|
38
|
-
)
|
|
32
|
+
throw new MissingContentError(options)
|
|
39
33
|
}
|
|
40
34
|
|
|
41
35
|
if (pages.length !== 1) {
|
|
42
|
-
throw new
|
|
43
|
-
`Multiple content defined on the CMS for content type ${
|
|
44
|
-
options.contentType
|
|
45
|
-
}. Remove duplicated content before proceeding. Context: ${JSON.stringify(
|
|
46
|
-
options,
|
|
47
|
-
null,
|
|
48
|
-
2
|
|
49
|
-
)}`
|
|
50
|
-
)
|
|
36
|
+
throw new MultipleContentError(options)
|
|
51
37
|
}
|
|
52
38
|
|
|
53
39
|
return pages[0] as T
|
|
@@ -2,9 +2,3 @@
|
|
|
2
2
|
@import "@faststore/ui/src/components/atoms/Overlay/styles.scss";
|
|
3
3
|
@import "@faststore/ui/src/components/atoms/SROnly/styles.scss";
|
|
4
4
|
@import "src/components/sections/Section/section.scss";
|
|
5
|
-
|
|
6
|
-
.section-navbar {
|
|
7
|
-
position: sticky;
|
|
8
|
-
top: 0;
|
|
9
|
-
z-index: var(--fs-z-index-high);
|
|
10
|
-
}
|