diff --git a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx index 861b5b09d0c..ad300532b35 100644 --- a/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx +++ b/apps/journeys-admin/src/components/Editor/Toolbar/Items/ShareItem/ShareItem.tsx @@ -2,6 +2,7 @@ import Box from '@mui/material/Box' import Button from '@mui/material/Button' import CircularProgress from '@mui/material/CircularProgress' import Stack from '@mui/material/Stack' +import { SxProps, Theme } from '@mui/material/styles' import dynamic from 'next/dynamic' import { useRouter } from 'next/router' import { useTranslation } from 'next-i18next' @@ -51,6 +52,7 @@ interface ShareItemProps { handleKeepMounted?: () => void buttonVariant?: 'icon' | 'default' setHasOpenDialog?: (hasOpenDialog: boolean) => void + buttonProps?: ComponentProps } /** @@ -70,7 +72,8 @@ export function ShareItem({ handleCloseMenu, handleKeepMounted, buttonVariant = 'icon', - setHasOpenDialog + setHasOpenDialog, + buttonProps }: ShareItemProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { enqueueSnackbar } = useSnackbar() @@ -124,7 +127,7 @@ export function ShareItem({ variant={variant} label={t('Share')} onClick={handleShowMenu} - ButtonProps={{ variant: 'contained' }} + ButtonProps={{ variant: 'contained', ...buttonProps }} icon={buttonVariant === 'icon' ? : undefined} /> ({ ), - DoneScreen: ({ - handleScreenNavigation - }: { - handleScreenNavigation: (screen: string) => void - }) => ( + DoneScreen: () => (

Done Screen

-
) })) @@ -784,90 +774,6 @@ describe('MultiStepForm', () => { }) }) - describe('Edit Manually button', () => { - it('should hide edit manually button when on the language screen', () => { - const journey = { - id: 'test-journey-id' - } as unknown as Journey - - setRouterQuery({ journeyId: 'test-journey-id' }) - - render( - - - - - - - - ) - - const editButton = screen.getByText('Edit Manually') - expect(editButton).toHaveStyle('visibility: hidden') - }) - - it('should show edit manually button when on any screen after the first screen', () => { - const journey = { - id: 'test-journey-id' - } as unknown as Journey - - setRouterQuery({ journeyId: 'test-journey-id' }) - - const { rerender } = render( - - - - - - - - ) - - const editButton = screen.getByText('Edit Manually') - expect(editButton).toHaveStyle('visibility: hidden') - - fireEvent.click(screen.getByTestId('language-next')) - setRouterQuery({ journeyId: 'test-journey-id', screen: 'social' }) - rerender( - - - - - - - - ) - const editButtonAfterRerender = screen.getByText('Edit Manually') - expect(editButtonAfterRerender).toHaveStyle('visibility: visible') - expect(editButtonAfterRerender).toHaveAttribute( - 'href', - '/journeys/test-journey-id' - ) - }) - - it('should disable edit manually button if journey is not found', () => { - const journey = { - id: null - } as unknown as Journey - - setRouterQuery({ journeyId: 'test-journey-id' }) - - render( - - - - - - - - ) - expect(screen.getByText('Edit Manually')).toHaveAttribute( - 'aria-disabled', - 'true' - ) - }) - }) - describe('progress stepper', () => { it('should not render progress stepper when journey has no customization capabilities', async () => { const journeyWithNoCapabilities = { @@ -1171,55 +1077,6 @@ describe('MultiStepForm', () => { }) }) - it('should call router.replace with correct URL when handleScreenNavigation is called', () => { - const journeyWithAllCapabilities = { - ...journey, - journeyCustomizationDescription: 'Hello {{ firstName: John }}!', - journeyCustomizationFields: [ - { - id: '1', - key: 'firstName', - value: 'John', - __typename: 'JourneyCustomizationField' - } - ], - blocks: [ - { - __typename: 'ButtonBlock', - id: '1', - label: 'Test Button', - action: { - __typename: 'LinkAction', - url: 'https://wa.me/123', - customizable: true, - parentStepId: null - } - } - ] - } as unknown as Journey - - const journeyId = journeyWithAllCapabilities.id - setRouterQuery({ journeyId, screen: 'done' }) - - render( - - - - - - - - ) - - expect(screen.getByTestId('done-screen')).toBeInTheDocument() - fireEvent.click(screen.getByTestId('done-screen-go-to-language')) - - expect(mockReplace).toHaveBeenCalledTimes(1) - expect(mockReplace).toHaveBeenCalledWith( - `/templates/${journeyId}/customize?screen=language` - ) - }) - it('should call router.replace with override journey id and next screen when handleNext(overrideJourneyId) is called', () => { const journeyWithAllCapabilities = { ...journey, diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx index e7f4812b22a..efe22e7e6e6 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/MultiStepForm.tsx @@ -1,8 +1,6 @@ import Box from '@mui/material/Box' -import Button from '@mui/material/Button' import Container from '@mui/material/Container' import Stack from '@mui/material/Stack' -import NextLink from 'next/link' import { useRouter } from 'next/router' import { useUser } from 'next-firebase-auth' import { useTranslation } from 'next-i18next' @@ -10,7 +8,6 @@ import { ReactElement, useMemo } from 'react' import { useJourney } from '@core/journeys/ui/JourneyProvider' import { useFlags } from '@core/shared/ui/FlagsProvider' -import Edit3 from '@core/shared/ui/icons/Edit3' import { CUSTOMIZE_SCREEN_QUERY_KEY, @@ -38,42 +35,21 @@ export const MULTI_STEP_FORM_MIN_HEIGHT = 900 function renderScreen( screen: CustomizationScreen, - handleNext: (overrideJourneyId?: string) => void, - handleScreenNavigation: (screen: CustomizationScreen) => void + handleNext: (overrideJourneyId?: string) => void ): ReactElement { switch (screen) { case 'language': - return ( - - ) + return case 'text': - return ( - - ) + return case 'links': - return ( - - ) + return case 'media': return case 'social': - return ( - - ) + return case 'done': - return + return default: return <> } @@ -89,7 +65,6 @@ export function MultiStepForm(): ReactElement { const firebaseUserLoaded = user?.firebaseUser != null const isAnon = user?.firebaseUser?.isAnonymous ?? false const journeyId = journey?.id ?? '' - const link = `/journeys/${journeyId}` const { screens, @@ -130,16 +105,11 @@ export function MultiStepForm(): ReactElement { ) } - async function handleScreenNavigation( - screen: CustomizationScreen - ): Promise { - void router.replace(buildCustomizeUrl(journeyId, screen, undefined)) - } - return ( - - - - + {(hasEditableText || hasCustomizableLinks || hasCustomizableMedia) && ( @@ -182,17 +132,7 @@ export function MultiStepForm(): ReactElement { /> )} - - - {renderScreen(activeScreen, handleNext, handleScreenNavigation)} - + {renderScreen(activeScreen, handleNext)} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx index d002acd995b..b4000cd53cb 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.spec.tsx @@ -128,7 +128,7 @@ describe('DoneScreen', () => { ) - expect(screen.getAllByText('Ready to Share!')).toHaveLength(2) + expect(screen.getAllByText('Ready to share!')).toHaveLength(2) }) it('renders first card of journey as preview', async () => { diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx index 9fc51812375..aa4cdc4fbb7 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/DoneScreen/DoneScreen.tsx @@ -1,4 +1,3 @@ -import Box from '@mui/material/Box' import Button from '@mui/material/Button' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' @@ -15,15 +14,9 @@ import ArrowRightContained1Icon from '@core/shared/ui/icons/ArrowRightContained1 import Play3Icon from '@core/shared/ui/icons/Play3' import { ShareItem } from '../../../../Editor/Toolbar/Items/ShareItem' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' +import { ScreenWrapper } from '../ScreenWrapper' -interface DoneScreenProps { - handleScreenNavigation?: (screen: CustomizationScreen) => void -} - -export function DoneScreen({ - handleScreenNavigation -}: DoneScreenProps): ReactElement { +export function DoneScreen(): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { journey } = useJourney() const router = useRouter() @@ -39,98 +32,67 @@ export function DoneScreen({ } return ( - - - {t('Ready to Share!')} - - - {t('Ready to Share!')} - - - {t('Share your unique link on any platform.')} - - - {t('Share your unique link on any platform.')} - - + } + sx={{ mt: 4 }} + > + + {t('Go To Projects Dashboard')} + + + } + > {steps.length > 0 && ( )} - - - - } sx={{ - flex: 1, - minWidth: 0, - '& button': { width: '100% !important' } + borderWidth: 2, + borderRadius: 2, + height: 48, + width: { xs: '100%', sm: 216 }, + borderColor: 'secondary.light' }} > - - + {t('Preview')} + + - - + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx index c134365330b..7bfbdffe96a 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.spec.tsx @@ -177,7 +177,6 @@ const mockTeamCreate: MockedResponse = { describe('LanguageScreen', () => { let handleNext: jest.Mock - const handleScreenNavigation = jest.fn() let push: jest.Mock beforeEach(() => { @@ -226,10 +225,7 @@ describe('LanguageScreen', () => { value={{ journey: nonTemplateJourney, variant: 'customize' }} > - + @@ -273,10 +269,7 @@ describe('LanguageScreen', () => { - + @@ -377,10 +370,7 @@ describe('LanguageScreen', () => { value={{ journey: journeyWithFromTemplateId, variant: 'admin' }} > - + @@ -490,10 +480,7 @@ describe('LanguageScreen', () => { - + @@ -589,10 +576,7 @@ describe('LanguageScreen', () => { - + @@ -780,10 +764,7 @@ describe('LanguageScreen', () => { }} > - + @@ -845,10 +826,7 @@ describe('LanguageScreen', () => { - + @@ -901,18 +879,13 @@ describe('LanguageScreen', () => { ]} > - - - - - - - + + + + + ) @@ -933,10 +906,7 @@ describe('LanguageScreen', () => { - + diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx index f312d4282d7..db068ce2b66 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LanguageScreen/LanguageScreen.tsx @@ -1,3 +1,4 @@ +import Box from '@mui/material/Box' import FormControl from '@mui/material/FormControl' import Stack from '@mui/material/Stack' import Typography from '@mui/material/Typography' @@ -27,20 +28,19 @@ import { useGetParentTemplateJourneyLanguages } from '../../../../../libs/useGet import { useTeamCreateMutation } from '../../../../../libs/useTeamCreateMutation' import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' -import { CardsPreview } from '../LinksScreen/CardsPreview' +import { CardsPreview, EDGE_FADE_PX } from '../LinksScreen/CardsPreview' +import { ScreenWrapper } from '../ScreenWrapper' import { JourneyCustomizeTeamSelect } from './JourneyCustomizeTeamSelect' interface LanguageScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } const FORM_SM_BREAKPOINT_WIDTH = '390px' export function LanguageScreen({ - handleNext, - handleScreenNavigation + handleNext }: LanguageScreenProps): ReactElement { const { t } = useTranslation('journeys-ui') const { templateCustomizationGuestFlow } = useFlags() @@ -291,119 +291,110 @@ export function LanguageScreen({ } return ( - - - + {({ handleSubmit: formikHandleSubmit, setFieldValue, values }) => ( + formikHandleSubmit()} + disabled={ + templateCustomizationGuestFlow == null || + !templateCustomizationGuestFlow || + loading + } + ariaLabel={t('Next')} + /> + } > - {t("Let's Get Started!")} - - - {t('Get Started')} - - - {t('A few quick edits and your template will be ready to share.')} - - - {t("A few quick edits and it's ready to share!")} - - - - - {`'${journey?.title ?? ''}'`} - - - {steps.length > 0 && } - - - {({ handleSubmit, setFieldValue, values }) => ( -
- + + {`'${journey?.title ?? ''}'`} + + - - - {t('Select a language')} - - - {t('Select a language')} - - ({ - id: language?.id, - name: language?.name, - slug: language?.slug - }))} - onChange={(value) => setFieldValue('languageSelect', value)} - /> - {isSignedIn && ( - <> - - {t('Select a team')} - - - - {t('Select a team')} - - - - )} - handleSubmit()} - disabled={ - templateCustomizationGuestFlow == null || - !templateCustomizationGuestFlow || - loading - } - ariaLabel={t('Next')} - /> - - -
- )} -
-
+ {steps.length > 0 && } + +
+ + + + {t('Select a language')} + + + {t('Select a language')} + + ({ + id: language?.id, + name: language?.name, + slug: language?.slug + }))} + onChange={(value) => setFieldValue('languageSelect', value)} + /> + {isSignedIn && ( + <> + + {t('Select a team')} + + + + {t('Select a team')} + + + + )} + + +
+ + + )} + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx index 0bfa7fac69d..1e9b575e458 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/CardsPreview.tsx @@ -51,7 +51,7 @@ const IFRAME_SCALE = CONTAINER_WIDTH / FRAME_WIDTH const CONTAINER_HEIGHT = Math.round(FRAME_HEIGHT * IFRAME_SCALE) // Spacing and offsets -const EDGE_FADE_PX = 16 +export const EDGE_FADE_PX = 40 function CardsPreviewItem({ step }: CardsPreviewItemProps): ReactElement { const { journey } = useJourney() @@ -159,6 +159,7 @@ export function CardsPreview({ steps }: CardsPreviewProps): ReactElement { slidesPerView="auto" spaceBetween={12} slidesOffsetBefore={EDGE_FADE_PX} + slidesOffsetAfter={EDGE_FADE_PX} observer observeParents sx={{ diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts index 0330eeabf1f..6e5e3f1a2bf 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/CardsPreview/index.ts @@ -1 +1 @@ -export { CardsPreview } from './CardsPreview' +export { CardsPreview, EDGE_FADE_PX } from './CardsPreview' diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx index 1037b89175c..89bb26e4190 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.spec.tsx @@ -85,7 +85,6 @@ describe('LinksScreen', () => { // // // // @@ -133,10 +132,7 @@ describe('LinksScreen', () => { render( - + ) @@ -292,10 +288,7 @@ describe('LinksScreen', () => { - + ) @@ -401,10 +394,7 @@ describe('LinksScreen', () => { - + ) @@ -471,10 +461,7 @@ describe('LinksScreen', () => { variant: 'admin' }} > - + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx index ff41feeb824..22fffd3994e 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/LinksScreen/LinksScreen.tsx @@ -1,6 +1,4 @@ import { useMutation } from '@apollo/client' -import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { Formik, FormikHelpers, FormikProvider } from 'formik' import { useTranslation } from 'next-i18next' import { ReactElement, useMemo } from 'react' @@ -25,22 +23,18 @@ import { useBlockActionLinkUpdateMutation } from '../../../../../libs/useBlockAc import { useBlockActionPhoneUpdateMutation } from '../../../../../libs/useBlockActionPhoneUpdateMutation' import { JOURNEY_CHAT_BUTTON_UPDATE } from '../../../../Editor/Slider/Settings/CanvasDetails/JourneyAppearance/Chat/ChatOption/Details/Details' import { countries } from '../../../../Editor/Slider/Settings/CanvasDetails/Properties/controls/Action/PhoneAction/countriesList' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { getJourneyLinks } from '../../../utils/getJourneyLinks' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' import { CardsPreview } from './CardsPreview' import { LinksForm } from './LinksForm' interface LinksScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } -export function LinksScreen({ - handleNext, - handleScreenNavigation -}: LinksScreenProps): ReactElement { +export function LinksScreen({ handleNext }: LinksScreenProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') const { journey } = useJourney() const links = useMemo(() => getJourneyLinks(t, journey), [journey]) @@ -189,137 +183,94 @@ export function LinksScreen({ } return ( - - - - {t('Links')} - - - {t('Links')} - - - {t( - 'This content contains buttons linking to external sites. Check them and update the links below.' - )} - - - {t( - 'Buttons here point to external sites. Check and update the links.' - )} - - - - >((acc, link) => { - if (link.linkType === 'phone') { - const block = journey?.blocks?.find((b) => b.id === link.id) - const action = - (block as any)?.action?.__typename === 'PhoneAction' - ? ((block as any).action as JourneyPhoneAction) - : undefined - const country = - action?.countryCode != null - ? countries.find((c) => c.countryCode === action.countryCode) - : undefined - const callingCode = country?.callingCode ?? '+' - const ccDigits = callingCode.replace(/[^\d]/g, '') - const prefix = ccDigits === '' ? '' : `+${ccDigits}` - const local = (action?.phone ?? '').startsWith(prefix) - ? (action?.phone ?? '').slice(prefix.length) - : (action?.phone ?? '').replace(/^\+/, '') - acc[`${link.id}__cc`] = callingCode - acc[`${link.id}__local`] = local + >((acc, link) => { + if (link.linkType === 'phone') { + const block = journey?.blocks?.find((b) => b.id === link.id) + const action = + (block as any)?.action?.__typename === 'PhoneAction' + ? ((block as any).action as JourneyPhoneAction) + : undefined + const country = + action?.countryCode != null + ? countries.find((c) => c.countryCode === action.countryCode) + : undefined + const callingCode = country?.callingCode ?? '+' + const ccDigits = callingCode.replace(/[^\d]/g, '') + const prefix = ccDigits === '' ? '' : `+${ccDigits}` + const local = (action?.phone ?? '').startsWith(prefix) + ? (action?.phone ?? '').slice(prefix.length) + : (action?.phone ?? '').replace(/^\+/, '') + acc[`${link.id}__cc`] = callingCode + acc[`${link.id}__local`] = local + } else { + acc[link.id] = link.url ?? '' + } + return acc + }, {})} + validationSchema={object().shape( + links.reduce>>((acc, link) => { + if (link.linkType === 'email') { + acc[link.id] = string().email(t('Enter a valid email')) + } else if (link.linkType === 'phone') { + acc[`${link.id}__cc`] = string().test( + 'valid-cc', + t('Enter a valid calling code'), + (val) => { + if (val == null || val.trim() === '') return false + const normalized = val.startsWith('+') ? val : `+${val}` + return countries.some((c) => c.callingCode === normalized) + } + ) + acc[`${link.id}__local`] = string().test( + 'valid-local', + t('Enter a valid phone number'), + (val) => + val == null || + val.trim() === '' || + /^[0-9\s\-()]+$/.test(val.trim()) + ) } else { - acc[link.id] = link.url ?? '' + acc[link.id] = string().url(t('Enter a valid URL')) } return acc - }, {})} - validationSchema={object().shape( - links.reduce>>( - (acc, link) => { - if (link.linkType === 'email') { - acc[link.id] = string().email(t('Enter a valid email')) - } else if (link.linkType === 'phone') { - acc[`${link.id}__cc`] = string().test( - 'valid-cc', - t('Enter a valid calling code'), - (val) => { - if (val == null || val.trim() === '') return false - const normalized = val.startsWith('+') ? val : `+${val}` - return countries.some((c) => c.callingCode === normalized) - } - ) - acc[`${link.id}__local`] = string().test( - 'valid-local', - t('Enter a valid phone number'), - (val) => - val == null || - val.trim() === '' || - /^[0-9\s\-()]+$/.test(val.trim()) - ) - } else { - acc[link.id] = string().url(t('Enter a valid URL')) - } - return acc - }, - {} - ) - )} - validateOnSubmit={false} - onSubmit={handleFormSubmit} - validateOnMount - > - {(formik) => ( - + }, {}) + )} + onSubmit={handleFormSubmit} + validateOnMount + > + {(formik) => ( + + + } + > + - - - )} - - + + + )} + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.spec.tsx index 980226690ea..61048b4388c 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.spec.tsx @@ -140,9 +140,9 @@ describe('MediaScreen', () => { it('should render the MediaScreen', () => { renderMediaScreen() - expect(screen.getByText('Media')).toBeInTheDocument() + expect(screen.getAllByText('Media')[0]).toBeInTheDocument() expect( - screen.getByText('Personalize and manage your media assets') + screen.getAllByText('Personalize and manage your media assets')[0] ).toBeInTheDocument() expect(screen.getByTestId('ImagesSection')).toBeInTheDocument() expect(screen.getByTestId('VideosSection')).toBeInTheDocument() diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx index 2ba8f54673e..470037d11ba 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/MediaScreen.tsx @@ -1,6 +1,3 @@ -import Box from '@mui/material/Box' -import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement, useEffect, useState } from 'react' @@ -12,6 +9,7 @@ import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys import { getJourneyMedia } from '../../../utils/getJourneyMedia' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' import { useTemplateVideoUpload } from '../../TemplateVideoUploadProvider' +import { ScreenWrapper } from '../ScreenWrapper' import { CardsSection, @@ -68,46 +66,28 @@ export function MediaScreen({ handleNext }: MediaScreenProps): ReactElement { setSelectedCardBlockId(getCardBlockIdFromStep(step)) } return ( - - - - - {t('Media')} - - - {t('Personalize and manage your media assets')} - - - {showLogo && } - - {showImages && ( - - )} - {showVideos && } + handleNext()} ariaLabel={t('Next')} loading={hasActiveUploads} /> - - + } + > + {showLogo && } + + {showImages && ( + + )} + {showVideos && } + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx index c23b32ec903..dd26d14b5be 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/MediaScreen/Sections/CardsSection/CardsSection.tsx @@ -6,6 +6,7 @@ import { ReactElement } from 'react' import { TreeBlock } from '@core/journeys/ui/block' import { TemplateCardPreview } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview' +import { OVERFLOW_PX } from '@core/journeys/ui/TemplateView/TemplatePreviewTabs/TemplateCardPreview/templateCardPreviewConfig' import { GetJourney_journey_blocks_StepBlock as StepBlock } from '@core/journeys/ui/useJourneyQuery/__generated__/GetJourney' interface CardsSectionProps { @@ -37,13 +38,19 @@ export function CardsSection({ gap={4} data-testid="CardsSection" sx={{ - width: '100%' + width: '100%', + overflow: 'visible' }} > {t('Cards')} - + { + it('renders title and subtitle', () => { + render( + +
content
+
+ ) + + expect(screen.getByTestId('ScreenWrapper')).toBeInTheDocument() + expect(screen.getAllByText('Test Title')[0]).toBeInTheDocument() + expect(screen.getAllByText('Test Subtitle')[0]).toBeInTheDocument() + }) + + it('renders children', () => { + render( + +
child content
+
+ ) + + expect(screen.getByTestId('child')).toBeInTheDocument() + }) + + it('renders footer when provided', () => { + render( + footer content} + > +
content
+
+ ) + + expect(screen.getByTestId('footer')).toBeInTheDocument() + }) + + it('does not render footer when not provided', () => { + render( + +
content
+
+ ) + + expect(screen.queryByTestId('footer')).not.toBeInTheDocument() + }) + + it('renders mobileTitle when provided', () => { + render( + +
content
+
+ ) + + expect(screen.getByText('Desktop Title')).toBeInTheDocument() + expect(screen.getByText('Mobile Title')).toBeInTheDocument() + }) + + it('falls back to title when mobileTitle is not provided', () => { + render( + +
content
+
+ ) + + const titleElements = screen.getAllByText('Shared Title') + expect(titleElements).toHaveLength(2) + }) + + it('renders mobileSubtitle when provided', () => { + render( + +
content
+
+ ) + + expect(screen.getByText('Desktop Subtitle')).toBeInTheDocument() + expect(screen.getByText('Mobile Subtitle')).toBeInTheDocument() + }) + + it('falls back to subtitle when mobileSubtitle is not provided', () => { + render( + +
content
+
+ ) + + const subtitleElements = screen.getAllByText('Shared Subtitle') + expect(subtitleElements).toHaveLength(2) + }) +}) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx new file mode 100644 index 00000000000..b8e706ab079 --- /dev/null +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/ScreenWrapper.tsx @@ -0,0 +1,79 @@ +import Stack from '@mui/material/Stack' +import Typography from '@mui/material/Typography' +import { ReactElement, ReactNode } from 'react' + +interface ScreenWrapperProps { + title: string + mobileTitle?: string + subtitle: string + mobileSubtitle?: string + footer?: ReactNode + children: ReactNode +} + +/** + * Wraps a multi-step form screen with a responsive title, subtitle, and optional footer. + * + * @param title - The heading displayed on desktop viewports. + * @param mobileTitle - Optional heading override for mobile viewports. Falls back to `title`. + * @param subtitle - The subheading displayed on desktop viewports. + * @param mobileSubtitle - Optional subheading override for mobile viewports. Falls back to `subtitle`. + * @param footer - Optional content rendered below the children. + * @param children - The main screen content. + */ +export function ScreenWrapper({ + title, + mobileTitle, + subtitle, + mobileSubtitle, + footer, + children +}: ScreenWrapperProps): ReactElement { + return ( + + + + {title} + + + {mobileTitle ?? title} + + + {subtitle} + + + {mobileSubtitle ?? subtitle} + + + {children} + {footer} + + ) +} diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts new file mode 100644 index 00000000000..dfa2c06cdb7 --- /dev/null +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/ScreenWrapper/index.ts @@ -0,0 +1 @@ +export { ScreenWrapper } from './ScreenWrapper' diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.spec.tsx index 18f332516d1..f960366511e 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.spec.tsx @@ -11,7 +11,6 @@ import { SocialScreen } from './SocialScreen' describe('SocialScreen', () => { const handleNext = jest.fn() - const handleScreenNavigation = jest.fn() const baseJourney = { ...journey, @@ -23,7 +22,6 @@ describe('SocialScreen', () => { beforeEach(() => { jest.clearAllMocks() handleNext.mockClear() - handleScreenNavigation.mockClear() }) const renderSocialScreen = ( @@ -32,10 +30,7 @@ describe('SocialScreen', () => { return render( - + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx index d054a95f5bb..e61b47aaa6c 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/SocialScreen/SocialScreen.tsx @@ -1,65 +1,33 @@ import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement } from 'react' import { DescriptionEdit } from '../../../../Editor/Slider/Settings/SocialDetails/DescriptionEdit' import { TitleEdit } from '../../../../Editor/Slider/Settings/SocialDetails/TitleEdit' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' import { SocialScreenSocialImage } from './SocialScreenSocialImage' interface SocialScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } -export function SocialScreen({ - handleNext, - handleScreenNavigation -}: SocialScreenProps): ReactElement { +export function SocialScreen({ handleNext }: SocialScreenProps): ReactElement { const { t } = useTranslation('apps-journeys-admin') return ( - handleNext()} + ariaLabel={t('Done')} + /> + } > - - {t('Final Details')} - - - {t('Final Details')} - - - {t('Customize how your invite appears when shared on social media.')} - - - {t('This is how your content will appear when shared on social media.')} - - handleNext()} - ariaLabel={t('Done')} - /> - + ) } diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx index 2266102a879..0242ccec144 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.spec.tsx @@ -38,10 +38,7 @@ describe('TextScreen', () => { render( - + ) @@ -55,10 +52,7 @@ describe('TextScreen', () => { render( - + ) @@ -115,10 +109,7 @@ describe('TextScreen', () => { render( - + ) @@ -147,10 +138,7 @@ describe('TextScreen', () => { render( - + ) diff --git a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx index 0c70d8d5e8b..86860b6274f 100644 --- a/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx +++ b/apps/journeys-admin/src/components/TemplateCustomization/MultiStepForm/Screens/TextScreen/TextScreen.tsx @@ -1,7 +1,5 @@ import { gql, useMutation } from '@apollo/client' import Box from '@mui/material/Box' -import Stack from '@mui/material/Stack' -import Typography from '@mui/material/Typography' import { useTranslation } from 'next-i18next' import { ReactElement, useCallback, useEffect, useState } from 'react' @@ -9,8 +7,8 @@ import { useJourney } from '@core/journeys/ui/JourneyProvider' import { GetJourney_journey_journeyCustomizationFields as JourneyCustomizationField } from '../../../../../../__generated__/GetJourney' import { JourneyCustomizationFieldUpdate } from '../../../../../../__generated__/JourneyCustomizationFieldUpdate' -import { CustomizationScreen } from '../../../utils/getCustomizeFlowConfig' import { CustomizeFlowNextButton } from '../../CustomizeFlowNextButton' +import { ScreenWrapper } from '../ScreenWrapper' export const JOURNEY_CUSTOMIZATION_FIELD_UPDATE = gql` mutation JourneyCustomizationFieldUpdate( @@ -126,13 +124,9 @@ const renderEditableText = ( interface TextScreenProps { handleNext: (overrideJourneyId?: string) => void - handleScreenNavigation: (screen: CustomizationScreen) => void } -export function TextScreen({ - handleNext, - handleScreenNavigation -}: TextScreenProps): ReactElement { +export function TextScreen({ handleNext }: TextScreenProps): ReactElement { const { t } = useTranslation() const { journey } = useJourney() const [journeyCustomizationFieldUpdate, { loading: isSubmitting }] = @@ -194,60 +188,21 @@ export function TextScreen({ } return ( - + } > - - - {t('Text')} - - - {t('Text')} - - - {t( - "Fill out the blue fields and we'll customize the content with your information." - )} - - - {t('Fill in the blue fields to customize the content.')} - - - - + ) } diff --git a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx index dc383c7599e..a2aa3310529 100644 --- a/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx +++ b/libs/journeys/ui/src/components/TemplateView/TemplatePreviewTabs/TemplateCardPreview/TemplateCardPreview.tsx @@ -15,6 +15,7 @@ import { GetJourney_journey_blocks_StepBlock as StepBlock } from '../../../../li import { TemplateActionButton } from '../../TemplateViewHeader/TemplateActionButton/TemplateActionButton' import { + type BreakpointSwiperOptions, SELECTED_SCALE, type TemplateCardPreviewVariant, VARIANT_CONFIGS @@ -64,6 +65,16 @@ function TemplateCardPreviewPlaceholder({ const StyledSwiperSlide = styled(SwiperSlide)(() => ({})) const StyledSwiper = styled(Swiper)(() => ({})) +function getSpacerWidth( + cardWidth: number, + bp: BreakpointSwiperOptions +): string { + const selectedWidth = cardWidth * SELECTED_SCALE + const space = bp.spaceBetween ?? 0 + const offset = bp.slidesOffsetBefore ?? 0 + return `calc(100% - ${selectedWidth}px - ${space}px - ${offset}px)` +} + /** * Horizontal carousel of template step cards with optional "more cards" slide. * @@ -165,6 +176,17 @@ export function TemplateCardPreview({ ) })} + {variant === 'media' && ( + + )} {showMoreCardsSlide && steps.length > slidesToRender.length && ( export interface VariantConfig { @@ -31,6 +31,7 @@ export interface VariantConfig { } export const SELECTED_SCALE = 1.07 +export const OVERFLOW_PX = 40 const MEDIA_CARD_HEIGHT = 209 const PREVIEW_CARD_HEIGHT_XS = 295 const PREVIEW_CARD_HEIGHT_SM = 404 @@ -47,8 +48,8 @@ const PREVIEW_VARIANT_CONFIG: VariantConfig = { borderRadius: 4 }, breakpoints: { - xs: { spaceBetween: 12, slidesOffsetAfter: 0 }, - sm: { spaceBetween: 28, slidesOffsetAfter: 0 } + xs: { spaceBetween: 12 }, + sm: { spaceBetween: 28 } }, cardSx: { position: 'relative', @@ -91,8 +92,8 @@ const MEDIA_VARIANT_CONFIG: VariantConfig = { borderRadius: '24px' }, breakpoints: { - xs: { spaceBetween: 12, slidesOffsetAfter: 265 }, - sm: { spaceBetween: 12, slidesOffsetAfter: 260 } + xs: { spaceBetween: 12, slidesOffsetBefore: 0 }, + sm: { spaceBetween: 12, slidesOffsetBefore: OVERFLOW_PX } }, swiperProps: { mousewheel: { forceToAxis: true }, @@ -118,7 +119,8 @@ const MEDIA_VARIANT_CONFIG: VariantConfig = { borderRadius: '12px' }, swiperSx: { - overflow: 'visible', + width: '100%', + overflow: { xs: 'visible', sm: 'hidden' }, zIndex: 2, '& .swiper-wrapper': { alignItems: 'center' diff --git a/libs/locales/en/apps-journeys-admin.json b/libs/locales/en/apps-journeys-admin.json index 509a70ca789..75760ae1842 100644 --- a/libs/locales/en/apps-journeys-admin.json +++ b/libs/locales/en/apps-journeys-admin.json @@ -1004,21 +1004,20 @@ "Link to project": "Link to project", "Feature Video Started": "Feature Video Started", "Feature Video Ended": "Feature Video Ended", - "Edit Manually": "Edit Manually", - "Ready to Share!": "Ready to Share!", + "Ready to share!": "Ready to share!", "Share your unique link on any platform.": "Share your unique link on any platform.", "Go To Projects Dashboard": "Go To Projects Dashboard", "Share!": "Share!", "{{count}} more cards_one": "{{count}} more cards", "{{count}} more cards_other": "{{count}} more cards", "Edit": "Edit", - "Links": "Links", - "This content contains buttons linking to external sites. Check them and update the links below.": "This content contains buttons linking to external sites. Check them and update the links below.", - "Buttons here point to external sites. Check and update the links.": "Buttons here point to external sites. Check and update the links.", "Enter a valid email": "Enter a valid email", "Enter a valid calling code": "Enter a valid calling code", "Enter a valid phone number": "Enter a valid phone number", "Enter a valid URL": "Enter a valid URL", + "Links": "Links", + "This content contains buttons linking to external sites. Check them and update the links below.": "This content contains buttons linking to external sites. Check them and update the links below.", + "Buttons here point to external sites. Check and update the links.": "Buttons here point to external sites. Check and update the links.", "Next": "Next", "Replace the links": "Replace the links", "Media": "Media", @@ -1031,9 +1030,8 @@ "Supports JPG, PNG, and GIF files.": "Supports JPG, PNG, and GIF files.", "Upload a video to see a preview here": "Upload a video to see a preview here", "Max size is 1 GB": "Max size is 1 GB", - "Final Details": "Final Details", - "Customize how your invite appears when shared on social media.": "Customize how your invite appears when shared on social media.", - "This is how your content will appear when shared on social media.": "This is how your content will appear when shared on social media.", + "Social Media": "Social Media", + "This is how your content will look on social media.": "This is how your content will look on social media.", "Failed to update social image, please try again later": "Failed to update social image, please try again later", "Social image updated": "Social image updated", "your.nextstep.is": "your.nextstep.is", diff --git a/libs/locales/en/journeys-ui.json b/libs/locales/en/journeys-ui.json index fb9bb927809..9ef4a8b85dc 100644 --- a/libs/locales/en/journeys-ui.json +++ b/libs/locales/en/journeys-ui.json @@ -7,7 +7,7 @@ "Get Started": "Get Started", "A few quick edits and your template will be ready to share.": "A few quick edits and your template will be ready to share.", "A few quick edits and it's ready to share!": "A few quick edits and it's ready to share!", + "Next": "Next", "Select a language": "Select a language", - "Select a team": "Select a team", - "Next": "Next" + "Select a team": "Select a team" }