diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx index c173ef197..0781a6edc 100644 --- a/app/[page]/page.tsx +++ b/app/[page]/page.tsx @@ -1,12 +1,8 @@ import type { Metadata } from 'next'; -import IconWithTextBlock, { IconBlockPlaceholder } from 'components/page/icon-with-text-block'; -import ImageWithTextBlock from 'components/page/image-with-text-block'; -import TextBlock from 'components/page/text-block'; -import { getPage, getPageMetaObjects } from 'lib/shopify'; -import { PageContent, PageMetafieldKey } from 'lib/shopify/types'; +import PageContent from 'components/page/page-content'; +import { getPage } from 'lib/shopify'; import { notFound } from 'next/navigation'; -import { Suspense } from 'react'; export const runtime = 'edge'; @@ -30,29 +26,11 @@ export async function generateMetadata({ }; } -// eslint-disable-next-line no-unused-vars -const contentMap: Record JSX.Element> = { - page_icon_section: (content) => ( - }> - - - ), - page_image_content: (content) => , - page_section: (content) => -}; - export default async function Page({ params }: { params: { page: string } }) { const page = await getPage(params.page); if (!page) return notFound(); - const pageContents = ( - await Promise.allSettled(page.metafields.map((metafield) => getPageMetaObjects(metafield))) - ) - .filter((result) => result.status === 'fulfilled') - .map((result) => (result as PromiseFulfilledResult).value) - .filter(Boolean) as PageContent[]; - return ( <>
@@ -63,8 +41,10 @@ export default async function Page({ params }: { params: { page: string } }) {
- {pageContents.map((content) => ( -
{contentMap[content.key](content)}
+ {page.metaobjects?.map((content) => ( +
+ +
))}
diff --git a/components/page/accordion-block-item.tsx b/components/page/accordion-block-item.tsx new file mode 100644 index 000000000..c426b895e --- /dev/null +++ b/components/page/accordion-block-item.tsx @@ -0,0 +1,23 @@ +'use client'; + +import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; +import { ChevronDownIcon } from '@heroicons/react/24/outline'; +import { ReactNode } from 'react'; + +const AccordionBlockItem = ({ title, children }: { title: string; children: ReactNode }) => { + return ( + +
+ + {title} + + +
+ + {children} + +
+ ); +}; + +export default AccordionBlockItem; diff --git a/components/page/accordion-block.tsx b/components/page/accordion-block.tsx new file mode 100644 index 000000000..f380b2377 --- /dev/null +++ b/components/page/accordion-block.tsx @@ -0,0 +1,39 @@ +import { getMetaobjectById, getMetaobjectsByIds } from 'lib/shopify'; +import { Metaobject } from 'lib/shopify/types'; +import AccordionBlockItem from './accordion-block-item'; +import PageContent from './page-content'; + +const AccordionItem = async ({ id }: { id: string }) => { + const accordionObject = await getMetaobjectById(id); + + if (!accordionObject) return null; + + const content = await getMetaobjectsByIds(JSON.parse(accordionObject.accordion_content || '[]')); + + return ( + + {content.map((block) => ( + + ))} + + ); +}; + +const AccordionBlock = async ({ block }: { block: Metaobject }) => { + const accordionItemIds = JSON.parse(block.accordion || '[]') as string[]; + + return ( +
+ {block.title && ( +

{block.title}

+ )} +
+ {accordionItemIds.map((id) => ( + + ))} +
+
+ ); +}; + +export default AccordionBlock; diff --git a/components/page/icon-with-text-block.tsx b/components/page/icon-with-text-block.tsx index bd200595d..2f05a7286 100644 --- a/components/page/icon-with-text-block.tsx +++ b/components/page/icon-with-text-block.tsx @@ -1,7 +1,7 @@ import Grid from 'components/grid'; import DynamicHeroIcon from 'components/hero-icon'; import { getMetaobjects, getMetaobjectsByIds } from 'lib/shopify'; -import { PageContent, ScreenSize } from 'lib/shopify/types'; +import { Metaobject, ScreenSize } from 'lib/shopify/types'; export const IconBlockPlaceholder = () => { return ( @@ -13,15 +13,10 @@ export const IconBlockPlaceholder = () => { ); }; -const IconWithTextBlock = async ({ content }: { content: PageContent }) => { - // for icon with text content, we only need the first metaobject as the array always contains only one element due to the metafield definition set up on Shopify - const metaobject = content.metaobjects[0]; - - if (!metaobject) return null; - +const IconWithTextBlock = async ({ block }: { block: Metaobject }) => { const [contentBlocks, layouts, screenSizes] = await Promise.all([ - getMetaobjectsByIds(metaobject.content ? JSON.parse(metaobject.content) : []), - getMetaobjectsByIds(metaobject.layouts ? JSON.parse(metaobject.layouts) : []), + getMetaobjectsByIds(block.content ? JSON.parse(block.content) : []), + getMetaobjectsByIds(block.layouts ? JSON.parse(block.layouts) : []), getMetaobjects('screen_sizes') ]); @@ -75,15 +70,18 @@ const IconWithTextBlock = async ({ content }: { content: PageContent }) => { return (
-

{metaobject.title}

+ {block.title ? ( +

{block.title}

+ ) : null} + {contentBlocks.map((block) => ( {block.icon_name && ( )} -
{block.title}
-

{block.content}

+ {block.title &&
{block.title}
} + {block.content &&

{block.content}

}
))}
diff --git a/components/page/image-with-text-block.tsx b/components/page/image-with-text-block.tsx index c9ae5422f..d48caf09a 100644 --- a/components/page/image-with-text-block.tsx +++ b/components/page/image-with-text-block.tsx @@ -1,35 +1,34 @@ -import { PageContent } from 'lib/shopify/types'; +import { Metaobject } from 'lib/shopify/types'; import { Suspense } from 'react'; import ImageDisplay from './image-display'; import RichTextDisplay from './rich-text-display'; -const ImageWithTextBlock = ({ content }: { content: PageContent }) => { - if (!content.metaobjects.length) return null; +const ImageWithTextBlock = ({ block }: { block: Metaobject }) => { + const description = block.description ? JSON.parse(block.description) : null; return ( -
- {content.metaobjects.map((metaobject) => { - const contentBlocks = JSON.parse(metaobject.description || '{}'); - - return ( -
-

{metaobject.title}

-
-
- - - -
-
- -
-
+
+ {block.title && ( +

{block.title}

+ )} + {description ? ( +
+
+ + +
- ); - })} +
+ +
+
+ ) : ( +
+ + + +
+ )}
); }; diff --git a/components/page/page-content.tsx b/components/page/page-content.tsx new file mode 100644 index 000000000..6aa321d21 --- /dev/null +++ b/components/page/page-content.tsx @@ -0,0 +1,24 @@ +import { Metaobject, PageType } from 'lib/shopify/types'; +import { Suspense } from 'react'; +import AccordionBlock from './accordion-block'; +import IconWithTextBlock, { IconBlockPlaceholder } from './icon-with-text-block'; +import ImageWithTextBlock from './image-with-text-block'; +import TextBlock from './text-block'; + +const PageContent = ({ block }: { block: Metaobject }) => { + // eslint-disable-next-line no-unused-vars + const contentMap: Record JSX.Element> = { + icon_content_section: (block) => ( + }> + + + ), + image: (block) => , + page_section: (block) => , + accordion: (block) => + }; + + return contentMap[block.type as PageType](block); +}; + +export default PageContent; diff --git a/components/page/rich-text-display.tsx b/components/page/rich-text-display.tsx index 56e9cea6f..4695953c3 100644 --- a/components/page/rich-text-display.tsx +++ b/components/page/rich-text-display.tsx @@ -1,10 +1,18 @@ +type Text = { + type: 'text'; + value: string; + bold?: boolean; +}; + type Content = - | { type: 'paragraph'; children: Array<{ type: 'text'; value: string; bold?: boolean }> } + | { type: 'paragraph'; children: Text[] } + | Text | { - type: 'text'; - value: string; - bold?: boolean; - }; + type: 'list'; + listType: 'bullet' | 'ordered'; + children: Array<{ type: 'listItem'; children: Text[] }>; + } + | { type: 'listItem'; children: Text[] }; const RichTextBlock = ({ block }: { block: Content }) => { if (block.type === 'text') { @@ -15,6 +23,22 @@ const RichTextBlock = ({ block }: { block: Content }) => { ); } + if (block.type === 'listItem') { + return block.children.map((child, index) => ); + } + + if (block.type === 'list' && block.listType === 'ordered') { + return ( +
    + {block.children.map((child, index) => ( +
  1. + +
  2. + ))} +
+ ); + } + return (

{block.children.map((child, index) => ( diff --git a/components/page/text-block.tsx b/components/page/text-block.tsx index ae5a15a78..545aacc84 100644 --- a/components/page/text-block.tsx +++ b/components/page/text-block.tsx @@ -1,21 +1,18 @@ -import { PageContent } from 'lib/shopify/types'; +import { Metaobject } from 'lib/shopify/types'; import RichTextDisplay from './rich-text-display'; -const TextBlock = ({ content }: { content: PageContent }) => { - if (!content.metaobjects.length) return null; +const TextBlock = ({ block }: { block: Metaobject }) => { + const content = JSON.parse(block.content || '{}'); return (

- {content.metaobjects.map((metaobject) => { - const contentBlocks = JSON.parse(metaobject.content || '{}'); +
+ {block.title && ( +

{block.title}

+ )} - return ( -
-

{metaobject.title}

- -
- ); - })} + +
); }; diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts index 1325915de..a9289062a 100644 --- a/lib/shopify/index.ts +++ b/lib/shopify/index.ts @@ -28,7 +28,7 @@ import { getCollectionsQuery } from './queries/collection'; import { getMenuQuery } from './queries/menu'; -import { getMetaobjectsQuery } from './queries/metaobject'; +import { getMetaobjectQuery, getMetaobjectsQuery } from './queries/metaobject'; import { getImageQuery, getMetaobjectsByIdsQuery } from './queries/node'; import { getPageQuery, getPagesQuery } from './queries/page'; import { @@ -45,10 +45,8 @@ import { Menu, Metaobject, Money, - PAGE_TYPES, Page, PageInfo, - PageMetafield, Product, ProductVariant, ShopifyAddToCartOperation, @@ -63,8 +61,8 @@ import { ShopifyImageOperation, ShopifyMenuOperation, ShopifyMetaobject, - ShopifyMetaobjectOperation, ShopifyMetaobjectsOperation, + ShopifyPage, ShopifyPageOperation, ShopifyPagesOperation, ShopifyProduct, @@ -238,7 +236,7 @@ const reshapeFilters = (filters: ShopifyFilter[]): Filter[] => { }; const reshapeMetaobjects = (metaobjects: ShopifyMetaobject[]): Metaobject[] => { - return metaobjects.map(({ fields, id }) => { + return metaobjects.map(({ fields, id, type }) => { const groupedFieldsByKey = fields.reduce( (acc, field) => { return { @@ -256,7 +254,7 @@ const reshapeMetaobjects = (metaobjects: ShopifyMetaobject[]): Metaobject[] => { } ); - return { id, ...groupedFieldsByKey }; + return { id, type, ...groupedFieldsByKey }; }); }; @@ -498,7 +496,10 @@ export async function getMetaobjects(type: string) { export async function getMetaobjectsByIds(ids: string[]) { if (!ids.length) return []; - const res = await shopifyFetch({ + const res = await shopifyFetch<{ + data: { nodes: ShopifyMetaobject[] }; + variables: { ids: string[] }; + }>({ query: getMetaobjectsByIdsQuery, variables: { ids } }); @@ -506,31 +507,39 @@ export async function getMetaobjectsByIds(ids: string[]) { return reshapeMetaobjects(res.body.data.nodes); } -export async function getPageMetaObjects(metafield: PageMetafield) { - let metaobjectIds = parseMetaFieldValue(metafield) || metafield.value; +export async function getMetaobjectById(id: string) { + const res = await shopifyFetch<{ + data: { metaobject: ShopifyMetaobject }; + variables: { id: string }; + }>({ + query: getMetaobjectQuery, + variables: { id } + }); - if (!metaobjectIds) { - return null; - } - - metaobjectIds = (Array.isArray(metaobjectIds) ? metaobjectIds : [metaobjectIds]) as string[]; - - const metaobjects = await getMetaobjectsByIds(metaobjectIds); - - return { metaobjects, id: metafield.id, key: metafield.key }; + return res.body.data.metaobject ? reshapeMetaobjects([res.body.data.metaobject])[0] : null; } export async function getPage(handle: string): Promise { - const metafieldIdentifiers = PAGE_TYPES.map((key) => ({ key, namespace: 'custom' })); const res = await shopifyFetch({ query: getPageQuery, - variables: { handle, metafieldIdentifiers } + variables: { handle, key: 'page_content', namespace: 'custom' } }); - return res.body.data.pageByHandle; + const page = res.body.data.pageByHandle; + + if (page.metafield) { + const metaobjectIds = parseMetaFieldValue(page.metafield) || []; + + const metaobjects = await getMetaobjectsByIds(metaobjectIds); + + const { metafield, ...restPage } = page; + return { ...restPage, metaobjects }; + } + + return page; } -export async function getPages(): Promise { +export async function getPages(): Promise { const res = await shopifyFetch({ query: getPagesQuery }); diff --git a/lib/shopify/queries/metaobject.ts b/lib/shopify/queries/metaobject.ts index 03b463b46..053b0cca4 100644 --- a/lib/shopify/queries/metaobject.ts +++ b/lib/shopify/queries/metaobject.ts @@ -18,3 +18,21 @@ export const getMetaobjectsQuery = /* GraphQL */ ` } } `; + +export const getMetaobjectQuery = /* GraphQL */ ` + query getMetaobject($id: ID!) { + metaobject(id: $id) { + id + type + fields { + reference { + ... on Metaobject { + id + } + } + key + value + } + } + } +`; diff --git a/lib/shopify/queries/node.ts b/lib/shopify/queries/node.ts index 9adaa5556..0a53ff934 100644 --- a/lib/shopify/queries/node.ts +++ b/lib/shopify/queries/node.ts @@ -18,6 +18,7 @@ export const getMetaobjectsByIdsQuery = /* GraphQL */ ` nodes(ids: $ids) { ... on Metaobject { id + type fields { reference { ... on Metaobject { diff --git a/lib/shopify/queries/page.ts b/lib/shopify/queries/page.ts index 179913095..7f33c8fbe 100644 --- a/lib/shopify/queries/page.ts +++ b/lib/shopify/queries/page.ts @@ -19,12 +19,11 @@ const pageFragment = /* GraphQL */ ` `; export const getPageQuery = /* GraphQL */ ` - query getPage($handle: String!, $metafieldIdentifiers: [HasMetafieldsIdentifier!]!) { + query getPage($handle: String!, $key: String!, $namespace: String) { pageByHandle(handle: $handle) { ...page - metafields(identifiers: $metafieldIdentifiers) { + metafield(key: $key, namespace: $namespace) { value - key id } } diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts index 2bb02dda5..4913b469c 100644 --- a/lib/shopify/types.ts +++ b/lib/shopify/types.ts @@ -53,14 +53,13 @@ export type Money = { export type PageMetafield = { id: string; - key: PageMetafieldKey; value: string; }; -export const PAGE_TYPES = ['page_icon_section', 'page_section', 'page_image_content'] as const; -export type PageMetafieldKey = (typeof PAGE_TYPES)[number]; +export const PAGE_TYPES = ['image', 'icon_content_section', 'page_section', 'accordion'] as const; +export type PageType = (typeof PAGE_TYPES)[number]; -export type Page = { +export type ShopifyPage = { id: string; title: string; handle: string; @@ -69,16 +68,15 @@ export type Page = { seo?: SEO; createdAt: string; updatedAt: string; - metafields: PageMetafield[]; + metafield: PageMetafield | null; }; -export type MetafieldIdentifier = { - key: string; - namespace: string; +export type Page = Omit & { + metaobjects?: Metaobject[]; }; - export type ShopifyMetaobject = { id: string; + type: string; fields: Array<{ key: string; value: string; @@ -90,6 +88,7 @@ export type ShopifyMetaobject = { export type Metaobject = { id: string; + type: string; [key: string]: string; }; @@ -296,8 +295,8 @@ export type ShopifyMenuOperation = { }; export type ShopifyPageOperation = { - data: { pageByHandle: Page }; - variables: { handle: string; metafieldIdentifiers: MetafieldIdentifier[] }; + data: { pageByHandle: ShopifyPage }; + variables: { handle: string; key: string; namespace: string }; }; export type ShopifyImageOperation = { @@ -312,7 +311,7 @@ export type ShopifyMetaobjectsOperation = { export type ShopifyPagesOperation = { data: { - pages: Connection; + pages: Connection; }; }; @@ -389,12 +388,6 @@ export type Filter = { }[]; }; -export type PageContent = { - id: string; - key: PageMetafieldKey; - metaobjects: Metaobject[]; -}; - export const SCREEN_SIZES = ['small', 'medium', 'large', 'extra_large'] as const; export type ScreenSize = (typeof SCREEN_SIZES)[number];