feat: implement accordion content type

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-05-24 13:19:14 +07:00
parent a1d65a54c1
commit e0da620ac9
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
13 changed files with 227 additions and 123 deletions

View File

@ -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<PageMetafieldKey, (content: PageContent) => JSX.Element> = {
page_icon_section: (content) => (
<Suspense fallback={<IconBlockPlaceholder />}>
<IconWithTextBlock content={content} />
</Suspense>
),
page_image_content: (content) => <ImageWithTextBlock content={content} />,
page_section: (content) => <TextBlock content={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<PageContent | null>).value)
.filter(Boolean) as PageContent[];
return (
<>
<div className="mx-auto mb-2 max-w-7xl px-4 sm:px-6 lg:px-8">
@ -63,8 +41,10 @@ export default async function Page({ params }: { params: { page: string } }) {
<main>
<div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
<div className="flex flex-col space-y-16">
{pageContents.map((content) => (
<div key={content.id}>{contentMap[content.key](content)}</div>
{page.metaobjects?.map((content) => (
<div key={content.id}>
<PageContent block={content} />
</div>
))}
</div>
</div>

View 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;

View 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;

View File

@ -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>

View File

@ -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>
);
};

View 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;

View File

@ -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) => (

View File

@ -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>
);
};

View File

@ -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<ShopifyMetaobjectOperation>({
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<string | string[]>(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<Page> {
const metafieldIdentifiers = PAGE_TYPES.map((key) => ({ key, namespace: 'custom' }));
const res = await shopifyFetch<ShopifyPageOperation>({
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<string[]>(page.metafield) || [];
const metaobjects = await getMetaobjectsByIds(metaobjectIds);
const { metafield, ...restPage } = page;
return { ...restPage, metaobjects };
}
return page;
}
export async function getPages(): Promise<Page[]> {
export async function getPages(): Promise<ShopifyPage[]> {
const res = await shopifyFetch<ShopifyPagesOperation>({
query: getPagesQuery
});

View File

@ -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
}
}
}
`;

View File

@ -18,6 +18,7 @@ export const getMetaobjectsByIdsQuery = /* GraphQL */ `
nodes(ids: $ids) {
... on Metaobject {
id
type
fields {
reference {
... on Metaobject {

View File

@ -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
}
}

View File

@ -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<ShopifyPage, 'metafield'> & {
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<Page>;
pages: Connection<ShopifyPage>;
};
};
@ -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];