From 4d7fe07b4794951b97f250c260785519e7463771 Mon Sep 17 00:00:00 2001 From: Thomas Cristina de Carvalho Date: Thu, 11 Jan 2024 13:45:59 -0500 Subject: [PATCH] Add richtext section --- pnpm-lock.yaml | 1 + .../blocks/AddToCartButtonBlock.tsx | 8 +- .../app/components/blocks/PriceBlock.tsx | 6 +- .../blocks/ShopifyDescriptionBlock.tsx | 8 +- .../components/blocks/ShopifyTitleBlock.tsx | 8 +- .../app/components/cva/contentAlignment.ts | 45 +++++++ .../app/components/icons/IconExternal.tsx | 24 ++++ .../app/components/product/ProductDetails.tsx | 34 ++++- .../app/components/product/ProductForm.tsx | 6 +- .../sanity/richtext/RichTextLayout.tsx | 47 +++++++ .../richtext/components/ButtonBlock.tsx | 27 ++++ .../components/ExternalLinkAnnotation.tsx | 33 +++++ .../sanity/richtext/components/ImageBlock.tsx | 51 ++++++++ .../components/InternalLinkAnnotation.tsx | 34 +++++ .../components/sections/RichtextSection.tsx | 82 ++++++++++++ .../app/lib/sectionRelsolver.ts | 5 + templates/hydrogen-theme/app/qroq/blocks.ts | 99 +++++++++++++-- templates/hydrogen-theme/app/qroq/links.ts | 34 ++--- templates/hydrogen-theme/app/qroq/sections.ts | 62 ++++++--- .../hydrogen-theme/studio/sanity.config.ts | 1 + .../hydrogen-theme/studio/schemas/index.ts | 4 + .../objects/global/productRichtext.tsx | 43 ++++++- .../schemas/objects/global/richtext.tsx | 118 ++++++++++++++++++ .../schemas/objects/global/sectionsList.ts | 3 + .../objects/sections/richtextSection.tsx | 83 ++++++++++++ .../studio/static/assets/richTextSection.png | Bin 0 -> 18220 bytes 26 files changed, 803 insertions(+), 63 deletions(-) create mode 100644 templates/hydrogen-theme/app/components/icons/IconExternal.tsx create mode 100644 templates/hydrogen-theme/app/components/sanity/richtext/RichTextLayout.tsx create mode 100644 templates/hydrogen-theme/app/components/sanity/richtext/components/ButtonBlock.tsx create mode 100644 templates/hydrogen-theme/app/components/sanity/richtext/components/ExternalLinkAnnotation.tsx create mode 100644 templates/hydrogen-theme/app/components/sanity/richtext/components/ImageBlock.tsx create mode 100644 templates/hydrogen-theme/app/components/sanity/richtext/components/InternalLinkAnnotation.tsx create mode 100644 templates/hydrogen-theme/app/components/sections/RichtextSection.tsx create mode 100644 templates/hydrogen-theme/studio/schemas/objects/global/richtext.tsx create mode 100644 templates/hydrogen-theme/studio/schemas/objects/sections/richtextSection.tsx create mode 100644 templates/hydrogen-theme/studio/static/assets/richTextSection.png diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 386e1bc..568b15a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9901,6 +9901,7 @@ packages: loose-envify: 1.4.0 object-assign: 4.1.1 dev: false + bundledDependencies: false /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} diff --git a/templates/hydrogen-theme/app/components/blocks/AddToCartButtonBlock.tsx b/templates/hydrogen-theme/app/components/blocks/AddToCartButtonBlock.tsx index d7e7fc8..25af415 100644 --- a/templates/hydrogen-theme/app/components/blocks/AddToCartButtonBlock.tsx +++ b/templates/hydrogen-theme/app/components/blocks/AddToCartButtonBlock.tsx @@ -1,16 +1,16 @@ -import type {InferType} from 'groqd'; +import type {TypeFromSelection} from 'groqd'; import {Await, useLoaderData} from '@remix-run/react'; import {flattenConnection} from '@shopify/hydrogen'; import {Suspense} from 'react'; -import type {ADD_TO_CART_BUTTON_BLOCK} from '~/qroq/blocks'; +import type {ADD_TO_CART_BUTTON_BLOCK_FRAGMENT} from '~/qroq/blocks'; import type {loader} from '~/routes/($locale).products.$productHandle'; import {ProductForm} from '../product/ProductForm'; -export type AddToCartButtonBlockProps = InferType< - typeof ADD_TO_CART_BUTTON_BLOCK +export type AddToCartButtonBlockProps = TypeFromSelection< + typeof ADD_TO_CART_BUTTON_BLOCK_FRAGMENT >; export function AddToCartButtonBlock(props: AddToCartButtonBlockProps) { diff --git a/templates/hydrogen-theme/app/components/blocks/PriceBlock.tsx b/templates/hydrogen-theme/app/components/blocks/PriceBlock.tsx index dee5637..2ac03d8 100644 --- a/templates/hydrogen-theme/app/components/blocks/PriceBlock.tsx +++ b/templates/hydrogen-theme/app/components/blocks/PriceBlock.tsx @@ -1,15 +1,15 @@ -import type {InferType} from 'groqd'; +import type {TypeFromSelection} from 'groqd'; import {Await, useLoaderData} from '@remix-run/react'; import {flattenConnection} from '@shopify/hydrogen'; import {Suspense} from 'react'; -import type {PRICE_BLOCK} from '~/qroq/blocks'; +import type {PRICE_BLOCK_FRAGMENT} from '~/qroq/blocks'; import type {loader} from '~/routes/($locale).products.$productHandle'; import {VariantPrice} from '../product/VariantPrice'; -export type PriceBlockProps = InferType; +export type PriceBlockProps = TypeFromSelection; export function PriceBlock(props: PriceBlockProps) { const loaderData = useLoaderData(); diff --git a/templates/hydrogen-theme/app/components/blocks/ShopifyDescriptionBlock.tsx b/templates/hydrogen-theme/app/components/blocks/ShopifyDescriptionBlock.tsx index 0b240e2..1cc2842 100644 --- a/templates/hydrogen-theme/app/components/blocks/ShopifyDescriptionBlock.tsx +++ b/templates/hydrogen-theme/app/components/blocks/ShopifyDescriptionBlock.tsx @@ -1,12 +1,12 @@ -import type {InferType} from 'groqd'; +import type {InferType, TypeFromSelection} from 'groqd'; import {useLoaderData} from '@remix-run/react'; -import type {SHOPIFY_DESCRIPTION_BLOCK} from '~/qroq/blocks'; +import type {SHOPIFY_DESCRIPTION_BLOCK_FRAGMENT} from '~/qroq/blocks'; import type {loader} from '~/routes/($locale).products.$productHandle'; -export type ShopifyDescriptionBlockProps = InferType< - typeof SHOPIFY_DESCRIPTION_BLOCK +export type ShopifyDescriptionBlockProps = TypeFromSelection< + typeof SHOPIFY_DESCRIPTION_BLOCK_FRAGMENT >; export function ShopifyDescriptionBlock(props: ShopifyDescriptionBlockProps) { diff --git a/templates/hydrogen-theme/app/components/blocks/ShopifyTitleBlock.tsx b/templates/hydrogen-theme/app/components/blocks/ShopifyTitleBlock.tsx index 5761787..d63d334 100644 --- a/templates/hydrogen-theme/app/components/blocks/ShopifyTitleBlock.tsx +++ b/templates/hydrogen-theme/app/components/blocks/ShopifyTitleBlock.tsx @@ -1,11 +1,13 @@ -import type {InferType} from 'groqd'; +import type {TypeFromSelection} from 'groqd'; import {useLoaderData} from '@remix-run/react'; -import type {SHOPIFY_TITLE_BLOCK} from '~/qroq/blocks'; +import type {SHOPIFY_TITLE_BLOCK_FRAGMENT} from '~/qroq/blocks'; import type {loader} from '~/routes/($locale).products.$productHandle'; -export type ShopifyTitleBlockProps = InferType; +export type ShopifyTitleBlockProps = TypeFromSelection< + typeof SHOPIFY_TITLE_BLOCK_FRAGMENT +>; export function ShopifyTitleBlock(props: ShopifyTitleBlockProps) { const {product} = useLoaderData(); diff --git a/templates/hydrogen-theme/app/components/cva/contentAlignment.ts b/templates/hydrogen-theme/app/components/cva/contentAlignment.ts index 7fbf4ed..ebff168 100644 --- a/templates/hydrogen-theme/app/components/cva/contentAlignment.ts +++ b/templates/hydrogen-theme/app/components/cva/contentAlignment.ts @@ -2,6 +2,11 @@ import type {VariantProps} from 'class-variance-authority'; import {cva} from 'class-variance-authority'; +/* +|-------------------------------------------------------------------------- +| Content Alignment +|-------------------------------------------------------------------------- +*/ export type ContentAlignmentVariantProps = VariantProps< typeof contentAlignment >; @@ -24,3 +29,43 @@ export const contentAlignment = cva('flex h-full', { export const contentAlignmentVariants = (props: ContentAlignmentVariantProps) => contentAlignment(props); + +/* +|-------------------------------------------------------------------------- +| Text Alignment +|-------------------------------------------------------------------------- +*/ +export type TextAlignmentVariantProps = VariantProps; + +export const textAlignment = cva('', { + variants: { + required: { + center: 'text-center', + left: 'text-left', + right: 'text-right', + }, + }, +}); + +export const textAlignmentVariants = (props: TextAlignmentVariantProps) => + textAlignment(props); + +/* +|-------------------------------------------------------------------------- +| Content Position +|-------------------------------------------------------------------------- +*/ +export type ContentPositionVariantProps = VariantProps; + +export const contentPosition = cva('', { + variants: { + required: { + center: 'mx-auto', + left: 'mr-auto', + right: 'ml-auto', + }, + }, +}); + +export const contentPositionVariants = (props: ContentPositionVariantProps) => + contentPosition(props); diff --git a/templates/hydrogen-theme/app/components/icons/IconExternal.tsx b/templates/hydrogen-theme/app/components/icons/IconExternal.tsx new file mode 100644 index 0000000..e5db7cd --- /dev/null +++ b/templates/hydrogen-theme/app/components/icons/IconExternal.tsx @@ -0,0 +1,24 @@ +import {cn} from '~/lib/utils'; + +import type {IconProps} from './Icon'; + +import {Icon} from './Icon'; + +export function IconExternal(props: IconProps) { + return ( + + External + + + + + ); +} diff --git a/templates/hydrogen-theme/app/components/product/ProductDetails.tsx b/templates/hydrogen-theme/app/components/product/ProductDetails.tsx index abda0c0..738e82e 100644 --- a/templates/hydrogen-theme/app/components/product/ProductDetails.tsx +++ b/templates/hydrogen-theme/app/components/product/ProductDetails.tsx @@ -1,3 +1,6 @@ +import type {PortableTextComponents} from '@portabletext/react'; +import type {PortableTextBlock} from '@portabletext/types'; + import {PortableText} from '@portabletext/react'; import {useMemo} from 'react'; @@ -5,16 +8,42 @@ import type {AddToCartButtonBlockProps} from '../blocks/AddToCartButtonBlock'; import type {PriceBlockProps} from '../blocks/PriceBlock'; import type {ShopifyDescriptionBlockProps} from '../blocks/ShopifyDescriptionBlock'; import type {ShopifyTitleBlockProps} from '../blocks/ShopifyTitleBlock'; +import type {ExternalLinkAnnotationProps} from '../sanity/richtext/components/ExternalLinkAnnotation'; +import type {InternalLinkAnnotationProps} from '../sanity/richtext/components/InternalLinkAnnotation'; import type {ProductInformationSectionProps} from '../sections/ProductInformationSection'; import {AddToCartButtonBlock} from '../blocks/AddToCartButtonBlock'; import {PriceBlock} from '../blocks/PriceBlock'; import {ShopifyDescriptionBlock} from '../blocks/ShopifyDescriptionBlock'; import {ShopifyTitleBlock} from '../blocks/ShopifyTitleBlock'; +import {ExternalLinkAnnotation} from '../sanity/richtext/components/ExternalLinkAnnotation'; +import {InternalLinkAnnotation} from '../sanity/richtext/components/InternalLinkAnnotation'; export function ProductDetails(props: {data: ProductInformationSectionProps}) { const components = useMemo( () => ({ + marks: { + externalLink: (props: { + children: React.ReactNode; + value: ExternalLinkAnnotationProps; + }) => { + return ( + + {props.children} + + ); + }, + internalLink: (props: { + children: React.ReactNode; + value: InternalLinkAnnotationProps; + }) => { + return ( + + {props.children} + + ); + }, + }, types: { addToCartButton: (props: {value: AddToCartButtonBlockProps}) => { return ; @@ -36,7 +65,10 @@ export function ProductDetails(props: {data: ProductInformationSectionProps}) { return (
{props.data.richtext && ( - + )}
); diff --git a/templates/hydrogen-theme/app/components/product/ProductForm.tsx b/templates/hydrogen-theme/app/components/product/ProductForm.tsx index 06cff5f..ecf8417 100644 --- a/templates/hydrogen-theme/app/components/product/ProductForm.tsx +++ b/templates/hydrogen-theme/app/components/product/ProductForm.tsx @@ -1,9 +1,9 @@ -import type {InferType} from 'groqd'; +import type {TypeFromSelection} from 'groqd'; import type {ProductVariantFragmentFragment} from 'storefrontapi.generated'; import {useLoaderData} from '@remix-run/react'; -import type {ADD_TO_CART_BUTTON_BLOCK} from '~/qroq/blocks'; +import type {ADD_TO_CART_BUTTON_BLOCK_FRAGMENT} from '~/qroq/blocks'; import type {loader} from '~/routes/($locale).products.$productHandle'; import {AddToCartForm} from './AddToCartForm'; @@ -12,7 +12,7 @@ import {VariantSelector} from './VariantSelector'; export function ProductForm( props: { variants: ProductVariantFragmentFragment[]; - } & InferType, + } & TypeFromSelection, ) { const {product} = useLoaderData(); const showQuantitySelector = props.quantitySelector; diff --git a/templates/hydrogen-theme/app/components/sanity/richtext/RichTextLayout.tsx b/templates/hydrogen-theme/app/components/sanity/richtext/RichTextLayout.tsx new file mode 100644 index 0000000..aa0ea74 --- /dev/null +++ b/templates/hydrogen-theme/app/components/sanity/richtext/RichTextLayout.tsx @@ -0,0 +1,47 @@ +import {vercelStegaCleanAll} from '@sanity/client/stega'; +import {cx} from 'class-variance-authority'; + +import type {simpleContentAlignmentValues} from '~/qroq/sections'; + +import { + contentPosition, + textAlignment, +} from '~/components/cva/contentAlignment'; + +type AlignmentValues = (typeof simpleContentAlignmentValues)[number]; + +export function RichtextLayout(props: { + children: React.ReactNode; + contentAligment?: AlignmentValues | null; + desktopContentPosition?: AlignmentValues | null; + maxWidth?: null | number; +}) { + const style = { + '--maxWidth': props.maxWidth ? `${props.maxWidth}px` : 'auto', + } as React.CSSProperties; + + const cleanContentAlignement = vercelStegaCleanAll(props.contentAligment); + const cleanContentPosition = vercelStegaCleanAll( + props.desktopContentPosition, + ); + + return ( +
li]:mt-2 [&_ul]:list-inside [&_ul]:list-disc', + '[&_ol>li]:mt-2 [&_ol]:list-inside [&_ol]:list-decimal', + ])} + style={style} + > + {props.children} +
+ ); +} diff --git a/templates/hydrogen-theme/app/components/sanity/richtext/components/ButtonBlock.tsx b/templates/hydrogen-theme/app/components/sanity/richtext/components/ButtonBlock.tsx new file mode 100644 index 0000000..e96fada --- /dev/null +++ b/templates/hydrogen-theme/app/components/sanity/richtext/components/ButtonBlock.tsx @@ -0,0 +1,27 @@ +import type {TypeFromSelection} from 'groqd'; + +import type {BUTTON_BLOCK_FRAGMENT} from '~/qroq/blocks'; + +import {Button} from '~/components/ui/Button'; + +import {SanityInternalLink} from '../../link/SanityInternalLink'; + +export type ButtonBlockProps = TypeFromSelection; + +export function ButtonBlock(props: ButtonBlockProps) { + return ( + + ); +} diff --git a/templates/hydrogen-theme/app/components/sanity/richtext/components/ExternalLinkAnnotation.tsx b/templates/hydrogen-theme/app/components/sanity/richtext/components/ExternalLinkAnnotation.tsx new file mode 100644 index 0000000..79b2da2 --- /dev/null +++ b/templates/hydrogen-theme/app/components/sanity/richtext/components/ExternalLinkAnnotation.tsx @@ -0,0 +1,33 @@ +import type {TypeFromSelection} from 'groqd'; + +import type {EXTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT} from '~/qroq/blocks'; + +import {IconExternal} from '~/components/icons/IconExternal'; +import {cn} from '~/lib/utils'; + +import {SanityExternalLink} from '../../link/SanityExternalLink'; +import {richTextLinkClassName} from './InternalLinkAnnotation'; + +export type ExternalLinkAnnotationProps = TypeFromSelection< + typeof EXTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT +>; + +export function ExternalLinkAnnotation( + props: ExternalLinkAnnotationProps & {children: React.ReactNode}, +) { + return ( + + {props.children} + {props.openInNewTab && } + + ); +} diff --git a/templates/hydrogen-theme/app/components/sanity/richtext/components/ImageBlock.tsx b/templates/hydrogen-theme/app/components/sanity/richtext/components/ImageBlock.tsx new file mode 100644 index 0000000..4e2d043 --- /dev/null +++ b/templates/hydrogen-theme/app/components/sanity/richtext/components/ImageBlock.tsx @@ -0,0 +1,51 @@ +import type {TypeFromSelection} from 'groqd'; + +import {vercelStegaCleanAll} from '@sanity/client/stega'; +import {cva} from 'class-variance-authority'; + +import type {IMAGE_BLOCK_FRAGMENT} from '~/qroq/blocks'; + +import {SanityImage} from '../../SanityImage'; + +export type ImageBlockProps = TypeFromSelection; + +export function ImageBlock( + props: ImageBlockProps & { + containerMaxWidth?: null | number; + }, +) { + const maxWidth = + props.containerMaxWidth && + props.maxWidth && + props.containerMaxWidth <= props.maxWidth + ? props.containerMaxWidth + : props.maxWidth; + const style = { + '--maxWidth': maxWidth ? `${maxWidth}px` : 'auto', + } as React.CSSProperties; + const sizes = maxWidth ? `(min-width: 1024px) ${maxWidth}px, 100vw` : '100vw'; + const alignment = props.alignment + ? vercelStegaCleanAll(props.alignment) + : 'center'; + + const alignmentVariants = cva('w-[var(--maxWidth)] max-w-full', { + variants: { + required: { + center: 'mx-auto', + left: 'mr-auto', + right: 'ml-auto', + }, + }, + }); + + return ( + + ); +} diff --git a/templates/hydrogen-theme/app/components/sanity/richtext/components/InternalLinkAnnotation.tsx b/templates/hydrogen-theme/app/components/sanity/richtext/components/InternalLinkAnnotation.tsx new file mode 100644 index 0000000..3648921 --- /dev/null +++ b/templates/hydrogen-theme/app/components/sanity/richtext/components/InternalLinkAnnotation.tsx @@ -0,0 +1,34 @@ +import type {TypeFromSelection} from 'groqd'; + +import {cx} from 'class-variance-authority'; + +import type {INTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT} from '~/qroq/blocks'; + +import {SanityInternalLink} from '../../link/SanityInternalLink'; + +export type InternalLinkAnnotationProps = TypeFromSelection< + typeof INTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT +>; + +export const richTextLinkClassName = cx( + 'text-color-scheme-primary-button-bg underline-offset-4 hover:underline', +); + +export function InternalLinkAnnotation( + props: InternalLinkAnnotationProps & {children: React.ReactNode}, +) { + return ( + + {props.children} + + ); +} diff --git a/templates/hydrogen-theme/app/components/sections/RichtextSection.tsx b/templates/hydrogen-theme/app/components/sections/RichtextSection.tsx new file mode 100644 index 0000000..b58b4bc --- /dev/null +++ b/templates/hydrogen-theme/app/components/sections/RichtextSection.tsx @@ -0,0 +1,82 @@ +import type {PortableTextComponents} from '@portabletext/react'; +import type {PortableTextBlock} from '@portabletext/types'; +import type {TypeFromSelection} from 'groqd'; + +import {PortableText} from '@portabletext/react'; +import {useMemo} from 'react'; + +import type {SectionDefaultProps} from '~/lib/type'; +import type {RICHTEXT_SECTION_FRAGMENT} from '~/qroq/sections'; + +import type {ButtonBlockProps} from '../sanity/richtext/components/ButtonBlock'; +import type {ExternalLinkAnnotationProps} from '../sanity/richtext/components/ExternalLinkAnnotation'; +import type {ImageBlockProps} from '../sanity/richtext/components/ImageBlock'; +import type {InternalLinkAnnotationProps} from '../sanity/richtext/components/InternalLinkAnnotation'; + +import {RichtextLayout} from '../sanity/richtext/RichTextLayout'; +import {ButtonBlock} from '../sanity/richtext/components/ButtonBlock'; +import {ExternalLinkAnnotation} from '../sanity/richtext/components/ExternalLinkAnnotation'; +import {ImageBlock} from '../sanity/richtext/components/ImageBlock'; +import {InternalLinkAnnotation} from '../sanity/richtext/components/InternalLinkAnnotation'; + +type RichtextSectionProps = TypeFromSelection; + +export function RichtextSection( + props: SectionDefaultProps & {data: RichtextSectionProps}, +) { + const {data} = props; + const containerMaxWidth = data.maxWidth; + + const components = useMemo( + () => ({ + marks: { + externalLink: (props: { + children: React.ReactNode; + value: ExternalLinkAnnotationProps; + }) => { + return ( + + {props.children} + + ); + }, + internalLink: (props: { + children: React.ReactNode; + value: InternalLinkAnnotationProps; + }) => { + return ( + + {props.children} + + ); + }, + }, + types: { + button: (props: {value: ButtonBlockProps}) => ( + + ), + image: (props: {value: ImageBlockProps}) => ( + + ), + }, + }), + [containerMaxWidth], + ); + + return ( +
+ + {data.richtext && ( + + )} + +
+ ); +} diff --git a/templates/hydrogen-theme/app/lib/sectionRelsolver.ts b/templates/hydrogen-theme/app/lib/sectionRelsolver.ts index c10d304..0fb5ccf 100644 --- a/templates/hydrogen-theme/app/lib/sectionRelsolver.ts +++ b/templates/hydrogen-theme/app/lib/sectionRelsolver.ts @@ -47,6 +47,11 @@ export const sections: { default: module.RelatedProductsSection, })), ), + richtextSection: lazy(() => + import('../components/sections/RichtextSection').then((module) => ({ + default: module.RichtextSection, + })), + ), socialLinksOnly: lazy(() => import('../components/footers/SocialLinksOnly').then((module) => ({ default: module.SocialLinksOnly, diff --git a/templates/hydrogen-theme/app/qroq/blocks.ts b/templates/hydrogen-theme/app/qroq/blocks.ts index df6e089..64e10f7 100644 --- a/templates/hydrogen-theme/app/qroq/blocks.ts +++ b/templates/hydrogen-theme/app/qroq/blocks.ts @@ -1,28 +1,111 @@ -import {q} from 'groqd'; +import type {Selection} from 'groqd'; + +import {q, z} from 'groqd'; + +import {IMAGE_FRAGMENT} from './fragments'; +import {LINK_REFERENCE_FRAGMENT} from './links'; +import {simpleContentAlignmentValues} from './sections'; + +/* +|-------------------------------------------------------------------------- +| Base Blocks +|-------------------------------------------------------------------------- +*/ +export const INTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT = { + _key: q.string(), + _type: q.literal('internalLink'), + anchor: q.string().nullable(), + link: LINK_REFERENCE_FRAGMENT, +} satisfies Selection; + +export const EXTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT = { + _key: q.string(), + _type: q.literal('externalLink'), + link: q.string().nullable(), + openInNewTab: q.boolean().nullable(), +} satisfies Selection; + +export const BASE_BLOCK_FRAGMENT = { + _key: q.string().optional(), + _type: q.string(), + children: q.array( + q.object({ + _key: q.string(), + _type: q.string(), + marks: q.array(q.string()), + text: q.string(), + }), + ), + level: q.number().optional(), + listItem: q.string().optional(), + markDefs: q('markDefs[]', {isArray: true}) + .filter() + .select({ + '_type == "externalLink"': EXTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT, + '_type == "internalLink"': INTERNAL_LINK_BLOCK_ANNOTATION_FRAGMENT, + default: ['{...}', q.object({})], + }), + style: q.string().optional(), +} satisfies Selection; /* |-------------------------------------------------------------------------- | Product Custom Blocks |-------------------------------------------------------------------------- */ -export const SHOPIFY_TITLE_BLOCK = q.object({ +export const SHOPIFY_TITLE_BLOCK_FRAGMENT = { _key: q.string(), _type: q.literal('shopifyTitle'), -}); +} satisfies Selection; -export const SHOPIFY_DESCRIPTION_BLOCK = q.object({ +export const SHOPIFY_DESCRIPTION_BLOCK_FRAGMENT = { _key: q.string(), _type: q.literal('shopifyDescription'), -}); +} satisfies Selection; -export const ADD_TO_CART_BUTTON_BLOCK = q.object({ +export const ADD_TO_CART_BUTTON_BLOCK_FRAGMENT = { _key: q.string(), _type: q.literal('addToCartButton'), quantitySelector: q.boolean().nullable(), shopPayButton: q.boolean().nullable(), -}); +} satisfies Selection; -export const PRICE_BLOCK = q.object({ +export const PRICE_BLOCK_FRAGMENT = { _key: q.string(), _type: q.literal('price'), +} satisfies Selection; + +export const PRODUCT_RICHTEXT_BLOCKS = q.select({ + '_type == "addToCartButton"': ADD_TO_CART_BUTTON_BLOCK_FRAGMENT, + '_type == "block"': BASE_BLOCK_FRAGMENT, + '_type == "price"': PRICE_BLOCK_FRAGMENT, + '_type == "shopifyDescription"': SHOPIFY_DESCRIPTION_BLOCK_FRAGMENT, + '_type == "shopifyTitle"': SHOPIFY_TITLE_BLOCK_FRAGMENT, +}); + +/* +|-------------------------------------------------------------------------- +| Richtext Blocks +|-------------------------------------------------------------------------- +*/ +export const BUTTON_BLOCK_FRAGMENT = { + _key: q.string(), + _type: q.literal('button'), + anchor: q.string().nullable(), + label: q.string().nullable(), + link: LINK_REFERENCE_FRAGMENT, +} satisfies Selection; + +export const IMAGE_BLOCK_FRAGMENT = { + _key: q.string(), + _type: q.literal('image'), + ...IMAGE_FRAGMENT, + alignment: z.enum(simpleContentAlignmentValues).nullable(), + maxWidth: q.number().nullable(), +} satisfies Selection; + +export const RICHTEXT_BLOCKS = q.select({ + '_type == "block"': BASE_BLOCK_FRAGMENT, + '_type == "button"': BUTTON_BLOCK_FRAGMENT, + '_type == "image"': IMAGE_BLOCK_FRAGMENT, }); diff --git a/templates/hydrogen-theme/app/qroq/links.ts b/templates/hydrogen-theme/app/qroq/links.ts index 452c919..ddd4a4f 100644 --- a/templates/hydrogen-theme/app/qroq/links.ts +++ b/templates/hydrogen-theme/app/qroq/links.ts @@ -7,26 +7,28 @@ import {q} from 'groqd'; | Links Fragments |-------------------------------------------------------------------------- */ +export const LINK_REFERENCE_FRAGMENT = q('link') + .deref() + .grab({ + documentType: ['_type', q.string()], + slug: [ + `coalesce( + slug, + store.slug + )`, + q.object({ + _type: q.string(), + current: q.string(), + }), + ], + }) + .nullable(); + export const INTERNAL_LINK_FRAGMENT = { _key: q.string().nullable(), _type: q.literal('internalLink'), anchor: q.string().nullable(), - link: q('link') - .deref() - .grab({ - documentType: ['_type', q.string()], - slug: [ - `coalesce( - slug, - store.slug - )`, - q.object({ - _type: q.string(), - current: q.string(), - }), - ], - }) - .nullable(), + link: LINK_REFERENCE_FRAGMENT, name: q.string().nullable(), } satisfies Selection; diff --git a/templates/hydrogen-theme/app/qroq/sections.ts b/templates/hydrogen-theme/app/qroq/sections.ts index 6e39491..2dd01e8 100644 --- a/templates/hydrogen-theme/app/qroq/sections.ts +++ b/templates/hydrogen-theme/app/qroq/sections.ts @@ -2,12 +2,7 @@ import type {Selection} from 'groqd'; import {q, z} from 'groqd'; -import { - ADD_TO_CART_BUTTON_BLOCK, - PRICE_BLOCK, - SHOPIFY_DESCRIPTION_BLOCK, - SHOPIFY_TITLE_BLOCK, -} from './blocks'; +import {PRODUCT_RICHTEXT_BLOCKS, RICHTEXT_BLOCKS} from './blocks'; import {COLOR_SCHEME_FRAGMENT, IMAGE_FRAGMENT} from './fragments'; import {getIntValue} from './utils'; @@ -23,6 +18,12 @@ export const contentAlignmentValues = [ 'bottom_right', ] as const; +export const simpleContentAlignmentValues = [ + 'left', + 'center', + 'right', +] as const; + /* |-------------------------------------------------------------------------- | Section Settings @@ -110,20 +111,16 @@ export const FEATURED_PRODUCT_SECTION_FRAGMENT = { export const PRODUCT_INFORMATION_SECTION_FRAGMENT = { _key: q.string().nullable(), _type: q.literal('productInformationSection'), - richtext: [ - getIntValue('richtext'), - q - .array( - q.union([ - SHOPIFY_TITLE_BLOCK, - SHOPIFY_DESCRIPTION_BLOCK, - ADD_TO_CART_BUTTON_BLOCK, - PRICE_BLOCK, - q.contentBlock(), - ]), - ) - .nullable(), - ], + richtext: q( + `coalesce( + richtext[_key == $language][0].value[], + richtext[_key == $defaultLanguage][0].value[], + )[]`, + {isArray: true}, + ) + .filter() + .select(PRODUCT_RICHTEXT_BLOCKS) + .nullable(), settings: SECTION_SETTINGS_FRAGMENT, } satisfies Selection; @@ -196,6 +193,30 @@ export const CAROUSEL_SECTION_FRAGMENT = { title: [getIntValue('title'), q.string()], } satisfies Selection; +/* +|-------------------------------------------------------------------------- +| Richtext Section +|-------------------------------------------------------------------------- +*/ +export const RICHTEXT_SECTION_FRAGMENT = { + _key: q.string().nullable(), + _type: q.literal('richtextSection'), + contentAlignment: z.enum(simpleContentAlignmentValues).nullable(), + desktopContentPosition: z.enum(simpleContentAlignmentValues).nullable(), + maxWidth: q.number().nullable(), + richtext: q( + `coalesce( + richtext[_key == $language][0].value[], + richtext[_key == $defaultLanguage][0].value[], + )[]`, + {isArray: true}, + ) + .filter() + .select(RICHTEXT_BLOCKS) + .nullable(), + settings: SECTION_SETTINGS_FRAGMENT, +} satisfies Selection; + /* |-------------------------------------------------------------------------- | List of sections @@ -208,6 +229,7 @@ export const SECTIONS_LIST_SELECTION = { "_type == 'featuredCollectionSection'": FEATURED_COLLECTION_SECTION_FRAGMENT, "_type == 'featuredProductSection'": FEATURED_PRODUCT_SECTION_FRAGMENT, "_type == 'imageBannerSection'": IMAGE_BANNER_SECTION_FRAGMENT, + "_type == 'richtextSection'": RICHTEXT_SECTION_FRAGMENT, }; /* diff --git a/templates/hydrogen-theme/studio/sanity.config.ts b/templates/hydrogen-theme/studio/sanity.config.ts index 4857d77..e76c6d3 100644 --- a/templates/hydrogen-theme/studio/sanity.config.ts +++ b/templates/hydrogen-theme/studio/sanity.config.ts @@ -61,6 +61,7 @@ export default defineConfig({ 'slug', 'headerNavigation', 'productRichtext', + 'richtext', ], buttonLocations: ['field'], }), diff --git a/templates/hydrogen-theme/studio/schemas/index.ts b/templates/hydrogen-theme/studio/schemas/index.ts index 8f1d4f2..e6dffee 100644 --- a/templates/hydrogen-theme/studio/schemas/index.ts +++ b/templates/hydrogen-theme/studio/schemas/index.ts @@ -36,6 +36,8 @@ import productInformationSection from './objects/sections/productInformationSect import productRichtext from './objects/global/productRichtext'; import relatedProductsSection from './objects/sections/relatedProductsSection'; import carouselSection from './objects/sections/carouselSection'; +import richtextSection from './objects/sections/richtextSection'; +import richtext from './objects/global/richtext'; const singletons = [home, header, footer, settings, themeContent]; const documents = [page, color, collection, product, blogPost, productVariant]; @@ -48,6 +50,7 @@ const sections = [ ctaSection, relatedProductsSection, carouselSection, + richtextSection, ]; const footers = [socialLinksOnly]; const objects = [ @@ -70,6 +73,7 @@ const objects = [ paddingObject, overlayOpacity, contentAlignment, + richtext, ]; export const schemaTypes = [ diff --git a/templates/hydrogen-theme/studio/schemas/objects/global/productRichtext.tsx b/templates/hydrogen-theme/studio/schemas/objects/global/productRichtext.tsx index 25a7923..ede5bc7 100644 --- a/templates/hydrogen-theme/studio/schemas/objects/global/productRichtext.tsx +++ b/templates/hydrogen-theme/studio/schemas/objects/global/productRichtext.tsx @@ -1,5 +1,13 @@ -import {BadgeDollarSign, ShoppingCart, Text, Type} from 'lucide-react'; +import { + BadgeDollarSign, + ExternalLink, + Link, + ShoppingCart, + Text, + Type, +} from 'lucide-react'; import {defineField} from 'sanity'; +import {internalLinkFields} from './richtext'; export default defineField({ name: 'productRichtext', @@ -7,6 +15,39 @@ export default defineField({ of: [ { type: 'block', + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Underline', value: 'underline'}, + {title: 'Strike-through', value: 'strike-through'}, + ], + annotations: [ + { + name: 'internalLink', + type: 'object', + title: 'Internal link', + icon: () => , + fields: [...internalLinkFields], + }, + { + name: 'externalLink', + type: 'object', + title: 'External link', + icon: () => , + fields: [ + defineField({ + name: 'link', + type: 'url', + }), + defineField({ + name: 'openInNewTab', + type: 'boolean', + }), + ], + }, + ], + }, }, { name: 'shopifyTitle', diff --git a/templates/hydrogen-theme/studio/schemas/objects/global/richtext.tsx b/templates/hydrogen-theme/studio/schemas/objects/global/richtext.tsx new file mode 100644 index 0000000..7740896 --- /dev/null +++ b/templates/hydrogen-theme/studio/schemas/objects/global/richtext.tsx @@ -0,0 +1,118 @@ +import {defineField} from 'sanity'; +import {internalLinkField} from './headerNavigation'; +import {ExternalLink, Link, MousePointerSquare} from 'lucide-react'; + +export const internalLinkFields = [ + internalLinkField, + defineField({ + name: 'anchor', + description: 'The ID of the element to scroll to, without the #.', + type: 'string', + }), +]; + +export default defineField({ + name: 'richtext', + type: 'array', + of: [ + { + type: 'block', + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Underline', value: 'underline'}, + {title: 'Strike-through', value: 'strike-through'}, + ], + annotations: [ + { + name: 'internalLink', + type: 'object', + title: 'Internal link', + icon: () => , + fields: [...internalLinkFields], + }, + { + name: 'externalLink', + type: 'object', + title: 'External link', + icon: () => , + fields: [ + defineField({ + name: 'link', + type: 'url', + }), + defineField({ + name: 'openInNewTab', + type: 'boolean', + }), + ], + }, + ], + }, + }, + { + type: 'image', + fields: [ + { + name: 'maxWidth', + type: 'rangeSlider', + options: { + min: 0, + max: 3840, + suffix: 'px', + }, + }, + { + name: 'alignment', + type: 'string', + options: { + list: [ + { + title: 'Left', + value: 'left', + }, + { + title: 'Center', + value: 'center', + }, + { + title: 'Right', + value: 'right', + }, + ], + }, + }, + ], + options: { + hotspot: true, + }, + initialValue: { + maxWidth: 900, + alignment: 'center', + }, + }, + { + name: 'button', + type: 'object', + fields: [ + defineField({ + name: 'label', + type: 'string', + }), + ...internalLinkFields, + ], + icon: () => , + preview: { + select: { + title: 'label', + }, + prepare: ({title}) => { + return { + title: title ? title : 'Button', + }; + }, + }, + }, + ], +}); diff --git a/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts b/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts index 4c1fdb8..df5f49f 100644 --- a/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts +++ b/templates/hydrogen-theme/studio/schemas/objects/global/sectionsList.ts @@ -21,6 +21,9 @@ const globalSections = [ { type: 'carouselSection', }, + { + type: 'richtextSection', + }, ]; const pdpSections = [ diff --git a/templates/hydrogen-theme/studio/schemas/objects/sections/richtextSection.tsx b/templates/hydrogen-theme/studio/schemas/objects/sections/richtextSection.tsx new file mode 100644 index 0000000..f103dce --- /dev/null +++ b/templates/hydrogen-theme/studio/schemas/objects/sections/richtextSection.tsx @@ -0,0 +1,83 @@ +import {TextSelect} from 'lucide-react'; +import {defineField} from 'sanity'; + +export default defineField({ + name: 'richtextSection', + title: 'Richtext', + type: 'object', + fields: [ + defineField({ + name: 'richtext', + type: 'internationalizedArrayRichtext', + }), + defineField({ + name: 'desktopContentPosition', + description: 'Position is automatically optimized for mobile.', + type: 'string', + options: { + list: [ + { + title: 'Left', + value: 'left', + }, + { + title: 'Center', + value: 'center', + }, + { + title: 'Right', + value: 'right', + }, + ], + }, + }), + defineField({ + name: 'contentAlignment', + title: 'Content Alignment', + type: 'string', + options: { + list: [ + { + title: 'Left', + value: 'left', + }, + { + title: 'Center', + value: 'center', + }, + { + title: 'Right', + value: 'right', + }, + ], + }, + }), + defineField({ + name: 'maxWidth', + title: 'Content Max Width', + type: 'rangeSlider', + options: { + min: 0, + max: 1920, + suffix: 'px', + }, + }), + defineField({ + type: 'sectionSettings', + name: 'settings', + }), + ], + initialValue: { + desktopContentPosition: 'center', + contentAlignment: 'left', + maxWidth: 900, + }, + preview: { + prepare() { + return { + title: 'Richtext', + media: () => , + }; + }, + }, +}); diff --git a/templates/hydrogen-theme/studio/static/assets/richTextSection.png b/templates/hydrogen-theme/studio/static/assets/richTextSection.png new file mode 100644 index 0000000000000000000000000000000000000000..d323efc6765a35652db5b94976ca4d1ea1d2b7f7 GIT binary patch literal 18220 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 WYglv}G(VL90000