mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 23:46:58 +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:
parent
4684d54ac3
commit
a1d65a54c1
@ -1,15 +1,26 @@
|
|||||||
import Footer from 'components/layout/footer';
|
import Footer from 'components/layout/footer';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
const Placeholder = () => {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mb-2 w-full max-w-7xl animate-pulse py-6">
|
||||||
|
<div className="h-10 w-1/2 rounded bg-gray-200" />
|
||||||
|
<div className="mt-6 h-96 w-full rounded bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<>
|
||||||
<div className="w-full">
|
<div className="min-h-[600px] w-full">
|
||||||
<div className="mx-8 max-w-2xl py-20 sm:mx-auto">
|
<Suspense fallback={<Placeholder />}>
|
||||||
<Suspense>{children}</Suspense>
|
<div className="py-6">{children}</div>
|
||||||
</div>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Suspense>
|
||||||
</Suspense>
|
<Footer />
|
||||||
|
</Suspense>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,12 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
import Prose from 'components/prose';
|
import IconWithTextBlock, { IconBlockPlaceholder } from 'components/page/icon-with-text-block';
|
||||||
import { getPage } from 'lib/shopify';
|
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 { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
@ -26,22 +30,45 @@ 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 } }) {
|
export default async function Page({ params }: { params: { page: string } }) {
|
||||||
const page = await getPage(params.page);
|
const page = await getPage(params.page);
|
||||||
|
|
||||||
if (!page) return notFound();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h1 className="mb-8 text-5xl font-bold">{page.title}</h1>
|
<div className="mx-auto mb-2 max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
<Prose className="mb-8" html={page.body as string} />
|
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
||||||
<p className="text-sm italic">
|
{page.title}
|
||||||
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
|
</h1>
|
||||||
year: 'numeric',
|
</div>
|
||||||
month: 'long',
|
<main>
|
||||||
day: 'numeric'
|
<div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
|
||||||
}).format(new Date(page.updatedAt))}.`}
|
<div className="flex flex-col space-y-16">
|
||||||
</p>
|
{pageContents.map((content) => (
|
||||||
|
<div key={content.id}>{contentMap[content.key](content)}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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.
|
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
<div className="ml-0 flex flex-row items-center gap-2 md:ml-auto">
|
<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
|
||||||
<Image alt="mastercard" src="/icons/mastercard.png" width={30} height={20} />
|
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
|
<Image
|
||||||
alt="american-express"
|
alt="american-express"
|
||||||
src="/icons/american-express.png"
|
src="/icons/american-express.png"
|
||||||
width={30}
|
width={30}
|
||||||
height={20}
|
height={30}
|
||||||
|
className="h-auto w-[30px]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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;
|
@ -29,6 +29,7 @@ import {
|
|||||||
} from './queries/collection';
|
} from './queries/collection';
|
||||||
import { getMenuQuery } from './queries/menu';
|
import { getMenuQuery } from './queries/menu';
|
||||||
import { getMetaobjectsQuery } from './queries/metaobject';
|
import { getMetaobjectsQuery } from './queries/metaobject';
|
||||||
|
import { getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
||||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||||
import {
|
import {
|
||||||
getProductQuery,
|
getProductQuery,
|
||||||
@ -44,8 +45,10 @@ import {
|
|||||||
Menu,
|
Menu,
|
||||||
Metaobject,
|
Metaobject,
|
||||||
Money,
|
Money,
|
||||||
|
PAGE_TYPES,
|
||||||
Page,
|
Page,
|
||||||
PageInfo,
|
PageInfo,
|
||||||
|
PageMetafield,
|
||||||
Product,
|
Product,
|
||||||
ProductVariant,
|
ProductVariant,
|
||||||
ShopifyAddToCartOperation,
|
ShopifyAddToCartOperation,
|
||||||
@ -57,8 +60,10 @@ import {
|
|||||||
ShopifyCollectionsOperation,
|
ShopifyCollectionsOperation,
|
||||||
ShopifyCreateCartOperation,
|
ShopifyCreateCartOperation,
|
||||||
ShopifyFilter,
|
ShopifyFilter,
|
||||||
|
ShopifyImageOperation,
|
||||||
ShopifyMenuOperation,
|
ShopifyMenuOperation,
|
||||||
ShopifyMetaobject,
|
ShopifyMetaobject,
|
||||||
|
ShopifyMetaobjectOperation,
|
||||||
ShopifyMetaobjectsOperation,
|
ShopifyMetaobjectsOperation,
|
||||||
ShopifyPageOperation,
|
ShopifyPageOperation,
|
||||||
ShopifyPagesOperation,
|
ShopifyPagesOperation,
|
||||||
@ -490,10 +495,36 @@ export async function getMetaobjects(type: string) {
|
|||||||
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getMetaobjectsByIds(ids: string[]) {
|
||||||
|
if (!ids.length) return [];
|
||||||
|
|
||||||
|
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
|
||||||
|
query: getMetaobjectsByIdsQuery,
|
||||||
|
variables: { ids }
|
||||||
|
});
|
||||||
|
|
||||||
|
return reshapeMetaobjects(res.body.data.nodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPageMetaObjects(metafield: PageMetafield) {
|
||||||
|
let metaobjectIds = parseMetaFieldValue<string | string[]>(metafield) || metafield.value;
|
||||||
|
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPage(handle: string): Promise<Page> {
|
export async function getPage(handle: string): Promise<Page> {
|
||||||
|
const metafieldIdentifiers = PAGE_TYPES.map((key) => ({ key, namespace: 'custom' }));
|
||||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||||
query: getPageQuery,
|
query: getPageQuery,
|
||||||
variables: { handle }
|
variables: { handle, metafieldIdentifiers }
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.body.data.pageByHandle;
|
return res.body.data.pageByHandle;
|
||||||
@ -604,3 +635,12 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
|||||||
|
|
||||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getImage = async (id: string): Promise<Image> => {
|
||||||
|
const res = await shopifyFetch<ShopifyImageOperation>({
|
||||||
|
query: getImageQuery,
|
||||||
|
variables: { id }
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.body.data.node.image;
|
||||||
|
};
|
||||||
|
33
lib/shopify/queries/node.ts
Normal file
33
lib/shopify/queries/node.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export const getImageQuery = /* GraphQL */ `
|
||||||
|
query getImage($id: ID!) {
|
||||||
|
node(id: $id) {
|
||||||
|
... on MediaImage {
|
||||||
|
image {
|
||||||
|
altText
|
||||||
|
width
|
||||||
|
height
|
||||||
|
url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const getMetaobjectsByIdsQuery = /* GraphQL */ `
|
||||||
|
query getMetaobjectsByIds($ids: [ID!]!) {
|
||||||
|
nodes(ids: $ids) {
|
||||||
|
... on Metaobject {
|
||||||
|
id
|
||||||
|
fields {
|
||||||
|
reference {
|
||||||
|
... on Metaobject {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
key
|
||||||
|
value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
@ -19,9 +19,14 @@ const pageFragment = /* GraphQL */ `
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const getPageQuery = /* GraphQL */ `
|
export const getPageQuery = /* GraphQL */ `
|
||||||
query getPage($handle: String!) {
|
query getPage($handle: String!, $metafieldIdentifiers: [HasMetafieldsIdentifier!]!) {
|
||||||
pageByHandle(handle: $handle) {
|
pageByHandle(handle: $handle) {
|
||||||
...page
|
...page
|
||||||
|
metafields(identifiers: $metafieldIdentifiers) {
|
||||||
|
value
|
||||||
|
key
|
||||||
|
id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
${pageFragment}
|
${pageFragment}
|
||||||
|
@ -51,6 +51,15 @@ export type Money = {
|
|||||||
currencyCode: string;
|
currencyCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 type Page = {
|
export type Page = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@ -60,6 +69,12 @@ export type Page = {
|
|||||||
seo?: SEO;
|
seo?: SEO;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
metafields: PageMetafield[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MetafieldIdentifier = {
|
||||||
|
key: string;
|
||||||
|
namespace: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyMetaobject = {
|
export type ShopifyMetaobject = {
|
||||||
@ -282,7 +297,12 @@ export type ShopifyMenuOperation = {
|
|||||||
|
|
||||||
export type ShopifyPageOperation = {
|
export type ShopifyPageOperation = {
|
||||||
data: { pageByHandle: Page };
|
data: { pageByHandle: Page };
|
||||||
variables: { handle: string };
|
variables: { handle: string; metafieldIdentifiers: MetafieldIdentifier[] };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ShopifyImageOperation = {
|
||||||
|
data: { node: { image: Image } };
|
||||||
|
variables: { id: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ShopifyMetaobjectsOperation = {
|
export type ShopifyMetaobjectsOperation = {
|
||||||
@ -296,6 +316,11 @@ export type ShopifyPagesOperation = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ShopifyMetaobjectOperation = {
|
||||||
|
data: { nodes: ShopifyMetaobject[] };
|
||||||
|
variables: { ids: string[] };
|
||||||
|
};
|
||||||
|
|
||||||
export type ShopifyProductOperation = {
|
export type ShopifyProductOperation = {
|
||||||
data: { product: ShopifyProduct };
|
data: { product: ShopifyProduct };
|
||||||
variables: {
|
variables: {
|
||||||
@ -363,3 +388,13 @@ export type Filter = {
|
|||||||
value: unknown;
|
value: unknown;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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];
|
||||||
|
@ -28,6 +28,7 @@
|
|||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"geist": "^1.3.0",
|
"geist": "^1.3.0",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
|
"lodash.startcase": "^4.4.0",
|
||||||
"next": "14.1.4",
|
"next": "14.1.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
"@tailwindcss/typography": "^0.5.11",
|
"@tailwindcss/typography": "^0.5.11",
|
||||||
"@types/lodash.get": "^4.4.9",
|
"@types/lodash.get": "^4.4.9",
|
||||||
|
"@types/lodash.startcase": "^4.4.9",
|
||||||
"@types/node": "20.11.30",
|
"@types/node": "20.11.30",
|
||||||
"@types/react": "18.2.72",
|
"@types/react": "18.2.72",
|
||||||
"@types/react-dom": "18.2.22",
|
"@types/react-dom": "18.2.22",
|
||||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ dependencies:
|
|||||||
lodash.get:
|
lodash.get:
|
||||||
specifier: ^4.4.2
|
specifier: ^4.4.2
|
||||||
version: 4.4.2
|
version: 4.4.2
|
||||||
|
lodash.startcase:
|
||||||
|
specifier: ^4.4.0
|
||||||
|
version: 4.4.0
|
||||||
next:
|
next:
|
||||||
specifier: 14.1.4
|
specifier: 14.1.4
|
||||||
version: 14.1.4(react-dom@18.2.0)(react@18.2.0)
|
version: 14.1.4(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -55,6 +58,9 @@ devDependencies:
|
|||||||
'@types/lodash.get':
|
'@types/lodash.get':
|
||||||
specifier: ^4.4.9
|
specifier: ^4.4.9
|
||||||
version: 4.4.9
|
version: 4.4.9
|
||||||
|
'@types/lodash.startcase':
|
||||||
|
specifier: ^4.4.9
|
||||||
|
version: 4.4.9
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: 20.11.30
|
specifier: 20.11.30
|
||||||
version: 20.11.30
|
version: 20.11.30
|
||||||
@ -753,6 +759,12 @@ packages:
|
|||||||
'@types/lodash': 4.17.1
|
'@types/lodash': 4.17.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/lodash.startcase@4.4.9:
|
||||||
|
resolution: {integrity: sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==}
|
||||||
|
dependencies:
|
||||||
|
'@types/lodash': 4.17.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/lodash@4.17.1:
|
/@types/lodash@4.17.1:
|
||||||
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
|
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
|
||||||
dev: true
|
dev: true
|
||||||
@ -2632,6 +2644,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.startcase@4.4.0:
|
||||||
|
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/log-update@6.0.0:
|
/log-update@6.0.0:
|
||||||
resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==}
|
resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user