mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 15:36:58 +00:00
support dynamic content on PLP
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
0fb7d0d3e5
commit
ebbc8f053c
@ -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>
|
||||
|
@ -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>
|
||||
)}
|
||||
|
38
components/page/mini-icon-block.tsx
Normal file
38
components/page/mini-icon-block.tsx
Normal 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;
|
@ -4,12 +4,18 @@ 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) => (
|
||||
icon_content_section: (block) =>
|
||||
block.mini ? (
|
||||
<Suspense>
|
||||
<MiniIconBlock block={block} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<Suspense fallback={<IconBlockPlaceholder />}>
|
||||
<IconWithTextBlock block={block} />
|
||||
</Suspense>
|
||||
|
@ -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} />);
|
||||
}
|
||||
|
31
components/plp/content.tsx
Normal file
31
components/plp/content.tsx
Normal 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;
|
28
components/plp/default-content.tsx
Normal file
28
components/plp/default-content.tsx
Normal 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;
|
20
components/plp/dynamic-content.tsx
Normal file
20
components/plp/dynamic-content.tsx
Normal 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;
|
4
components/plp/tab-components.tsx
Normal file
4
components/plp/tab-components.tsx
Normal 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
60
components/plp/tabs.tsx
Normal 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;
|
@ -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
1
lib/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type SearchParams = { [key: string]: string | string[] | undefined };
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user