support dynamic content on PLP

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-07-07 15:05:15 +07:00
parent 0fb7d0d3e5
commit ebbc8f053c
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
13 changed files with 211 additions and 9 deletions

View File

@ -21,6 +21,7 @@ import HelpfulLinks from 'components/layout/search/helpful-links';
import ProductsGridPlaceholder from 'components/layout/search/placeholder';
import SortingMenu from 'components/layout/search/sorting-menu';
import Models from 'components/models';
import Content from 'components/plp/content';
import TransmissionCode from 'components/transmission-codes';
import { Suspense } from 'react';
@ -135,6 +136,9 @@ export default async function CategorySearchPage(props: {
</div>
</div>
</div>
<Suspense>
<Content collection={collectionHandle} />
</Suspense>
<FAQ handle="plp-faqs" />
{collectionHandle.startsWith('transmissions') && (
<Suspense>

View File

@ -29,7 +29,7 @@ const AccordionBlock = ({
const accordionItemIds = JSON.parse(block.accordion || '[]') as string[];
return (
<div className="divide-y divide-gray-900/10 px-4 md:px-0">
<div className="w-full divide-y divide-gray-900/10 px-4 md:px-0">
{block.title && (
<h3 className="mb-7 text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
)}

View File

@ -0,0 +1,38 @@
import DynamicHeroIcon from 'components/hero-icon';
import { getMetaobjectsByIds } from 'lib/shopify';
import { Metaobject } from 'lib/shopify/types';
const MiniIconBlock = async ({ block }: { block: Metaobject }) => {
const contentIds = block.content ? JSON.parse(block.content) : [];
const contentBlocks = await getMetaobjectsByIds(contentIds);
if (!contentBlocks || contentBlocks.length === 0) {
return null;
}
return (
<div className="flex w-full flex-col gap-y-3">
{block.title ? (
<h3 className="text-xl font-semibold leading-6 text-gray-900">{block.title}</h3>
) : null}
{contentBlocks.map((content) => (
<div key={content.id} className="flex items-center gap-x-3">
{content.icon_name && (
<DynamicHeroIcon icon={content.icon_name} className="w-5 text-secondary" />
)}
{content.title && content.content && (
<div>
{content.title && (
<div className="text-sm font-medium text-black-700">{content.title}</div>
)}
{content.content && <p className="mt-2 text-sm text-black-700">{content.content}</p>}
</div>
)}
{content.title && <div className="text-sm text-black-700">{content.title}</div>}
</div>
))}
</div>
);
};
export default MiniIconBlock;

View File

@ -4,16 +4,22 @@ import AccordionBlock from './accordion-block';
import CategoryPreview, { CategoryPreviewPlaceholder } from './category-preview';
import IconWithTextBlock, { IconBlockPlaceholder } from './icon-with-text-block';
import ImageWithTextBlock from './image-with-text-block';
import MiniIconBlock from './mini-icon-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>
),
icon_content_section: (block) =>
block.mini ? (
<Suspense>
<MiniIconBlock block={block} />
</Suspense>
) : (
<Suspense fallback={<IconBlockPlaceholder />}>
<IconWithTextBlock block={block} />
</Suspense>
),
image: (block) => <ImageWithTextBlock block={block} />,
page_section: (block) => <TextBlock block={block} />,
accordion: (block) => <AccordionBlock block={block} />,

View File

@ -1,4 +1,5 @@
import clsx from 'clsx';
import Link from 'next/link';
type Text = {
type: 'text';
@ -14,7 +15,8 @@ type Content =
listType: 'bullet' | 'ordered' | 'unordered';
children: Array<{ type: 'listItem'; children: Text[] }>;
}
| { type: 'listItem'; children: Text[] };
| { type: 'listItem'; children: Text[] }
| { type: 'link'; children: Text[]; target: string; title: string; url: string };
const RichTextBlock = ({ block }: { block: Content }) => {
if (block.type === 'text') {
@ -25,6 +27,14 @@ const RichTextBlock = ({ block }: { block: Content }) => {
);
}
if (block.type === 'link') {
return (
<Link href={block.url} target={block.target} title={block.title} className="underline">
{block.children[0]?.value || block.title}
</Link>
);
}
if (block.type === 'listItem') {
return block.children.map((child, index) => <RichTextBlock key={index} block={child} />);
}

View File

@ -0,0 +1,31 @@
import { getMetaobject } from 'lib/shopify';
import DefaultContent from './default-content';
import DynamicContent from './dynamic-content';
const Content = async ({ collection }: { collection: string }) => {
const [lastSegment] = collection.split('_').slice(-1);
if (!lastSegment) {
return <DefaultContent />;
}
let content = null;
if (collection.startsWith('transmissions')) {
content = await getMetaobject({
handle: { handle: `transmission_code_${lastSegment}`, type: 'plp_content' }
});
} else if (collection.startsWith('engines')) {
content = await getMetaobject({
handle: { handle: `engine_size_${lastSegment}`, type: 'plp_content' }
});
}
if (!content) {
return <DefaultContent />;
}
return <DynamicContent content={content} />;
};
export default Content;

View File

@ -0,0 +1,28 @@
import Tag from 'components/tag';
import { getMetaobject } from 'lib/shopify';
import { Suspense } from 'react';
import Tabs, { TabsPlaceholder } from './tabs';
const DefaultContent = async () => {
const defaultPLPContent = await getMetaobject({
handle: { handle: 'default-plp-content', type: 'plp_content' }
});
if (!defaultPLPContent) return null;
const sectionIds = defaultPLPContent.sections ? JSON.parse(defaultPLPContent.sections) : [];
return (
<div className="mx-auto mt-6 max-w-screen-2xl px-8 pb-10">
<Tag text="Learn More" />
<h3 className="mb-5 text-3xl font-bold leading-loose text-black-700">
{defaultPLPContent.title}
</h3>
<Suspense fallback={<TabsPlaceholder />}>
<Tabs tabItemIds={sectionIds} />
</Suspense>
</div>
);
};
export default DefaultContent;

View File

@ -0,0 +1,20 @@
import Tag from 'components/tag';
import { Metaobject } from 'lib/shopify/types';
import { Suspense } from 'react';
import Tabs, { TabsPlaceholder } from './tabs';
const DynamicContent = async ({ content }: { content: Metaobject }) => {
const sectionIds = content.sections ? JSON.parse(content.sections) : [];
return (
<div className="mx-auto mt-6 max-w-screen-2xl px-8 pb-10">
<Tag text="Learn More" />
<h3 className="mb-5 text-3xl font-bold leading-loose text-black-700">{content.title}</h3>
<Suspense fallback={<TabsPlaceholder />}>
<Tabs tabItemIds={sectionIds} />
</Suspense>
</div>
);
};
export default DynamicContent;

View File

@ -0,0 +1,4 @@
'use client';
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react';
export { Tab, TabGroup, TabList, TabPanel, TabPanels };

60
components/plp/tabs.tsx Normal file
View File

@ -0,0 +1,60 @@
import { ChevronRightIcon } from '@heroicons/react/24/solid';
import PageContent from 'components/page/page-content';
import { getMetaobjectsByIds } from 'lib/shopify';
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from './tab-components';
const TabPanelContent = async ({ ids }: { ids: string[] }) => {
const content = await getMetaobjectsByIds(ids);
return (
<TabPanel className="flex min-w-full space-y-5">
{content.map((block) => (
<PageContent key={block.id} block={block} />
))}
</TabPanel>
);
};
const Tabs = async ({ tabItemIds }: { tabItemIds: string[] }) => {
const tabItems = await getMetaobjectsByIds(tabItemIds);
if (!tabItems || tabItems.length === 0) return null;
return (
<TabGroup vertical>
<div className="flex w-full gap-x-10">
<TabList className="flex shrink-0 basis-1/4 flex-col gap-2">
{tabItems.map((item) => (
<Tab
key={item.id}
className="flex items-center justify-between rounded-sm bg-gray-200/60 p-3 text-left text-sm font-medium text-black-700 focus:outline-none focus:ring-0 data-[selected]:bg-primary data-[selected]:text-white"
>
{item.title}
<ChevronRightIcon className="size-4" />
</Tab>
))}
</TabList>
<TabPanels className="flex basis-3/4">
{tabItems.map((item) => (
<TabPanelContent key={item.id} ids={item.content ? JSON.parse(item.content) : []} />
))}
</TabPanels>
</div>
</TabGroup>
);
};
export const TabsPlaceholder = () => {
return (
<div className="flex w-full gap-x-10">
<div className="flex shrink-0 basis-1/4 animate-pulse flex-col gap-2">
<div className="h-14 bg-gray-200/60" />
<div className="h-14 bg-gray-200/60" />
<div className="h-14 bg-gray-200/60" />
<div className="h-14 bg-gray-200/60" />
<div className="h-14 bg-gray-200/60" />
</div>
<div className="flex h-96 basis-3/4 animate-pulse rounded bg-gray-200/60" />
</div>
);
};
export default Tabs;

View File

@ -28,7 +28,7 @@ const TransmissionCode = async ({ collectionHandle }: { collectionHandle: string
<div className="mt-6 grid grid-cols-2 gap-x-12 gap-y-5 md:grid-cols-3 md:gap-y-8 lg:grid-cols-4 xl:grid-cols-5">
{transmissionCodes.values.map((transmissionCode) => (
<Link
href={`${getCollectionUrl(collectionHandle)}?${TRANSMISSION_CODE_FILTER_ID}=${transmissionCode.value}`}
href={`${getCollectionUrl(collectionHandle)}/${transmissionCode.label.toLowerCase()}`}
key={transmissionCode.id}
>
<div className="rounded border border-primary px-2 py-1 text-sm">

1
lib/types.ts Normal file
View File

@ -0,0 +1 @@
export type SearchParams = { [key: string]: string | string[] | undefined };

View File

@ -145,7 +145,7 @@ export const isBeforeToday = (date?: string | null) => {
};
export const getCollectionUrl = (handle: string, includeSlashPrefix = true) => {
const rewriteUrl = handle.split('_').filter(Boolean).join('/');
const rewriteUrl = handle.split('_').filter(Boolean).join('/').toLowerCase();
return includeSlashPrefix ? `/${rewriteUrl}` : rewriteUrl;
};