diff --git a/packages/core/cms/faststore/content-types.json b/packages/core/cms/faststore/content-types.json index 28f56ac2a2..64a1ab632e 100644 --- a/packages/core/cms/faststore/content-types.json +++ b/packages/core/cms/faststore/content-types.json @@ -207,5 +207,23 @@ ] } ] + }, + { + "id": "login", + "name": "Login", + "configurationSchemaSets": [], + "isSingleton": true + }, + { + "id": "500", + "name": "Error 500", + "configurationSchemaSets": [], + "isSingleton": true + }, + { + "id": "404", + "name": "Error 404", + "configurationSchemaSets": [], + "isSingleton": true } ] diff --git a/packages/core/cms/faststore/sections.json b/packages/core/cms/faststore/sections.json index 8dcbeaa29c..b60c291cac 100644 --- a/packages/core/cms/faststore/sections.json +++ b/packages/core/cms/faststore/sections.json @@ -1949,5 +1949,78 @@ } } } + }, + { + "name": "EmptyState", + "schema": { + "title": "Empty State", + "type": "object", + "description": "Empty State configuration", + "properties": { + "title": { + "title": "Title", + "type": "string" + }, + "titleIcon": { + "title": "Title Icon", + "type": "object", + "properties": { + "icon": { + "title": "Icon", + "type": "string", + "enumNames": ["CircleWavy Warning"], + "enum": ["CircleWavyWarning"] + }, + "alt": { + "title": "Alternative Label", + "type": "string" + } + } + }, + "subtitle": { + "title": "Subtitle", + "type": "string" + }, + "showLoader": { + "type": "boolean", + "title": "Show loader?", + "default": false + }, + "errorState": { + "title": "Error state used for shown errorId and fromUrl properties in 500 and 404 pages", + "type": "object", + "properties": { + "errorId": { + "title": "errorId used in 500 and 404 pages", + "type": "object", + "properties": { + "show": { + "type": "boolean", + "title": "Show errorId in the end of message?" + }, + "description": { + "type": "string", + "title": "Description shown before the errorId" + } + } + }, + "fromUrl": { + "title": "fromUrl used in 500 and 404 pages", + "type": "object", + "properties": { + "show": { + "type": "boolean", + "title": "Show fromUrl in the end of message?" + }, + "description": { + "type": "string", + "title": "Description shown before the fromUrl" + } + } + } + } + } + } + } } ] diff --git a/packages/core/index.ts b/packages/core/index.ts index 33b1e00c30..7a8e9e71d1 100644 --- a/packages/core/index.ts +++ b/packages/core/index.ts @@ -24,6 +24,7 @@ export { default as AlertSection } from './src/components/sections/Alert' export { default as BannerTextSection } from './src/components/sections/BannerText' export { default as BreadcrumbSection } from './src/components/sections/Breadcrumb' export { default as CrossSellingShelfSection } from './src/components/sections/CrossSellingShelf' +export { default as EmptyState } from './src/components/sections/EmptyState' export { default as HeroSection } from './src/components/sections/Hero' export { default as NavbarSection } from './src/components/sections/Navbar' export { default as NewsletterSection } from './src/components/sections/Newsletter' diff --git a/packages/core/src/components/sections/EmptyState/DefaultComponents.ts b/packages/core/src/components/sections/EmptyState/DefaultComponents.ts new file mode 100644 index 0000000000..abd5d6643d --- /dev/null +++ b/packages/core/src/components/sections/EmptyState/DefaultComponents.ts @@ -0,0 +1,5 @@ +import { EmptyState as UIEmptyState } from '@faststore/ui' + +export const EmptyStateDefaultComponents = { + EmptyState: UIEmptyState, +} as const diff --git a/packages/core/src/components/sections/EmptyState/EmptyState.tsx b/packages/core/src/components/sections/EmptyState/EmptyState.tsx index e2f6af5b4a..11ab8e7f7f 100644 --- a/packages/core/src/components/sections/EmptyState/EmptyState.tsx +++ b/packages/core/src/components/sections/EmptyState/EmptyState.tsx @@ -1,33 +1,116 @@ -import { ReactNode } from 'react' import type { PropsWithChildren } from 'react' +import { useRouter } from 'next/router' + +import { Icon as UIIcon, Loader as UILoader } from '@faststore/ui' + +import { useOverrideComponents } from '../../../sdk/overrides/OverrideContext' import Section from '../Section' + import styles from './section.module.scss' -import { EmptyState as EmptyStateWrapper } from 'src/components/sections/EmptyState/Overrides' +import { EmptyStateDefaultComponents } from './DefaultComponents' +import { getOverridableSection } from '../../../sdk/overrides/getOverriddenSection' export interface EmptyStateProps { + /** + * Title for the `EmptyState` component. + */ title: string - titleIcon?: ReactNode + /** + * A React component that will be rendered as an icon. + */ + titleIcon?: { + icon: string + alt: string + } + /** + * Subtitle for the `EmptyState` component. + */ + subtitle?: string + /** + * Boolean that makes the loader be shown. + */ + showLoader?: boolean + /** + * Object that manages the error state descriptions. + */ + errorState?: { + errorId?: { + show?: boolean + description?: string + } + fromUrl?: { + show?: boolean + description?: string + } + } +} + +const useErrorState = () => { + const router = useRouter() + const { + query: { errorId, fromUrl }, + pathname, + asPath, + } = router + + return { + errorId, + fromUrl: fromUrl ?? asPath ?? pathname, + } } function EmptyState({ - title = EmptyStateWrapper.props.title, - titleIcon = EmptyStateWrapper.props.titleIcon, + title, + titleIcon, children, + subtitle, + errorState, + showLoader = false, }: PropsWithChildren) { + const { EmptyState: EmptyStateWrapper } = + useOverrideComponents<'EmptyState'>() + const { errorId, fromUrl } = useErrorState() + + const icon = !!titleIcon?.icon ? ( + + ) : ( + EmptyStateWrapper.props.titleIcon + ) + return (
+ {!!subtitle &&

{subtitle}

} + {!!errorState?.errorId?.show && ( +

{`${errorState?.errorId?.description} ${errorId}`}

+ )} + {!!errorState?.fromUrl?.show && ( +

{`${errorState?.fromUrl?.description} ${fromUrl}`}

+ )} + {showLoader && } {children}
) } -export default EmptyState +const OverridableEmptyState = getOverridableSection( + 'EmptyState', + EmptyState, + EmptyStateDefaultComponents +) + +export default OverridableEmptyState diff --git a/packages/core/src/components/sections/EmptyState/OverriddenDefaultEmptyState.ts b/packages/core/src/components/sections/EmptyState/OverriddenDefaultEmptyState.ts new file mode 100644 index 0000000000..cc43f67ff9 --- /dev/null +++ b/packages/core/src/components/sections/EmptyState/OverriddenDefaultEmptyState.ts @@ -0,0 +1,15 @@ +import { override } from 'src/customizations/src/components/overrides/EmptyState' +import { getOverriddenSection } from 'src/sdk/overrides/getOverriddenSection' + +import type { SectionOverrideDefinitionV1 } from 'src/typings/overridesDefinition' +import EmptyState from './EmptyState' + +/** + * This component exists to support overrides 1.0 + * + * This allows users to override the default EmptyState section present in the Headless CMS + */ +export const OverriddenDefaultEmptyState = getOverriddenSection({ + ...(override as SectionOverrideDefinitionV1<'EmptyState'>), + Section: EmptyState, +}) diff --git a/packages/core/src/components/sections/EmptyState/Overrides.tsx b/packages/core/src/components/sections/EmptyState/Overrides.tsx deleted file mode 100644 index 203f9b69e7..0000000000 --- a/packages/core/src/components/sections/EmptyState/Overrides.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { EmptyState as UIEmptyState } from '@faststore/ui' - -import { getSectionOverrides } from 'src/sdk/overrides/overrides' -import { override } from 'src/customizations/src/components/overrides/EmptyState' -import type { SectionOverrideDefinitionV1 } from 'src/typings/overridesDefinition' - -const { EmptyState } = getSectionOverrides( - { - EmptyState: UIEmptyState, - }, - override as SectionOverrideDefinitionV1<'EmptyState'> -) - -export { EmptyState } diff --git a/packages/core/src/pages/404.tsx b/packages/core/src/pages/404.tsx index 7f8e0da750..35e2236eb8 100644 --- a/packages/core/src/pages/404.tsx +++ b/packages/core/src/pages/404.tsx @@ -1,48 +1,44 @@ +import { Locator } from '@vtex/client-cms' +import { GetStaticProps } from 'next' import { NextSeo } from 'next-seo' -import { useRouter } from 'next/router' import GlobalSections, { GlobalSectionsData, getGlobalSectionsData, } from 'src/components/cms/GlobalSections' -import { GetStaticProps } from 'next' -import { Locator } from '@vtex/client-cms' - -import { Icon as UIIcon } from '@faststore/ui' -import EmptyState from 'src/components/sections/EmptyState' +import type { ComponentType } from 'react' -const useErrorState = () => { - const router = useRouter() - const { pathname, asPath } = router +import RenderSections from 'src/components/cms/RenderSections' +import { OverriddenDefaultEmptyState as EmptyState } from 'src/components/sections/EmptyState/OverriddenDefaultEmptyState' +import CUSTOM_COMPONENTS from 'src/customizations/src/components' +import { PageContentType, getPage } from 'src/server/cms' - return { - fromUrl: asPath ?? pathname, - } +/* A list of components that can be used in the CMS. */ +const COMPONENTS: Record> = { + EmptyState, + ...CUSTOM_COMPONENTS, } type Props = { + page: PageContentType globalSections: GlobalSectionsData } -function Page({ globalSections }: Props) { - const { fromUrl } = useErrorState() - +function Page({ page: { sections }, globalSections }: Props) { return ( + {/* + WARNING: Do not import or render components from any + other folder than '../components/sections' in here. + + This is necessary to keep the integration with the CMS + easy and consistent, enabling the change and reorder + of elements on this page. - - } - > -

This app could not find url {fromUrl}

-
+ If needed, wrap your component in a
component + (not the HTML tag) before rendering it here. + */} + ) } @@ -52,10 +48,16 @@ export const getStaticProps: GetStaticProps< Record, Locator > = async ({ previewData }) => { - const globalSections = await getGlobalSectionsData(previewData) + const [page, globalSections] = await Promise.all([ + getPage({ + ...(previewData?.contentType === '404' && previewData), + contentType: '404', + }), + getGlobalSectionsData(previewData), + ]) return { - props: { globalSections }, + props: { page, globalSections }, } } diff --git a/packages/core/src/pages/500.tsx b/packages/core/src/pages/500.tsx index 4a0c0ceaca..859bf01dd2 100644 --- a/packages/core/src/pages/500.tsx +++ b/packages/core/src/pages/500.tsx @@ -1,53 +1,45 @@ import { Locator } from '@vtex/client-cms' import { GetStaticProps } from 'next' import { NextSeo } from 'next-seo' -import { useRouter } from 'next/router' import GlobalSections, { GlobalSectionsData, getGlobalSectionsData, } from 'src/components/cms/GlobalSections' +import type { ComponentType } from 'react' -import { Icon as UIIcon } from '@faststore/ui' -import EmptyState from 'src/components/sections/EmptyState' +import RenderSections from 'src/components/cms/RenderSections' +import { OverriddenDefaultEmptyState as EmptyState } from 'src/components/sections/EmptyState/OverriddenDefaultEmptyState' +import CUSTOM_COMPONENTS from 'src/customizations/src/components' +import { PageContentType, getPage } from 'src/server/cms' -type Props = { - globalSections: GlobalSectionsData +/* A list of components that can be used in the CMS. */ +const COMPONENTS: Record> = { + EmptyState, + ...CUSTOM_COMPONENTS, } -const useErrorState = () => { - const router = useRouter() - const { errorId, fromUrl } = router.query - - return { - errorId, - fromUrl, - } +type Props = { + page: PageContentType + globalSections: GlobalSectionsData } -function Page({ globalSections }: Props) { - const { errorId, fromUrl } = useErrorState() - +function Page({ page: { sections }, globalSections }: Props) { return ( - - } - > -

Internal Server Error

+ {/* + WARNING: Do not import or render components from any + other folder than '../components/sections' in here. + + This is necessary to keep the integration with the CMS + easy and consistent, enabling the change and reorder + of elements on this page. -
- The server errored with id {errorId} when visiting page {fromUrl} -
-
+ If needed, wrap your component in a
component + (not the HTML tag) before rendering it here. + */} + ) } @@ -57,10 +49,16 @@ export const getStaticProps: GetStaticProps< Record, Locator > = async ({ previewData }) => { - const globalSections = await getGlobalSectionsData(previewData) + const [page, globalSections] = await Promise.all([ + getPage({ + ...(previewData?.contentType === '500' && previewData), + contentType: '500', + }), + getGlobalSectionsData(previewData), + ]) return { - props: { globalSections }, + props: { page, globalSections }, } } diff --git a/packages/core/src/pages/login.tsx b/packages/core/src/pages/login.tsx index ab3abcd5e2..663b085966 100644 --- a/packages/core/src/pages/login.tsx +++ b/packages/core/src/pages/login.tsx @@ -1,5 +1,6 @@ import { useEffect } from 'react' import { NextSeo } from 'next-seo' +import type { ComponentType } from 'react' import storeConfig from '../../faststore.config' import GlobalSections, { @@ -8,15 +9,23 @@ import GlobalSections, { } from 'src/components/cms/GlobalSections' import { GetStaticProps } from 'next' import { Locator } from '@vtex/client-cms' +import RenderSections from 'src/components/cms/RenderSections' +import { OverriddenDefaultEmptyState as EmptyState } from 'src/components/sections/EmptyState/OverriddenDefaultEmptyState' +import CUSTOM_COMPONENTS from 'src/customizations/src/components' +import { PageContentType, getPage } from 'src/server/cms' -import { Loader as UILoader } from '@faststore/ui' -import EmptyState from 'src/components/sections/EmptyState' +/* A list of components that can be used in the CMS. */ +const COMPONENTS: Record> = { + EmptyState, + ...CUSTOM_COMPONENTS, +} type Props = { + page: PageContentType globalSections: GlobalSectionsData } -function Page({ globalSections }: Props) { +function Page({ page: { sections }, globalSections }: Props) { useEffect(() => { const loginUrl = new URL(storeConfig.loginUrl) const incomingParams = new URLSearchParams(window.location.search) @@ -31,10 +40,18 @@ function Page({ globalSections }: Props) { return ( + {/* + WARNING: Do not import or render components from any + other folder than '../components/sections' in here. + + This is necessary to keep the integration with the CMS + easy and consistent, enabling the change and reorder + of elements on this page. - - - + If needed, wrap your component in a
component + (not the HTML tag) before rendering it here. + */} + ) } @@ -44,10 +61,16 @@ export const getStaticProps: GetStaticProps< Record, Locator > = async ({ previewData }) => { - const globalSections = await getGlobalSectionsData(previewData) + const [page, globalSections] = await Promise.all([ + getPage({ + ...(previewData?.contentType === 'login' && previewData), + contentType: 'login', + }), + getGlobalSectionsData(previewData), + ]) return { - props: { globalSections }, + props: { page, globalSections }, } } diff --git a/packages/core/src/typings/overrides.ts b/packages/core/src/typings/overrides.ts index 387c25838b..3aa5bb8bdd 100644 --- a/packages/core/src/typings/overrides.ts +++ b/packages/core/src/typings/overrides.ts @@ -49,6 +49,7 @@ import Alert from '../components/sections/Alert' import Breadcrumb from '../components/sections/Breadcrumb' import BannerText from '../components/sections/BannerText' import CrossSellingShelf from '../components/sections/CrossSellingShelf' +import EmptyState from '../components/sections/EmptyState' import Hero from '../components/sections/Hero' import ProductShelf from '../components/sections/ProductShelf' import ProductDetails from '../components/sections/ProductDetails' @@ -137,7 +138,7 @@ export type SectionsOverrides = { } } EmptyState: { - Section: never + Section: typeof EmptyState components: { EmptyState: ComponentOverrideDefinition< PropsWithChildren, @@ -154,7 +155,7 @@ export type SectionsOverrides = { } } Navbar: { - Section: typeof Navbar, + Section: typeof Navbar components: { Navbar: ComponentOverrideDefinition NavbarLinks: ComponentOverrideDefinition<