mirror of
https://github.com/vercel/commerce.git
synced 2025-07-25 11:11:24 +00:00
feat: implement text/image-with-text/icon-with-text content block
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
17
components/hero-icon.tsx
Normal file
17
components/hero-icon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import * as HeroIcons from '@heroicons/react/24/outline';
|
||||
import startcase from 'lodash.startcase';
|
||||
|
||||
type IconName = keyof typeof HeroIcons;
|
||||
interface IconProps {
|
||||
icon: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DynamicHeroIcon = ({ icon, className }: IconProps) => {
|
||||
const _icon = startcase(icon).replace(/\s/g, '');
|
||||
const SingleIcon = HeroIcons[`${_icon}Icon` as IconName];
|
||||
|
||||
return SingleIcon ? <SingleIcon className={className} /> : null;
|
||||
};
|
||||
|
||||
export default DynamicHeroIcon;
|
@@ -53,13 +53,26 @@ export default async function Footer() {
|
||||
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
|
||||
</p>
|
||||
<div className="ml-0 flex flex-row items-center gap-2 md:ml-auto">
|
||||
<Image alt="visa" src="/icons/visa.png" width={30} height={20} />
|
||||
<Image alt="mastercard" src="/icons/mastercard.png" width={30} height={20} />
|
||||
<Image
|
||||
alt="visa"
|
||||
src="/icons/visa.png"
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
<Image
|
||||
alt="mastercard"
|
||||
src="/icons/mastercard.png"
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
<Image
|
||||
alt="american-express"
|
||||
src="/icons/american-express.png"
|
||||
width={30}
|
||||
height={20}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
94
components/page/icon-with-text-block.tsx
Normal file
94
components/page/icon-with-text-block.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import Grid from 'components/grid';
|
||||
import DynamicHeroIcon from 'components/hero-icon';
|
||||
import { getMetaobjects, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import { PageContent, ScreenSize } from 'lib/shopify/types';
|
||||
|
||||
export const IconBlockPlaceholder = () => {
|
||||
return (
|
||||
<div className="flex animate-pulse flex-col gap-5 px-4 md:px-0">
|
||||
<div className="h-10 w-1/2 rounded bg-gray-200" />
|
||||
<div className="h-40 w-full rounded bg-gray-200" />
|
||||
<div className="h-40 w-full rounded bg-gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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 [contentBlocks, layouts, screenSizes] = await Promise.all([
|
||||
getMetaobjectsByIds(metaobject.content ? JSON.parse(metaobject.content) : []),
|
||||
getMetaobjectsByIds(metaobject.layouts ? JSON.parse(metaobject.layouts) : []),
|
||||
getMetaobjects('screen_sizes')
|
||||
]);
|
||||
|
||||
const availableLayouts = layouts.reduce(
|
||||
(acc, layout) => {
|
||||
const screenSize = screenSizes.find((screen) => screen.id === layout.screen_size);
|
||||
if (screenSize?.size) {
|
||||
acc[screenSize.size.toLowerCase() as ScreenSize] = Number(layout.number_of_columns);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ScreenSize, number>
|
||||
);
|
||||
|
||||
let classnames = {} as { [key: string]: boolean };
|
||||
|
||||
if (availableLayouts.small) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'sm:grid-cols-1': availableLayouts.small === 1,
|
||||
'sm:grid-cols-2': availableLayouts.small === 2,
|
||||
'sm:grid-cols-3': availableLayouts.small === 3,
|
||||
'sm:grid-cols-4': availableLayouts.small === 4
|
||||
};
|
||||
}
|
||||
|
||||
if (availableLayouts.medium) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'md:grid-cols-1': availableLayouts.medium === 1,
|
||||
'md:grid-cols-2': availableLayouts.medium === 2,
|
||||
'md:grid-cols-3': availableLayouts.medium === 3,
|
||||
'md:grid-cols-4': availableLayouts.medium === 4
|
||||
};
|
||||
}
|
||||
|
||||
if (availableLayouts.large) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'lg:grid-cols-1': availableLayouts.large === 1,
|
||||
'lg:grid-cols-2': availableLayouts.large === 2,
|
||||
'lg:grid-cols-3': availableLayouts.large === 3,
|
||||
'lg:grid-cols-4': availableLayouts.large === 4
|
||||
};
|
||||
}
|
||||
|
||||
const validClassnames = Object.keys(classnames)
|
||||
.filter((key) => classnames[key])
|
||||
.join(' ');
|
||||
|
||||
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>
|
||||
<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>
|
||||
</Grid.Item>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconWithTextBlock;
|
17
components/page/image-display.tsx
Normal file
17
components/page/image-display.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getImage } from 'lib/shopify';
|
||||
import Image from 'next/image';
|
||||
|
||||
const ImageDisplay = async ({ fileId, title }: { fileId: string; title: string }) => {
|
||||
const image = await getImage(fileId);
|
||||
return (
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.altText || `Display Image for ${title} section`}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDisplay;
|
37
components/page/image-with-text-block.tsx
Normal file
37
components/page/image-with-text-block.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { PageContent } 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;
|
||||
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWithTextBlock;
|
37
components/page/rich-text-display.tsx
Normal file
37
components/page/rich-text-display.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
type Content =
|
||||
| { type: 'paragraph'; children: Array<{ type: 'text'; value: string; bold?: boolean }> }
|
||||
| {
|
||||
type: 'text';
|
||||
value: string;
|
||||
bold?: boolean;
|
||||
};
|
||||
|
||||
const RichTextBlock = ({ block }: { block: Content }) => {
|
||||
if (block.type === 'text') {
|
||||
return block.bold ? (
|
||||
<strong className="font-semibold">{block.value}</strong>
|
||||
) : (
|
||||
<span>{block.value}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-gray-800">
|
||||
{block.children.map((child, index) => (
|
||||
<RichTextBlock key={index} block={child} />
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const RichTextDisplay = ({ contentBlocks }: { contentBlocks: Content[] }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{contentBlocks.map((block, index) => (
|
||||
<RichTextBlock key={index} block={block} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextDisplay;
|
23
components/page/text-block.tsx
Normal file
23
components/page/text-block.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { PageContent } from 'lib/shopify/types';
|
||||
import RichTextDisplay from './rich-text-display';
|
||||
|
||||
const TextBlock = ({ content }: { content: PageContent }) => {
|
||||
if (!content.metaobjects.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{content.metaobjects.map((metaobject) => {
|
||||
const contentBlocks = JSON.parse(metaobject.content || '{}');
|
||||
|
||||
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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextBlock;
|
Reference in New Issue
Block a user