-
- {children}
-
+ <>
+
-
-
+
+
+
+ >
);
}
diff --git a/app/[page]/page.tsx b/app/[page]/page.tsx
index c21525502..c173ef197 100644
--- a/app/[page]/page.tsx
+++ b/app/[page]/page.tsx
@@ -1,8 +1,12 @@
import type { Metadata } from 'next';
-import Prose from 'components/prose';
-import { getPage } from 'lib/shopify';
+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 { notFound } from 'next/navigation';
+import { Suspense } from 'react';
export const runtime = 'edge';
@@ -26,22 +30,45 @@ export async function generateMetadata({
};
}
+// eslint-disable-next-line no-unused-vars
+const contentMap: Record
JSX.Element> = {
+ page_icon_section: (content) => (
+ }>
+
+
+ ),
+ page_image_content: (content) => ,
+ page_section: (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).value)
+ .filter(Boolean) as PageContent[];
+
return (
<>
- {page.title}
-
-
- {`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
- year: 'numeric',
- month: 'long',
- day: 'numeric'
- }).format(new Date(page.updatedAt))}.`}
-
+
+
+ {page.title}
+
+
+
+
+
+ {pageContents.map((content) => (
+
{contentMap[content.key](content)}
+ ))}
+
+
+
>
);
}
diff --git a/components/hero-icon.tsx b/components/hero-icon.tsx
new file mode 100644
index 000000000..e23895bb6
--- /dev/null
+++ b/components/hero-icon.tsx
@@ -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 ? : null;
+};
+
+export default DynamicHeroIcon;
diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx
index 8211cb00e..ab88f767f 100644
--- a/components/layout/footer.tsx
+++ b/components/layout/footer.tsx
@@ -53,13 +53,26 @@ export default async function Footer() {
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
-
-
+
+
diff --git a/components/page/icon-with-text-block.tsx b/components/page/icon-with-text-block.tsx
new file mode 100644
index 000000000..bd200595d
--- /dev/null
+++ b/components/page/icon-with-text-block.tsx
@@ -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 (
+
+ );
+
+ 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 (
+
+
{metaobject.title}
+
+ {contentBlocks.map((block) => (
+
+ {block.icon_name && (
+
+ )}
+ {block.title}
+ {block.content}
+
+ ))}
+
+
+ );
+};
+
+export default IconWithTextBlock;
diff --git a/components/page/image-display.tsx b/components/page/image-display.tsx
new file mode 100644
index 000000000..b1b19cbdc
--- /dev/null
+++ b/components/page/image-display.tsx
@@ -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 (
+
+ );
+};
+
+export default ImageDisplay;
diff --git a/components/page/image-with-text-block.tsx b/components/page/image-with-text-block.tsx
new file mode 100644
index 000000000..c9ae5422f
--- /dev/null
+++ b/components/page/image-with-text-block.tsx
@@ -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 (
+
+ {content.metaobjects.map((metaobject) => {
+ const contentBlocks = JSON.parse(metaobject.description || '{}');
+
+ return (
+
+ );
+ })}
+
+ );
+};
+
+export default ImageWithTextBlock;
diff --git a/components/page/rich-text-display.tsx b/components/page/rich-text-display.tsx
new file mode 100644
index 000000000..56e9cea6f
--- /dev/null
+++ b/components/page/rich-text-display.tsx
@@ -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 ? (
+ {block.value}
+ ) : (
+ {block.value}
+ );
+ }
+
+ return (
+
+ {block.children.map((child, index) => (
+
+ ))}
+
+ );
+};
+
+const RichTextDisplay = ({ contentBlocks }: { contentBlocks: Content[] }) => {
+ return (
+
+ {contentBlocks.map((block, index) => (
+
+ ))}
+
+ );
+};
+
+export default RichTextDisplay;
diff --git a/components/page/text-block.tsx b/components/page/text-block.tsx
new file mode 100644
index 000000000..ae5a15a78
--- /dev/null
+++ b/components/page/text-block.tsx
@@ -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 (
+
+ {content.metaobjects.map((metaobject) => {
+ const contentBlocks = JSON.parse(metaobject.content || '{}');
+
+ return (
+
+
{metaobject.title}
+
+
+ );
+ })}
+
+ );
+};
+
+export default TextBlock;
diff --git a/lib/shopify/index.ts b/lib/shopify/index.ts
index 2d7c71dda..1325915de 100644
--- a/lib/shopify/index.ts
+++ b/lib/shopify/index.ts
@@ -29,6 +29,7 @@ import {
} from './queries/collection';
import { getMenuQuery } from './queries/menu';
import { getMetaobjectsQuery } from './queries/metaobject';
+import { getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
import { getPageQuery, getPagesQuery } from './queries/page';
import {
getProductQuery,
@@ -44,8 +45,10 @@ import {
Menu,
Metaobject,
Money,
+ PAGE_TYPES,
Page,
PageInfo,
+ PageMetafield,
Product,
ProductVariant,
ShopifyAddToCartOperation,
@@ -57,8 +60,10 @@ import {
ShopifyCollectionsOperation,
ShopifyCreateCartOperation,
ShopifyFilter,
+ ShopifyImageOperation,
ShopifyMenuOperation,
ShopifyMetaobject,
+ ShopifyMetaobjectOperation,
ShopifyMetaobjectsOperation,
ShopifyPageOperation,
ShopifyPagesOperation,
@@ -490,10 +495,36 @@ export async function getMetaobjects(type: string) {
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
}
+export async function getMetaobjectsByIds(ids: string[]) {
+ if (!ids.length) return [];
+
+ const res = await shopifyFetch({
+ query: getMetaobjectsByIdsQuery,
+ variables: { ids }
+ });
+
+ return reshapeMetaobjects(res.body.data.nodes);
+}
+
+export async function getPageMetaObjects(metafield: PageMetafield) {
+ let metaobjectIds = parseMetaFieldValue(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 {
+ const metafieldIdentifiers = PAGE_TYPES.map((key) => ({ key, namespace: 'custom' }));
const res = await shopifyFetch({
query: getPageQuery,
- variables: { handle }
+ variables: { handle, metafieldIdentifiers }
});
return res.body.data.pageByHandle;
@@ -604,3 +635,12 @@ export async function revalidate(req: NextRequest): Promise {
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
}
+
+export const getImage = async (id: string): Promise => {
+ const res = await shopifyFetch({
+ query: getImageQuery,
+ variables: { id }
+ });
+
+ return res.body.data.node.image;
+};
diff --git a/lib/shopify/queries/node.ts b/lib/shopify/queries/node.ts
new file mode 100644
index 000000000..9adaa5556
--- /dev/null
+++ b/lib/shopify/queries/node.ts
@@ -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
+ }
+ }
+ }
+ }
+`;
diff --git a/lib/shopify/queries/page.ts b/lib/shopify/queries/page.ts
index ac6f6f986..179913095 100644
--- a/lib/shopify/queries/page.ts
+++ b/lib/shopify/queries/page.ts
@@ -19,9 +19,14 @@ const pageFragment = /* GraphQL */ `
`;
export const getPageQuery = /* GraphQL */ `
- query getPage($handle: String!) {
+ query getPage($handle: String!, $metafieldIdentifiers: [HasMetafieldsIdentifier!]!) {
pageByHandle(handle: $handle) {
...page
+ metafields(identifiers: $metafieldIdentifiers) {
+ value
+ key
+ id
+ }
}
}
${pageFragment}
diff --git a/lib/shopify/types.ts b/lib/shopify/types.ts
index 3ef0cbe57..2bb02dda5 100644
--- a/lib/shopify/types.ts
+++ b/lib/shopify/types.ts
@@ -51,6 +51,15 @@ export type Money = {
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 = {
id: string;
title: string;
@@ -60,6 +69,12 @@ export type Page = {
seo?: SEO;
createdAt: string;
updatedAt: string;
+ metafields: PageMetafield[];
+};
+
+export type MetafieldIdentifier = {
+ key: string;
+ namespace: string;
};
export type ShopifyMetaobject = {
@@ -282,7 +297,12 @@ export type ShopifyMenuOperation = {
export type ShopifyPageOperation = {
data: { pageByHandle: Page };
- variables: { handle: string };
+ variables: { handle: string; metafieldIdentifiers: MetafieldIdentifier[] };
+};
+
+export type ShopifyImageOperation = {
+ data: { node: { image: Image } };
+ variables: { id: string };
};
export type ShopifyMetaobjectsOperation = {
@@ -296,6 +316,11 @@ export type ShopifyPagesOperation = {
};
};
+export type ShopifyMetaobjectOperation = {
+ data: { nodes: ShopifyMetaobject[] };
+ variables: { ids: string[] };
+};
+
export type ShopifyProductOperation = {
data: { product: ShopifyProduct };
variables: {
@@ -363,3 +388,13 @@ export type Filter = {
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];
diff --git a/package.json b/package.json
index 05ac76aa3..5c20c0cee 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"clsx": "^2.1.0",
"geist": "^1.3.0",
"lodash.get": "^4.4.2",
+ "lodash.startcase": "^4.4.0",
"next": "14.1.4",
"react": "18.2.0",
"react-dom": "18.2.0",
@@ -40,6 +41,7 @@
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.11",
"@types/lodash.get": "^4.4.9",
+ "@types/lodash.startcase": "^4.4.9",
"@types/node": "20.11.30",
"@types/react": "18.2.72",
"@types/react-dom": "18.2.22",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f78bba5b4..0962faa59 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -23,6 +23,9 @@ dependencies:
lodash.get:
specifier: ^4.4.2
version: 4.4.2
+ lodash.startcase:
+ specifier: ^4.4.0
+ version: 4.4.0
next:
specifier: 14.1.4
version: 14.1.4(react-dom@18.2.0)(react@18.2.0)
@@ -55,6 +58,9 @@ devDependencies:
'@types/lodash.get':
specifier: ^4.4.9
version: 4.4.9
+ '@types/lodash.startcase':
+ specifier: ^4.4.9
+ version: 4.4.9
'@types/node':
specifier: 20.11.30
version: 20.11.30
@@ -753,6 +759,12 @@ packages:
'@types/lodash': 4.17.1
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:
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
dev: true
@@ -2632,6 +2644,10 @@ packages:
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
dev: true
+ /lodash.startcase@4.4.0:
+ resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
+ dev: false
+
/log-update@6.0.0:
resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==}
engines: {node: '>=18'}