-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
301 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
core/lib/makeswift/components/product-detail/client.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
'use client'; | ||
|
||
import React, { | ||
type ComponentPropsWithoutRef, | ||
createContext, | ||
forwardRef, | ||
type PropsWithChildren, | ||
type ReactNode, | ||
type Ref, | ||
useCallback, | ||
useContext, | ||
} from 'react'; | ||
|
||
import { type Streamable } from '@/vibes/soul/lib/streamable'; | ||
import { ProductDetail, type ProductDetailProduct } from '@/vibes/soul/sections/product-detail'; | ||
import { | ||
computedStremableProp, | ||
useComputedStremableProp, | ||
} from '~/lib/makeswift/hooks/use-computed-stremable-prop'; | ||
import { useStreamablePropsMemo } from '~/lib/makeswift/hooks/use-streamable-props-memo'; | ||
import { mergeSections } from '~/lib/makeswift/utils/merge-sections'; | ||
|
||
type VibesProductDetailProps = ComponentPropsWithoutRef<typeof ProductDetail>; | ||
export type ProductDetail = ProductDetailProduct & { | ||
plainTextDescription?: string; | ||
}; | ||
|
||
export type Props = VibesProductDetailProps & { | ||
product: Streamable<ProductDetail>; | ||
}; | ||
|
||
const PropsContext = createContext<Props | null>(null); | ||
|
||
export const PropsContextProvider = ({ value, children }: PropsWithChildren<{ value: Props }>) => ( | ||
<PropsContext.Provider value={value}>{children}</PropsContext.Provider> | ||
); | ||
|
||
export const DescriptionSource = { | ||
CatalogPlainText: 'CatalogPlainText', | ||
CatalogRichText: 'CatalogRichText', | ||
Custom: 'Custom', | ||
} as const; | ||
|
||
type DescriptionSource = (typeof DescriptionSource)[keyof typeof DescriptionSource]; | ||
|
||
interface EditableProps { | ||
summaryText: string | undefined; | ||
description: { source: DescriptionSource; slot: ReactNode }; | ||
accordions: Exclude<Awaited<ProductDetailProduct['accordions']>, undefined>; | ||
} | ||
|
||
const ProductDetailImpl = ({ | ||
summaryText, | ||
description, | ||
accordions, | ||
...props | ||
}: VibesProductDetailProps & EditableProps) => { | ||
const getProductDescription = useCallback( | ||
(product: ProductDetail): ProductDetail['description'] => { | ||
switch (description.source) { | ||
case DescriptionSource.CatalogPlainText: | ||
return product.plainTextDescription; | ||
|
||
case DescriptionSource.CatalogRichText: | ||
return product.description; | ||
|
||
case DescriptionSource.Custom: | ||
return description.slot; | ||
} | ||
}, | ||
[description.source, description.slot], | ||
); | ||
|
||
const getProductAccordions = useCallback( | ||
(product: ProductDetail): ProductDetail['accordions'] => | ||
product.accordions != null | ||
? computedStremableProp(product.accordions, (productAccordions) => | ||
mergeSections(productAccordions, accordions, (left, right) => ({ | ||
...left, | ||
content: right.content, | ||
})), | ||
) | ||
: undefined, | ||
[accordions], | ||
); | ||
|
||
const memoizedProps = useStreamablePropsMemo(props); | ||
const computedProduct = useComputedStremableProp( | ||
memoizedProps.product, | ||
useCallback( | ||
(product: ProductDetailProduct) => ({ | ||
...product, | ||
summary: summaryText, | ||
description: getProductDescription(product), | ||
accordions: getProductAccordions(product), | ||
}), | ||
[summaryText, getProductDescription, getProductAccordions], | ||
), | ||
); | ||
|
||
return <ProductDetail {...{ ...memoizedProps, product: computedProduct }} />; | ||
}; | ||
|
||
export const MakeswiftProductDetail = forwardRef( | ||
(props: EditableProps, ref: Ref<HTMLDivElement>) => { | ||
const passedProps = useContext(PropsContext); | ||
|
||
if (passedProps == null) { | ||
// eslint-disable-next-line no-console | ||
console.error('No context provided for MakeswiftProductDetail'); | ||
|
||
return <p ref={ref}>There was an error rendering the product detail.</p>; | ||
} | ||
|
||
return ( | ||
<div className="flex flex-col" ref={ref}> | ||
<ProductDetailImpl {...{ ...passedProps, ...props }} /> | ||
</div> | ||
); | ||
}, | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import { Component } from '~/lib/makeswift/component'; | ||
|
||
import { type Props as ClientProps, PropsContextProvider } from './client'; | ||
import { COMPONENT_TYPE } from './register'; | ||
|
||
type Props = ClientProps & { productId: number; productTitle: Promise<string> }; | ||
|
||
export const ProductDetail = ({ productId, productTitle, ...props }: Props) => ( | ||
<PropsContextProvider value={props}> | ||
<Component | ||
label={(async () => `Detail for ${await productTitle}`)()} | ||
snapshotId={`product-detail-${productId}`} | ||
type={COMPONENT_TYPE} | ||
/> | ||
</PropsContextProvider> | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { List, Select, Shape, Slot, TextArea, TextInput } from '@makeswift/runtime/controls'; | ||
|
||
import { runtime } from '~/lib/makeswift/runtime'; | ||
|
||
import { DescriptionSource, MakeswiftProductDetail } from './client'; | ||
|
||
export const COMPONENT_TYPE = 'catalyst-makeswift-product-detail-description'; | ||
|
||
const description = Shape({ | ||
label: 'Description', | ||
type: { | ||
source: Select({ | ||
label: 'Source', | ||
options: [ | ||
{ label: 'Catalog (plain text)', value: DescriptionSource.CatalogPlainText }, | ||
{ label: 'Catalog (rich text)', value: DescriptionSource.CatalogRichText }, | ||
{ label: 'Custom', value: DescriptionSource.Custom }, | ||
], | ||
defaultValue: DescriptionSource.CatalogRichText, | ||
}), | ||
slot: Slot(), | ||
}, | ||
}); | ||
|
||
runtime.registerComponent(MakeswiftProductDetail, { | ||
type: COMPONENT_TYPE, | ||
label: 'MakeswiftProductDetail (private)', | ||
hidden: true, | ||
props: { | ||
summaryText: TextArea({ | ||
label: 'Summary', | ||
}), | ||
description, | ||
accordions: List({ | ||
label: 'Product info', | ||
type: Shape({ | ||
label: 'Product info section', | ||
type: { | ||
title: TextInput({ label: 'Title', defaultValue: 'Section' }), | ||
content: Slot(), | ||
}, | ||
}), | ||
getItemLabel: (section) => section?.title || 'Section', | ||
}), | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { useMemo } from 'react'; | ||
|
||
import { type Streamable } from '@/vibes/soul/lib/streamable'; | ||
|
||
export const computedStremableProp = <T>( | ||
prop: Streamable<T>, | ||
computation: (prop: NonNullable<T>) => NonNullable<T>, | ||
): Streamable<T> => { | ||
if (prop == null) return prop; | ||
|
||
if (prop instanceof Promise) { | ||
return new Promise((resolve, reject) => { | ||
Promise.resolve(prop) | ||
.then((resolvedProp) => { | ||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
resolve(resolvedProp == null ? resolvedProp : computation(resolvedProp)); | ||
}) | ||
.catch((error: unknown) => | ||
reject(error instanceof Error ? error : new Error(String(error))), | ||
); | ||
}); | ||
} | ||
|
||
return computation(prop); | ||
}; | ||
|
||
export const useComputedStremableProp = <T>( | ||
prop: Streamable<T>, | ||
computation: (prop: NonNullable<T>) => NonNullable<T>, | ||
) => useMemo((): Streamable<T> => computedStremableProp(prop, computation), [prop, computation]); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { type ReactElement, useMemo } from 'react'; | ||
|
||
interface Options { | ||
maxDepth?: number; | ||
} | ||
|
||
function isReactElement(value: object): value is ReactElement { | ||
return '$$typeof' in value && typeof value.$$typeof === 'symbol'; | ||
} | ||
|
||
function streamablePropsMemoProxy<T extends object>(props: T, { maxDepth = 2 }: Options = {}): T { | ||
if (maxDepth <= 0) return props; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const resolvedProps: Record<string | symbol, any> = {}; | ||
|
||
const proxyHandler: ProxyHandler<T> = { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
get(target: T, prop: string | symbol): any { | ||
if (!(prop in target)) { | ||
return undefined; | ||
} | ||
|
||
if (prop in resolvedProps) { | ||
return resolvedProps[prop]; | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions | ||
const targetProp = prop in target ? target[prop as keyof T] : undefined; | ||
|
||
if (targetProp == null) { | ||
return targetProp; | ||
} | ||
|
||
if (targetProp instanceof Promise) { | ||
return new Promise((resolve, reject) => { | ||
Promise.resolve(targetProp) | ||
.then((value) => { | ||
resolve( | ||
(resolvedProps[prop] = | ||
typeof value !== 'object' || value == null | ||
? value | ||
: streamablePropsMemoProxy(value, { maxDepth: maxDepth - 1 })), | ||
); | ||
}) | ||
.catch((error: unknown) => | ||
reject(error instanceof Error ? error : new Error(String(error))), | ||
); | ||
}); | ||
} | ||
|
||
if (typeof targetProp !== 'object' || isReactElement(targetProp)) return targetProp; | ||
|
||
return (resolvedProps[prop] = streamablePropsMemoProxy(targetProp, { | ||
maxDepth: maxDepth - 1, | ||
})); | ||
}, | ||
}; | ||
|
||
return new Proxy<T>(props, proxyHandler); | ||
} | ||
|
||
export const useStreamablePropsMemo = <T extends object>(props: T, { maxDepth }: Options = {}) => | ||
useMemo( | ||
(): T => streamablePropsMemoProxy(props, { maxDepth }), | ||
// eslint-disable-next-line react-hooks/exhaustive-deps, @typescript-eslint/no-unsafe-assignment | ||
[...Object.entries(props).flatMap((e) => e), maxDepth], | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters