diff --git a/packages/twenty-front/.oxlintrc.json b/packages/twenty-front/.oxlintrc.json index 5c52c2c103..1529aace44 100644 --- a/packages/twenty-front/.oxlintrc.json +++ b/packages/twenty-front/.oxlintrc.json @@ -11,6 +11,7 @@ "src/generated-metadata", "src/generated-admin", "src/locales/generated", + "src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneDots.ts", "src/testing/mock-data", "**/__mocks__/**", "**/*.stories.ts", diff --git a/packages/twenty-front/__mocks__/workerMock.js b/packages/twenty-front/__mocks__/workerMock.js new file mode 100644 index 0000000000..54440cc48b --- /dev/null +++ b/packages/twenty-front/__mocks__/workerMock.js @@ -0,0 +1,6 @@ +export default class WorkerMock { + postMessage() {} + terminate() {} + addEventListener() {} + removeEventListener() {} +} diff --git a/packages/twenty-front/jest.config.mjs b/packages/twenty-front/jest.config.mjs index 82279f3fe8..6a09f82205 100644 --- a/packages/twenty-front/jest.config.mjs +++ b/packages/twenty-front/jest.config.mjs @@ -56,6 +56,7 @@ const jestConfig = { '\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$': '/__mocks__/imageMockFront.js', '\\.css$': '/__mocks__/styleMock.js', + '\\?worker$': '/__mocks__/workerMock.js', ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { prefix: '/', }), diff --git a/packages/twenty-front/public/images/onboarding/welcome-halftone.svg b/packages/twenty-front/public/images/onboarding/welcome-halftone.svg new file mode 100644 index 0000000000..01172ed046 --- /dev/null +++ b/packages/twenty-front/public/images/onboarding/welcome-halftone.svg @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx b/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx index ca5b611c2e..ec80c8f3ef 100644 --- a/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/WorkspaceAppProviders.tsx @@ -20,6 +20,7 @@ import { IsMinimalMetadataReadyEffect } from '@/metadata-store/effect-components import { MinimalMetadataLoadEffect } from '@/metadata-store/effect-components/MinimalMetadataLoadEffect'; import { UserMetadataProviderInitialEffect } from '@/metadata-store/effect-components/UserMetadataProviderInitialEffect'; import { ApolloCoreProvider } from '@/object-metadata/components/ApolloCoreProvider'; +import { WelcomeOverlay } from '@/onboarding/components/WelcomeOverlay/WelcomeOverlay'; import { ApolloAdminProvider } from '@/settings/admin-panel/apollo/components/ApolloAdminProvider'; import { EndTrialAfterPaymentMethodGater } from '@/settings/billing/components/EndTrialAfterPaymentMethodGater'; import { SSEProvider } from '@/sse-db-event/components/SSEProvider'; @@ -80,6 +81,7 @@ export const WorkspaceAppProviders = () => { + diff --git a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 6e277d833a..bf1b7b0e45 100644 --- a/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -1,5 +1,7 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksOnAnyLocationChange'; import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState'; +import { ONBOARDING_PATHS } from '@/auth/constants/OnboardingPaths'; +import { ONGOING_USER_CREATION_PATHS } from '@/auth/constants/OngoingUserCreationPaths'; import { useReturnToPath } from '@/auth/hooks/useReturnToPath'; import { useIsOnAuthOrOnboardingPage } from '@/auth/hooks/useIsOnAuthOrOnboardingPage'; import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu'; @@ -19,6 +21,9 @@ import { useActiveRecordTableRow } from '@/object-record/record-table/hooks/useA import { useFocusedRecordTableRow } from '@/object-record/record-table/hooks/useFocusedRecordTableRow'; import { useOpenNewRecordTitleCell } from '@/object-record/record-title-cell/hooks/useOpenNewRecordTitleCell'; import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; +import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; +import { isWelcomeAnimationVisibleState } from '@/onboarding/states/isWelcomeAnimationVisibleState'; +import { shouldShowWelcomeAnimationOnNavigate } from '@/onboarding/utils/shouldShowWelcomeAnimationOnNavigate'; import { PageFocusId } from '@/types/PageFocusId'; import { useResetFocusStackToFocusItem } from '@/ui/utilities/focus/hooks/useResetFocusStackToFocusItem'; import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType'; @@ -39,6 +44,12 @@ import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffect import { getPageLayoutIdForLocation } from '~/modules/app/utils/getPageLayoutIdForLocation'; import { isMatchingLocation } from '~/utils/isMatchingLocation'; +const ONBOARDING_OR_AUTH_NAVIGATE_PATHS = [ + ...ONBOARDING_PATHS, + ...ONGOING_USER_CREATION_PATHS, + AppPath.ResetPassword, +]; + // TODO: break down into smaller functions and / or hooks // - moved usePageChangeEffectNavigateLocation into dedicated hook export const PageChangeEffect = () => { @@ -95,6 +106,12 @@ export const PageChangeEffect = () => { const isOnAuthOrOnboardingPage = useIsOnAuthOrOnboardingPage(); + const onboardingStatus = useOnboardingStatus(); + + const isOnOnboardingPage = ONBOARDING_PATHS.some((appPath) => + isMatchingLocation(location, appPath), + ); + const closeSidePanelUnlessNotRelevant = useCallback(() => { const currentPage = store.get(sidePanelPageState.atom); @@ -152,6 +169,21 @@ export const PageChangeEffect = () => { const consumedReturnToPath = getReturnToPath() === pageChangeEffectNavigateLocation; + const isNavigatingToOnboardingOrAuthPath = + ONBOARDING_OR_AUTH_NAVIGATE_PATHS.some( + (appPath) => pageChangeEffectNavigateLocation === appPath, + ); + + if ( + shouldShowWelcomeAnimationOnNavigate({ + onboardingStatus, + isOnOnboardingPage, + isNavigatingToOnboardingOrAuthPath, + }) + ) { + store.set(isWelcomeAnimationVisibleState.atom, true); + } + navigate(pageChangeEffectNavigateLocation); if (consumedReturnToPath) { @@ -163,9 +195,12 @@ export const PageChangeEffect = () => { pageChangeEffectNavigateLocation, isAppEffectRedirectEnabled, isOnAuthOrOnboardingPage, + isOnOnboardingPage, + onboardingStatus, saveReturnToPath, getReturnToPath, clearReturnToPath, + store, ]); useEffect(() => { diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeAnimationAutoLeaveEffect.tsx b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeAnimationAutoLeaveEffect.tsx new file mode 100644 index 0000000000..6483abae10 --- /dev/null +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeAnimationAutoLeaveEffect.tsx @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; + +const WELCOME_HOLD_DURATION_MS = 2900; + +type WelcomeAnimationAutoLeaveEffectProps = { + onAutoLeave: () => void; +}; + +export const WelcomeAnimationAutoLeaveEffect = ({ + onAutoLeave, +}: WelcomeAnimationAutoLeaveEffectProps) => { + useEffect(() => { + const autoLeaveTimeoutId = setTimeout( + onAutoLeave, + WELCOME_HOLD_DURATION_MS, + ); + + return () => { + clearTimeout(autoLeaveTimeoutId); + }; + }, [onAutoLeave]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvas.tsx b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvas.tsx new file mode 100644 index 0000000000..1fd99d7783 --- /dev/null +++ b/packages/twenty-front/src/modules/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvas.tsx @@ -0,0 +1,40 @@ +import { styled } from '@linaria/react'; +import { useCallback, useRef, useState } from 'react'; + +import { WelcomeHalftoneCanvasEffect } from '@/onboarding/components/WelcomeOverlay/WelcomeHalftoneCanvasEffect'; + +import './welcomeHalftone.css'; + +const StyledCanvas = styled.canvas` + display: block; + height: 100%; + width: 100%; +`; + +type WelcomeHalftoneCanvasProps = { + isLeaving: boolean; +}; + +export const WelcomeHalftoneCanvas = ({ + isLeaving, +}: WelcomeHalftoneCanvasProps) => { + const canvasRef = useRef(null); + const [hasWorkerFailed, setHasWorkerFailed] = useState(false); + const markWorkerAsFailed = useCallback(() => setHasWorkerFailed(true), []); + + return ( + <> +