mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
feat: implement accordion content type
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
23
components/page/accordion-block-item.tsx
Normal file
23
components/page/accordion-block-item.tsx
Normal file
@@ -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 (
|
||||
<Disclosure as="div" className="pt-6">
|
||||
<dt>
|
||||
<DisclosureButton className="group flex w-full items-start justify-between text-left text-gray-900">
|
||||
<span className="text-lg font-semibold leading-7">{title}</span>
|
||||
<ChevronDownIcon className="size-5 group-data-[open]:rotate-180" />
|
||||
</DisclosureButton>
|
||||
</dt>
|
||||
<DisclosurePanel as="dd" className="mt-2 flex flex-col gap-4 py-4 text-base text-gray-800">
|
||||
{children}
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionBlockItem;
|
39
components/page/accordion-block.tsx
Normal file
39
components/page/accordion-block.tsx
Normal file
@@ -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 (
|
||||
<AccordionBlockItem title={accordionObject.title || 'Section Title'}>
|
||||
{content.map((block) => (
|
||||
<PageContent block={block} key={block.id} />
|
||||
))}
|
||||
</AccordionBlockItem>
|
||||
);
|
||||
};
|
||||
|
||||
const AccordionBlock = async ({ block }: { block: Metaobject }) => {
|
||||
const accordionItemIds = JSON.parse(block.accordion || '[]') as string[];
|
||||
|
||||
return (
|
||||
<div className="divide-y divide-gray-900/10">
|
||||
{block.title && (
|
||||
<h3 className="mb-7 text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
|
||||
)}
|
||||
<dl className="w-full space-y-6 divide-y divide-gray-900/10">
|
||||
{accordionItemIds.map((id) => (
|
||||
<AccordionItem key={id} id={id} />
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccordionBlock;
|
@@ -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 (
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0">
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
{block.title ? (
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
|
||||
) : null}
|
||||
|
||||
<Grid className={validClassnames}>
|
||||
{contentBlocks.map((block) => (
|
||||
<Grid.Item key={block.id} className="flex flex-col gap-2">
|
||||
{block.icon_name && (
|
||||
<DynamicHeroIcon icon={block.icon_name} className="w-16 text-secondary" />
|
||||
)}
|
||||
<div className="text-lg font-medium">{block.title}</div>
|
||||
<p className="text-base text-gray-800">{block.content}</p>
|
||||
{block.title && <div className="text-lg font-medium">{block.title}</div>}
|
||||
{block.content && <p className="text-base text-gray-800">{block.content}</p>}
|
||||
</Grid.Item>
|
||||
))}
|
||||
</Grid>
|
||||
|
@@ -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 (
|
||||
<div className="flex flex-col gap-10">
|
||||
{content.metaobjects.map((metaobject) => {
|
||||
const contentBlocks = JSON.parse(metaobject.description || '{}');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-4 md:px-0" key={metaobject.id}>
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<div className="relative col-span-1">
|
||||
<Suspense>
|
||||
<ImageDisplay
|
||||
title={metaobject.title as string}
|
||||
fileId={metaobject.file as string}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<RichTextDisplay contentBlocks={contentBlocks.children} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 px-4 md:px-0">
|
||||
{block.title && (
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
|
||||
)}
|
||||
{description ? (
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<div className="relative col-span-1">
|
||||
<Suspense>
|
||||
<ImageDisplay title={block.title || 'Image Preview'} fileId={block.file as string} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="col-span-2">
|
||||
<RichTextDisplay contentBlocks={description.children} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative w-full">
|
||||
<Suspense>
|
||||
<ImageDisplay title={block.title || 'Image Preview'} fileId={block.file as string} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
24
components/page/page-content.tsx
Normal file
24
components/page/page-content.tsx
Normal file
@@ -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<PageType, (block: Metaobject) => JSX.Element> = {
|
||||
icon_content_section: (block) => (
|
||||
<Suspense fallback={<IconBlockPlaceholder />}>
|
||||
<IconWithTextBlock block={block} />
|
||||
</Suspense>
|
||||
),
|
||||
image: (block) => <ImageWithTextBlock block={block} />,
|
||||
page_section: (block) => <TextBlock block={block} />,
|
||||
accordion: (block) => <AccordionBlock block={block} />
|
||||
};
|
||||
|
||||
return contentMap[block.type as PageType](block);
|
||||
};
|
||||
|
||||
export default PageContent;
|
@@ -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) => <RichTextBlock key={index} block={child} />);
|
||||
}
|
||||
|
||||
if (block.type === 'list' && block.listType === 'ordered') {
|
||||
return (
|
||||
<ol className="ml-10 list-decimal">
|
||||
{block.children.map((child, index) => (
|
||||
<li key={index}>
|
||||
<RichTextBlock block={child} />
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-gray-800">
|
||||
{block.children.map((child, index) => (
|
||||
|
@@ -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 (
|
||||
<div className="flex flex-col gap-8">
|
||||
{content.metaobjects.map((metaobject) => {
|
||||
const contentBlocks = JSON.parse(metaobject.content || '{}');
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0">
|
||||
{block.title && (
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
|
||||
)}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0" key={metaobject.id}>
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
<RichTextDisplay contentBlocks={contentBlocks.children} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<RichTextDisplay contentBlocks={content.children} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user