Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -212,14 +212,15 @@
compareLabel={t('Compare.compare')}
compareProducts={streamableCompareProducts}
emptyStateSubtitle={t('Brand.Empty.subtitle')}
promotionCalloutsMoreLabel={t('Brand.PromotionCallouts.more')}
emptyStateTitle={t('Brand.Empty.title')}

Check warning on line 216 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
filterLabel={t('FacetedSearch.filters')}

Check warning on line 217 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
filters={streamableFilters}

Check warning on line 218 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
filtersPanelTitle={t('FacetedSearch.filters')}

Check warning on line 219 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
maxCompareLimitMessage={t('Compare.maxCompareLimit')}

Check warning on line 220 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
maxItems={MAX_COMPARE_LIMIT}

Check warning on line 221 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
paginationInfo={streamablePagination}

Check warning on line 222 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
products={streamableProducts}

Check warning on line 223 in core/app/[locale]/(default)/(faceted)/brand/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
rangeFilterApplyLabel={t('FacetedSearch.Range.apply')}
removeLabel={t('Compare.remove')}
resetFiltersLabel={t('FacetedSearch.resetFilters')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,9 @@
<ProductsListSection
breadcrumbs={breadcrumbs}
compareLabel={t('Compare.compare')}
promotionCalloutsMoreLabel={t('Category.PromotionCallouts.more')}
compareProducts={streamableCompareProducts}

Check warning on line 255 in core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
emptyStateSubtitle={t('Category.Empty.subtitle')}

Check warning on line 256 in core/app/[locale]/(default)/(faceted)/category/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Props should be sorted alphabetically
emptyStateTitle={t('Category.Empty.title')}
filterLabel={t('FacetedSearch.filters')}
filters={streamableFilters}
Expand Down
1 change: 1 addition & 0 deletions core/app/[locale]/(default)/(faceted)/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ export default async function Search(props: Props) {
compareLabel={t('Compare.compare')}
compareProducts={streamableCompareProducts}
emptyStateSubtitle={t('Search.Empty.subtitle')}
promotionCalloutsMoreLabel={t('Search.PromotionCallouts.more')}
emptyStateTitle={streamableEmptyStateTitle}
filterLabel={t('FacetedSearch.filters')}
filters={streamableFilters}
Expand Down
4 changes: 4 additions & 0 deletions core/app/[locale]/(default)/product/[slug]/page-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,10 @@ const ProductQuery = graphql(
numberOfReviews
}
description
featuredPromotions {
entityId
text
}
...ProductOptionsFragment
}
}
Expand Down
10 changes: 10 additions & 0 deletions core/app/[locale]/(default)/product/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,15 @@
};
});

const streamablePromotionCallouts = Streamable.from(async () => {

Check failure on line 434 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Async arrow function has no 'await' expression

Check failure on line 434 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe assignment of an error typed value
return (

Check failure on line 435 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe return of a value of type error
baseProduct.featuredPromotions?.map((p) => ({

Check failure on line 436 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe call of a(n) `error` type typed value
id: p.entityId.toString(),

Check failure on line 437 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe member access .entityId on an `any` value

Check failure on line 437 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe call of a(n) `any` typed value

Check failure on line 437 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe assignment of an `any` value
text: p.text,

Check failure on line 438 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe member access .text on an `any` value

Check failure on line 438 in core/app/[locale]/(default)/product/[slug]/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Typecheck, and gql.tada

Unsafe assignment of an `any` value
})) ?? []
);
});

const streameableAccordions = Streamable.from(async () => {
const product = await streamableProduct;

Expand Down Expand Up @@ -583,6 +592,7 @@
maxQuantity: streamableMaxQuantity,
stockDisplayData: streamableStockDisplayData,
backorderDisplayData: streamableBackorderDisplayData,
promotionCallouts: streamablePromotionCallouts,
}}
quantityLabel={t('ProductDetails.quantity')}
recaptchaSiteKey={recaptchaSiteKey}
Expand Down
4 changes: 4 additions & 0 deletions core/components/product-card/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export const ProductCardFragment = graphql(
}
}
}
featuredPromotions {
entityId
text
}
...PricingFragment
}
`,
Expand Down
7 changes: 7 additions & 0 deletions core/data-transformers/product-card-transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export const singleProductCardTransformer = (
'variants' in product
? getInventoryMessage(product, outOfStockMessage, showBackorderMessage)
: undefined,
promotionCallouts:
'featuredPromotions' in product
? product.featuredPromotions.map((p) => ({
id: p.entityId.toString(),
text: p.text,
}))
: [],
};
};

Expand Down
56 changes: 56 additions & 0 deletions core/lib/makeswift/components/product-card/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import { useLocale } from 'next-intl';
import useSWR from 'swr';

import {
BcProductSchema,
useBcProductToVibesProduct,
} from '~/lib/makeswift/utils/use-bc-product-to-vibes-product/use-bc-product-to-vibes-product';
import { ProductCard, ProductCardSkeleton } from '~/vibes/soul/primitives/product-card';

interface Props {
className?: string;
entityId?: string;
aspectRatio: '1:1' | '5:6' | '3:4';
colorScheme: 'light' | 'dark';
badge: { show: boolean; text: string };
showCompare?: boolean;
showPromotionCallouts?: boolean;
}

export function MakeswiftProductCard({
className,
entityId,
badge,
showPromotionCallouts = true,
...props
}: Props) {
const bcProductToVibesProduct = useBcProductToVibesProduct();
const locale = useLocale();
const { data, isLoading } = useSWR(
entityId ? `/api/products/${entityId}?locale=${locale}` : null,
async (url) =>
fetch(url)
.then((r) => r.json())
.then(BcProductSchema.parse),
);

if (entityId == null || isLoading || data == null) {
return <ProductCardSkeleton className={className} />;
}

const product = bcProductToVibesProduct(data);

return (
<ProductCard
className={className}
product={{
...product,
badge: badge.show ? badge.text : undefined,
promotionCallouts: showPromotionCallouts ? product.promotionCallouts : undefined,
}}
{...props}
/>
);
}
56 changes: 56 additions & 0 deletions core/lib/makeswift/components/product-card/register.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Checkbox, Combobox, Group, Select, Style, TextInput } from '@makeswift/runtime/controls';

import { runtime } from '~/lib/makeswift/runtime';

import { searchProducts } from '../../utils/search-products';

import { MakeswiftProductCard } from './client';

runtime.registerComponent(MakeswiftProductCard, {
type: 'catalog-product-card',
label: 'Catalog / Product Card',
props: {
className: Style(),
entityId: Combobox({
label: 'Product',
async getOptions(query) {
const products = await searchProducts(query);

return products.map((product) => ({
id: product.entityId.toString(),
label: product.name,
value: product.entityId.toString(),
}));
},
}),
aspectRatio: Select({
label: 'Image aspect ratio',
options: [
{ value: '1:1', label: 'Square' },
{ value: '5:6', label: '5:6' },
{ value: '3:4', label: '3:4' },
],
defaultValue: '5:6',
}),
colorScheme: Select({
label: 'Color scheme',
options: [
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' },
],
defaultValue: 'light',
}),
badge: Group({
label: 'Badge',
props: {
show: Checkbox({ label: 'Show badge', defaultValue: true }),
text: TextInput({ label: 'Badge text', defaultValue: 'New' }),
},
}),
showCompare: Checkbox({ label: 'Show compare', defaultValue: true }),
showPromotionCallouts: Checkbox({
label: 'Show promotion callouts',
defaultValue: true,
}),
},
});
125 changes: 125 additions & 0 deletions core/lib/makeswift/components/product-detail/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
'use client';

import React, {
type ComponentPropsWithoutRef,
createContext,
forwardRef,
type PropsWithChildren,
type ReactNode,
type Ref,
useCallback,
useContext,
} from 'react';

import { Stream, type Streamable } from '@/vibes/soul/lib/streamable';
import { ProductDetail, ProductDetailSkeleton } from '@/vibes/soul/sections/product-detail';
import { mergeSections } from '~/lib/makeswift/utils/merge-sections';

type VibesProductDetailProps = ComponentPropsWithoutRef<typeof ProductDetail>;
type VibesProductDetail = Exclude<Awaited<VibesProductDetailProps['product']>, null>;

export type ProductDetail = VibesProductDetail & {
plainTextDescription?: string;
};

export type Props = Omit<VibesProductDetailProps, 'product'> & {
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<VibesProductDetail['accordions']>, undefined>;
}

const ProductDetailImpl = ({
summaryText,
description,
accordions,
product: streamableProduct,
...props
}: Props & 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(
(
productAccordions: Awaited<ProductDetail['accordions']>,
): Awaited<ProductDetail['accordions']> =>
productAccordions != null
? mergeSections(productAccordions, accordions, (left, right) => ({
...left,
content: right.content,
}))
: undefined,
[accordions],
);

return (
<Stream fallback={<ProductDetailSkeleton />} value={streamableProduct}>
{(product) => (
<Stream fallback={<ProductDetailSkeleton />} value={product.accordions}>
{(productAccordions) => (
<ProductDetail
{...{
...props,
product: {
...product,
summary: summaryText,
description: getProductDescription(product),
accordions: getProductAccordions(productAccordions),
},
}}
/>
)}
</Stream>
)}
</Stream>
);
};

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>
);
},
);
41 changes: 41 additions & 0 deletions core/lib/makeswift/components/products-list/client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import { ComponentPropsWithoutRef } from 'react';

import { ProductList, ProductListSkeleton } from '@/vibes/soul/sections/product-list';

import { useProducts } from '../../utils/use-products';

type MSProductsListProps = Omit<ComponentPropsWithoutRef<typeof ProductList>, 'products'> & {
className: string;
collection: 'none' | 'best-selling' | 'newest' | 'featured';
limit: number;
additionalProducts: Array<{
entityId?: string;
}>;
};

export function MSProductsList({
className,
collection,
limit,
additionalProducts,
...props
}: MSProductsListProps) {
const additionalProductIds = additionalProducts.map(({ entityId }) => entityId ?? '');
const { products, isLoading } = useProducts({
collection,
collectionLimit: limit,
additionalProductIds,
});

if (isLoading) {
return <ProductListSkeleton className={className} />;
}

if (products == null || products.length === 0) {
return <ProductListSkeleton className={className} />;
}

return <ProductList {...props} className={className} products={products} />;
}
Loading
Loading