diff --git a/app/[locale]/[[...slug]]/page.tsx b/app/[locale]/[[...slug]]/page.tsx index 7d8658f..82e579a 100644 --- a/app/[locale]/[[...slug]]/page.tsx +++ b/app/[locale]/[[...slug]]/page.tsx @@ -11,7 +11,7 @@ import { ComponentDuplexFieldsFragment } from '#/components/duplex-ctf/duplex-ct import { ComponentHeroBannerFieldsFragment } from '#/components/hero-banner-ctf/hero-banner-ctf'; import { LanguageDataSetter } from '#/components/language-data-provider/language-data-provider'; import { ComponentSEOFieldsFragment, getSeoMetadata } from '#/components/seo/seo-ctf'; -import { ComponentTopicBusinessInfoFieldsFragment } from '#/components/topic-business-info/topic-business-info'; +import { TopicBusinessInfoFieldsFragment } from '#/components/topic-business-info/topic-business-info'; import { addContentSourceMaps } from '#/lib/contentSourceMaps'; import { graphqlClient } from '#/lib/graphqlClient'; import { getLocaleFromPath } from '#/locales/get-locale-from-path'; @@ -29,12 +29,14 @@ const getPage = async (slug: string, locale: string, preview = false) => { items { topSectionCollection(limit: 10) { items { + __typename ...ComponentHeroBannerFields ...ComponentDuplexFields } } pageContent { - ...ComponentTopicBusinessInfo + __typename + ...TopicBusinessInfo } slugEn: slug(locale: "en-US") slugDe: slug(locale: "de-DE") @@ -42,7 +44,7 @@ const getPage = async (slug: string, locale: string, preview = false) => { } } `, - [ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, ComponentTopicBusinessInfoFieldsFragment] + [ComponentHeroBannerFieldsFragment, ComponentDuplexFieldsFragment, TopicBusinessInfoFieldsFragment] ); const response = await graphqlClient(preview).query(pageQuery, { diff --git a/cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}} b/cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}} index 19b2e93..2d03fad 100644 --- a/cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}} +++ b/cli/scaffolds/components/{{hyphenCase name}}-client.{{ext}} @@ -2,16 +2,16 @@ import { ResultOf } from 'gql.tada'; -import { Component{{pascalCase name}}FieldsFragment } from '#/components/{{hyphenCase name}}/{{hyphenCase name}}'; +import { {{pascalCase name}}Fragment } from '#/components/{{hyphenCase name}}/{{hyphenCase name}}'; import { {{pascalCase name}} } from '#/components/ui/{{hyphenCase name}}'; import { useComponentPreview } from '../hooks/use-component-preview'; export const {{pascalCase name}}Client: React.FC<{ - data: ResultOf; + data: ResultOf; }> = (props) => { const { data: originalData } = props; - const { data, addAttributes } = useComponentPreview(originalData); + const { data, addAttributes } = useComponentPreview(originalData); return <{{pascalCase name}} id={data.sys.id} addAttributes={addAttributes} />; }; diff --git a/cli/scaffolds/components/{{hyphenCase name}}.{{ext}} b/cli/scaffolds/components/{{hyphenCase name}}.{{ext}} index 4dac8c7..56c84f4 100644 --- a/cli/scaffolds/components/{{hyphenCase name}}.{{ext}} +++ b/cli/scaffolds/components/{{hyphenCase name}}.{{ext}} @@ -2,8 +2,8 @@ import { FragmentOf, graphql, readFragment } from 'gql.tada'; import { {{pascalCase name}}Client } from './{{hyphenCase name}}-client'; -export const Component{{pascalCase name}}FieldsFragment = graphql(` - fragment Component{{pascalCase name}} on {{pascalCase name}} { +export const {{pascalCase name}}Fragment = graphql(` + fragment {{pascalCase name}} on {{pascalCase name}} { __typename sys { id @@ -12,10 +12,10 @@ export const Component{{pascalCase name}}FieldsFragment = graphql(` `); export type {{pascalCase name}}Props = { - data: FragmentOf; + data: FragmentOf & Record; }; export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = (props) => { - const data = readFragment(Component{{pascalCase name}}FieldsFragment, props.data); + const data = readFragment({{pascalCase name}}Fragment, props.data); return <{{pascalCase name}}Client data={data} />; }; diff --git a/cli/utils.mjs b/cli/utils.mjs index 316ec62..a4bb8bb 100644 --- a/cli/utils.mjs +++ b/cli/utils.mjs @@ -59,7 +59,7 @@ export const scaffoldComponentFiles = (contentType, updateMappings) => { const config = { name: contentType, data: { - ext: 'tsx' + ext: 'tsx', }, templates: [path.join(__dirname, 'scaffolds', 'components'), path.join(__dirname, 'scaffolds', 'ui')], createSubFolder: true, @@ -82,11 +82,12 @@ export const scaffoldComponentFiles = (contentType, updateMappings) => { .then(() => { const mappingFilePath = path.join(__dirname, '../components/component-renderer/mappings.ts'); const mappings = fs.readFileSync(mappingFilePath, 'utf-8').split('\n'); - mappings.splice( - -2, - 0, - ` ${hyphenToPascal(contentType)}: dynamic(() => import(\'#/components/${contentType}\').then((mod) => mod.${hyphenToPascal(contentType)})),` + // Add component import to the top of the file. + mappings.unshift( + `import { ${hyphenToPascal(contentType)} } from '#/components/${contentType}/${contentType}';` ); + // Add the new component to the mappings file. + mappings.splice(-2, 0, ` ${hyphenToPascal(contentType)}: ${hyphenToPascal(contentType)},`); const updatedMappings = mappings.join('\n'); fs.writeFileSync(mappingFilePath, updatedMappings, { encoding: 'utf-8' }); }) diff --git a/components/component-renderer/component-renderer.tsx b/components/component-renderer/component-renderer.tsx index 11fc3e8..93f6f3e 100644 --- a/components/component-renderer/component-renderer.tsx +++ b/components/component-renderer/component-renderer.tsx @@ -1,34 +1,49 @@ +/** + * ComponentRenderer is a component that renders components based on the data supplied. + * + * A few ground principles for ComponentRenderer + * + * 1. It should accept data and decide which component(s) to render + * 2. It should allow arrays of data to be passed and handle null/undefined + * 3. As a proxy, it should let you know if it can't render the component you're asking because you didn't provide enough data. + * 4. It should be able to render the component(s) with the data you provided. + * 5. It should skip rendering if component is not found in the componentMap. + */ + import { componentMap } from './mappings'; +import { ComponentProps } from 'react'; + +type ComponentMapType = typeof componentMap; +type Data = ComponentProps['data']; +type ComponentKey = keyof ComponentMapType; +type DataWithTypename = (Data & { __typename: string }) | { __typename: string } | null; + +function isComponentKey(key: string): key is ComponentKey { + return key in componentMap; +} -export default function ComponentRenderer({ - data, -}: { - data: any; // @TODO: Fixme -}) { +export default function ComponentRenderer({ data }: { data: T }) { + if (data === null) { + return null; + } + + // If we have an array, we render each item in the array if (Array.isArray(data)) { return ( <> - {data.map((item) => { - if (!item?.sys?.id) { - return null; - } - - return ; + {data.map((item, index) => { + return ; })} ); } - if (!data?.__typename) { - return null; - } - - // @TODO: Fix typings for componentMap. - // @ts-ignore - const Component = componentMap[data.__typename]; - if (Component) { - return ; + if (isComponentKey(data.__typename)) { + const Component = componentMap[data.__typename]; + // At this point we know data is one of the accepted props for the component + return ; } + // If we don't know the component, we don't render anything return null; } diff --git a/components/component-renderer/mappings.ts b/components/component-renderer/mappings.ts index 55919ce..d9d5640 100644 --- a/components/component-renderer/mappings.ts +++ b/components/component-renderer/mappings.ts @@ -1,8 +1,11 @@ -import dynamic from 'next/dynamic'; +import { HeroBannerCtf } from '#/components/hero-banner-ctf/hero-banner-ctf'; +import { DuplexCtf } from '#/components/duplex-ctf/duplex-ctf'; +import { TopicBusinessInfo } from '#/components/topic-business-info/topic-business-info'; +import { TopicPerson } from '#/components/topic-person/topic-person'; export const componentMap = { - ComponentHeroBanner: dynamic(() => import('#/components/hero-banner-ctf').then((mod) => mod.HeroBannerCtf)), - ComponentDuplex: dynamic(() => import('#/components/duplex-ctf').then((mod) => mod.DuplexCtf)), - TopicBusinessInfo: dynamic(() => import('#/components/topic-business-info').then((mod) => mod.TopicBusinessInfo)), - TopicPersons: dynamic(() => import('#/components/topic-person').then((mod) => mod.TopicPerson)), -}; + ComponentHeroBanner: HeroBannerCtf, + ComponentDuplex: DuplexCtf, + TopicBusinessInfo: TopicBusinessInfo, + TopicPersons: TopicPerson, +} as const; diff --git a/components/duplex-ctf/duplex-ctf-client.tsx b/components/duplex-ctf/duplex-ctf-client.tsx index 7b3d52a..63fc0fe 100644 --- a/components/duplex-ctf/duplex-ctf-client.tsx +++ b/components/duplex-ctf/duplex-ctf-client.tsx @@ -35,7 +35,7 @@ export const DuplexCtfClient: React.FC<{ data: ResultOf; }> = (props) => { const { data: originalData } = props; - const { data, addAttributes } = useComponentPreview(originalData); + const { data, addAttributes } = useComponentPreview(originalData); const { track } = useAnalytics(); return ( diff --git a/components/duplex-ctf/duplex-ctf.tsx b/components/duplex-ctf/duplex-ctf.tsx index 62daefa..b75205b 100644 --- a/components/duplex-ctf/duplex-ctf.tsx +++ b/components/duplex-ctf/duplex-ctf.tsx @@ -31,7 +31,7 @@ export const ComponentDuplexFieldsFragment = graphql( ); export type DuplexProps = { - data: FragmentOf; + data: FragmentOf & Record; }; export const DuplexCtf: React.FC = (props) => { diff --git a/components/hero-banner-ctf/hero-banner-ctf-client.tsx b/components/hero-banner-ctf/hero-banner-ctf-client.tsx index 02dc82f..2b8bf4a 100644 --- a/components/hero-banner-ctf/hero-banner-ctf-client.tsx +++ b/components/hero-banner-ctf/hero-banner-ctf-client.tsx @@ -16,7 +16,7 @@ export const HeroBannerCtfClient: React.FC<{ data: ResultOf; }> = (props) => { const { data: originalData } = props; - const { data, addAttributes } = useComponentPreview(originalData); + const { data, addAttributes } = useComponentPreview(originalData); // We use createAnalyticsEvent helper to create typed event. const analyticsInViewEvent = createAnalyticsEvent('heroBannerViewed', { category: 'duplexViewed', diff --git a/components/hero-banner-ctf/hero-banner-ctf.tsx b/components/hero-banner-ctf/hero-banner-ctf.tsx index babbf3e..11c1ca3 100644 --- a/components/hero-banner-ctf/hero-banner-ctf.tsx +++ b/components/hero-banner-ctf/hero-banner-ctf.tsx @@ -38,7 +38,7 @@ export const ComponentHeroBannerFieldsFragment = graphql( ); export type HeroBannerProps = { - data: FragmentOf; + data: FragmentOf & Record; }; export const HeroBannerCtf: React.FC = (props) => { diff --git a/components/hooks/use-component-preview.ts b/components/hooks/use-component-preview.ts index 7e23bbf..9c346ff 100644 --- a/components/hooks/use-component-preview.ts +++ b/components/hooks/use-component-preview.ts @@ -1,9 +1,11 @@ 'use client'; -import { Argument } from '@contentful/live-preview/dist/types'; +import { Entity } from '@contentful/live-preview/dist/types'; import { useContentfulInspectorMode, useContentfulLiveUpdates } from '@contentful/live-preview/react'; -export const useComponentPreview = (data: (typeof useContentfulLiveUpdates)['arguments'][0]) => { +type Argument = Entity & { sys: { id: string } }; + +export const useComponentPreview = (data: T) => { const previewData = useContentfulLiveUpdates(data); const inspectorProps = useContentfulInspectorMode({ entryId: data.sys.id, diff --git a/components/topic-business-info/topic-business-info-client.tsx b/components/topic-business-info/topic-business-info-client.tsx index cf542a9..78301ae 100644 --- a/components/topic-business-info/topic-business-info-client.tsx +++ b/components/topic-business-info/topic-business-info-client.tsx @@ -7,13 +7,13 @@ import { RichTextCtf } from '#/components/rich-text-ctf'; import { TopicBusinessInfo } from '#/components/ui/topic-business-info'; import { useComponentPreview } from '../hooks/use-component-preview'; -import { ComponentTopicBusinessInfoFieldsFragment } from './topic-business-info'; +import { TopicBusinessInfoFieldsFragment } from './topic-business-info'; export const TopicBusinessInfoClient: React.FC<{ - data: ResultOf; + data: ResultOf; }> = (props) => { const { data: originalData } = props; - const { data, addAttributes } = useComponentPreview(originalData); + const { data, addAttributes } = useComponentPreview(originalData); return ( ; + data: FragmentOf>; }; export const TopicBusinessInfo: React.FC = (props) => { - const data = readFragment(ComponentTopicBusinessInfoFieldsFragment, props.data); + const data = readFragment(TopicBusinessInfoFieldsFragment, props.data); return ; }; diff --git a/components/topic-person/topic-person-client.tsx b/components/topic-person/topic-person-client.tsx index 67b6e5a..9068263 100644 --- a/components/topic-person/topic-person-client.tsx +++ b/components/topic-person/topic-person-client.tsx @@ -7,13 +7,13 @@ import { RichTextCtf } from '#/components/rich-text-ctf'; import { TopicPerson } from '#/components/ui/topic-person'; import { useComponentPreview } from '../hooks/use-component-preview'; -import { ComponentTopicPersonFieldsFragment } from '././topic-person'; +import { TopicPersonFieldsFragment } from '././topic-person'; export const TopicPersonClient: React.FC<{ - data: ResultOf; + data: ResultOf; }> = (props) => { const { data: originalData } = props; - const { data, addAttributes } = useComponentPreview(originalData); + const { data, addAttributes } = useComponentPreview(originalData); return ( ; +export type TopicPersonProps = { + data: FragmentOf>; }; -export const TopicPerson: React.FC = (props) => { - const data = readFragment(ComponentTopicPersonFieldsFragment, props.data); +export const TopicPerson: React.FC = (props) => { + const data = readFragment(TopicPersonFieldsFragment, props.data); return ; }; diff --git a/docs/components.md b/docs/components.md index 2dc1a18..2b6f446 100644 --- a/docs/components.md +++ b/docs/components.md @@ -257,7 +257,7 @@ Remember why we bother to create a Client Component in the first place? To get R ```jsx const { data: originalData } = props; -const { data, addAttributes } = useComponentPreview(originalData); +const { data, addAttributes } = useComponentPreview(originalData); return