diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/index.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/index.tsx index 3730d1637a72b..ab58f89bb66fa 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/index.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/index.tsx @@ -1,3 +1,4 @@ +import { useHasEnTranslation } from '@automattic/i18n-utils'; import { useTranslate } from 'i18n-calypso'; import DocumentHead from 'calypso/components/data/document-head'; import UnifiedDesignPicker from './unified-design-picker'; @@ -8,8 +9,11 @@ import type { Step } from '../../types'; */ const DesignSetup: Step = ( props ) => { const translate = useTranslate(); + const hasEnTranslation = useHasEnTranslation(); - const headerText = translate( 'Pick a design' ); + const headerText = hasEnTranslation( 'Pick a theme' ) + ? translate( 'Pick a theme' ) + : translate( 'Pick a design' ); return ( <> diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/test/unified-design-picker.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/test/unified-design-picker.tsx index ebd930882c867..9ef6c43dfc6a2 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/test/unified-design-picker.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/test/unified-design-picker.tsx @@ -182,7 +182,7 @@ describe( 'UnifiedDesignPickerStep', () => { ); await waitFor( () => { - expect( screen.getByText( 'Pick a design' ) ).toBeInTheDocument(); + expect( screen.getByText( 'Pick a theme' ) ).toBeInTheDocument(); expect( container.getElementsByClassName( 'unified-design-picker__designs' ) ).toHaveLength( 1 ); diff --git a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/unified-design-picker.tsx b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/unified-design-picker.tsx index f6fcb5ab2f0a1..6cd0b779bb036 100644 --- a/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/unified-design-picker.tsx +++ b/client/landing/stepper/declarative-flow/internals/steps-repository/design-setup/unified-design-picker.tsx @@ -23,7 +23,7 @@ import { isAssemblerDesign, PERSONAL_THEME, } from '@automattic/design-picker'; -import { useLocale } from '@automattic/i18n-utils'; +import { useLocale, useHasEnTranslation } from '@automattic/i18n-utils'; import { StepContainer, DESIGN_FIRST_FLOW } from '@automattic/onboarding'; import { useSelect, useDispatch } from '@wordpress/data'; import { addQueryArgs } from '@wordpress/url'; @@ -130,6 +130,7 @@ const UnifiedDesignPickerStep: Step = ( { navigation, flow, stepName } ) => { const translate = useTranslate(); const locale = useLocale(); + const hasEnTranslation = useHasEnTranslation(); const { intent, goals } = useSelect( ( select ) => { const onboardStore = select( ONBOARD_STORE ) as OnboardSelect; @@ -914,7 +915,11 @@ const UnifiedDesignPickerStep: Step = ( { navigation, flow, stepName } ) => { const heading = ( { const translate = useTranslate(); - return { translate( 'No designs matched.' ) }; + return { translate( 'No themes matched.' ) }; }; export default NoResults; diff --git a/packages/design-picker/src/components/style.scss b/packages/design-picker/src/components/style.scss index 290a2568e80e1..40da49b7f5f1f 100644 --- a/packages/design-picker/src/components/style.scss +++ b/packages/design-picker/src/components/style.scss @@ -76,16 +76,28 @@ } } - .design-picker__grid, - .design-picker__grid-minimal { - flex: 5; - } + .design-picker__design-card-group { + .design-picker__design-card-title { + position: sticky; + top: 0; + padding: 16px 12px; + margin: 0 -12px 8px; + color: var(--studio-black); + background-color: var(--color-body-background); + font-size: $font-body; + font-style: normal; + font-weight: 500; + line-height: 24px; + z-index: 10; + } - .design-picker__grid { - margin: 0 -12px 30px; + .design-picker__grid { + margin-bottom: 24px; + } } - .design-picker__grid-minimal { + .design-picker__grid { + flex: 5; margin: 0 -12px 30px; } @@ -144,23 +156,6 @@ } } - .design-picker__grid-minimal { - display: grid; - grid-template-columns: 1fr; - row-gap: 36px; - margin: 0 0 30px; - - @include break-medium { - grid-template-columns: 1fr 1fr; - column-gap: 24px; - } - - @include break-xlarge { - grid-template-columns: 1fr 1fr 1fr; - column-gap: 32px; - } - } - .design-button-container { width: auto; margin: 0; @@ -425,7 +420,7 @@ .design-picker__grid { @supports ( display: grid ) { - row-gap: 32px; + row-gap: 24px; @include break-medium { grid-template-columns: 1fr 1fr 1fr; diff --git a/packages/design-picker/src/components/unified-design-picker.tsx b/packages/design-picker/src/components/unified-design-picker.tsx index b86345449f082..097f6bdf44965 100644 --- a/packages/design-picker/src/components/unified-design-picker.tsx +++ b/packages/design-picker/src/components/unified-design-picker.tsx @@ -2,9 +2,9 @@ import { recordTracksEvent } from '@automattic/calypso-analytics'; import clsx from 'clsx'; import { useTranslate } from 'i18n-calypso'; import { useCallback, useMemo, useRef, useState } from 'react'; -import { useInView } from 'react-intersection-observer'; +import { InView } from 'react-intersection-observer'; import { SHOW_ALL_SLUG } from '../constants'; -import { useFilteredDesigns } from '../hooks/use-filtered-designs'; +import { useFilteredDesignsByGroup } from '../hooks/use-filtered-designs'; import { isDefaultGlobalStylesVariationSlug, isFeatureCategory, @@ -170,6 +170,78 @@ const DesignCard: React.FC< DesignCardProps > = ( { ); }; +interface DesignCardGroup { + title?: string | React.ReactNode; + designs: Design[]; + locale: string; + category?: string | null; + isPremiumThemeAvailable?: boolean; + shouldLimitGlobalStyles?: boolean; + oldHighResImageLoading?: boolean; // Temporary for A/B test. + showActiveThemeBadge?: boolean; + siteActiveTheme?: string | null; + showNoResults?: boolean; + onChangeVariation: ( design: Design, variation?: StyleVariation ) => void; + onPreview: ( design: Design, variation?: StyleVariation ) => void; + getBadge: ( themeId: string, isLockedStyleVariation: boolean ) => React.ReactNode; +} + +const DesignCardGroup = ( { + title, + designs, + category, + locale, + isPremiumThemeAvailable, + shouldLimitGlobalStyles, + oldHighResImageLoading, + showActiveThemeBadge = false, + siteActiveTheme, + showNoResults, + onChangeVariation, + onPreview, + getBadge, +}: DesignCardGroup ) => { + const content = ( +
+ { designs.map( ( design, index ) => { + return ( + + ); + } ) } + { showNoResults && designs.length === 0 && } +
+ ); + + if ( ! showNoResults && designs.length === 0 ) { + return null; + } + + if ( ! title || designs.length === 0 ) { + return content; + } + + return ( +
+
+ { title } ({ designs.length }) +
+ { content } +
+ ); +}; + interface DesignPickerFilterGroupProps { title?: string; grow?: boolean; @@ -222,18 +294,35 @@ const DesignPicker: React.FC< DesignPickerProps > = ( { isMultiFilterEnabled = false, onChangeTier, } ) => { - const filteredDesigns = useFilteredDesigns( designs ); + const translate = useTranslate(); + const { all, best, ...designsByGroup } = useFilteredDesignsByGroup( designs ); + const categories = categorization?.categories || []; + const isNoResults = Object.values( designsByGroup ).every( + ( categoryDesigns ) => categoryDesigns.length === 0 + ); const categoryTypes = useMemo( - () => ( categorization?.categories || [] ).filter( ( { slug } ) => isFeatureCategory( slug ) ), + () => categories.filter( ( { slug } ) => isFeatureCategory( slug ) ), [ categorization?.categories ] ); const categoryTopics = useMemo( - () => - ( categorization?.categories || [] ).filter( ( { slug } ) => ! isFeatureCategory( slug ) ), + () => categories.filter( ( { slug } ) => ! isFeatureCategory( slug ) ), [ categorization?.categories ] ); - const translate = useTranslate(); + const getCategoryName = ( value: string ) => + categories.find( ( { slug } ) => slug === value )?.name || ''; + + const designCardProps = { + locale, + isPremiumThemeAvailable, + shouldLimitGlobalStyles, + onChangeVariation, + onPreview, + getBadge, + oldHighResImageLoading, + showActiveThemeBadge, + siteActiveTheme, + }; return (
@@ -268,26 +357,34 @@ const DesignPicker: React.FC< DesignPickerProps > = ( { ) }
-
- { filteredDesigns.map( ( design, index ) => { - return ( - - ); - } ) } - { filteredDesigns.length === 0 && } -
+ { isMultiFilterEnabled && categorization && categorization.selections.length > 1 && ( + + ) } + { /* We want to show the last one on top first. */ } + { Object.entries( designsByGroup ) + .reverse() + .map( ( [ categorySlug, categoryDesigns ], index, array ) => ( + + ) ) } ); }; @@ -331,16 +428,6 @@ const UnifiedDesignPicker: React.FC< UnifiedDesignPickerProps > = ( { } ) => { const hasCategories = !! Object.keys( categorization?.categories || {} ).length; - const { ref } = useInView( { - onChange: ( inView ) => { - if ( inView ) { - onViewAllDesigns(); - } - }, - } ); - // eslint-disable-next-line wpcalypso/jsx-classname-namespace - const bottomAnchorContent =
; - return (
= ( { isMultiFilterEnabled={ isMultiFilterEnabled } onChangeTier={ onChangeTier } /> - { bottomAnchorContent } + inView && onViewAllDesigns() } />
); diff --git a/packages/design-picker/src/hooks/use-filtered-designs.ts b/packages/design-picker/src/hooks/use-filtered-designs.ts index 8823110fbc970..865246b6e5721 100644 --- a/packages/design-picker/src/hooks/use-filtered-designs.ts +++ b/packages/design-picker/src/hooks/use-filtered-designs.ts @@ -3,62 +3,63 @@ import { isBlankCanvasDesign } from '../utils/available-designs'; import { useDesignPickerFilters } from './use-design-picker-filters'; import type { Design } from '../types'; -const getDesignSlug = ( design: Design ) => design.recipe?.slug ?? design.slug; - // Returns designs that match the features, subjects and tiers. // Designs with `showFirst` are always included regardless of the selected features and subjects. -export const filterDesigns = ( +export const getFilteredDesignsByCategory = ( designs: Design[], categorySlugs: string[] | null | undefined, selectedDesignTier: string = '' -): Design[] => { - const categorySlugsSet = new Set( categorySlugs || [] ); +) => { const filteredDesigns = designs.filter( ( design ) => - ( design.showFirst || - categorySlugsSet.size === 0 || - design.categories.find( ( { slug } ) => categorySlugsSet.has( slug ) ) ) && ( ! selectedDesignTier || design.design_tier === selectedDesignTier ) && ! isBlankCanvasDesign( design ) ); - if ( categorySlugsSet.size > 1 ) { - const scores: { [ key: string ]: number } = filteredDesigns.reduce( - ( result, design ) => ( { - ...result, - [ getDesignSlug( design ) ]: design.categories.reduce( - ( sum, { slug } ) => sum + ( categorySlugsSet.has( slug ) ? 1 : 0 ), - 0 - ), - } ), - {} - ); + const filteredDesignsByCategory: { [ key: string ]: Design[] } = { + all: filteredDesigns, + best: [], + ...Object.fromEntries( ( categorySlugs || [] ).map( ( slug ) => [ slug, [] ] ) ), + }; - filteredDesigns.sort( ( a: Design, b: Design ) => { - const aScore = scores[ getDesignSlug( a ) ]; - const bScore = scores[ getDesignSlug( b ) ]; + // Return early if none of the category is selected. + if ( ! categorySlugs || categorySlugs.length === 0 ) { + return filteredDesignsByCategory; + } - if ( aScore > bScore ) { - return -1; - } else if ( aScore < bScore ) { - return 1; + // Get designs by the selected category. + const categorySlugsSet = new Set( categorySlugs ); + for ( let i = 0; i < filteredDesigns.length; i++ ) { + const design = filteredDesigns[ i ]; + let count = 0; + for ( let j = 0; j < design.categories.length; j++ ) { + const category = design.categories[ j ]; + if ( categorySlugsSet.has( category.slug ) ) { + filteredDesignsByCategory[ category.slug ].push( design ); + count++; } - return 0; - } ); + } + + // For designs that match all selected categories. + if ( count === categorySlugs.length ) { + filteredDesignsByCategory.best.push( design ); + } } - return filteredDesigns; + return filteredDesignsByCategory; }; -export const useFilteredDesigns = ( designs: Design[] ) => { +export const useFilteredDesignsByGroup = ( designs: Design[] ): { [ key: string ]: Design[] } => { const { selectedCategories, selectedDesignTier } = useDesignPickerFilters(); const filteredDesigns = useMemo( () => { if ( selectedCategories.length > 0 || selectedDesignTier ) { - return filterDesigns( designs, selectedCategories, selectedDesignTier ); + return getFilteredDesignsByCategory( designs, selectedCategories, selectedDesignTier ); } - return designs; + return { + all: designs, + }; }, [ designs, selectedCategories, selectedDesignTier ] ); return filteredDesigns;