welcome.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {Fragment, useCallback, useEffect} from 'react';
  2. import styled from '@emotion/styled';
  3. import {motion, MotionProps} from 'framer-motion';
  4. import OnboardingInstall from 'sentry-images/spot/onboarding-install.svg';
  5. import OnboardingSetup from 'sentry-images/spot/onboarding-setup.svg';
  6. import {openInviteMembersModal} from 'sentry/actionCreators/modal';
  7. import {Button} from 'sentry/components/button';
  8. import Link from 'sentry/components/links/link';
  9. import {t, tct} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  12. import testableTransition from 'sentry/utils/testableTransition';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import FallingError from 'sentry/views/onboarding/components/fallingError';
  15. import WelcomeBackground from 'sentry/views/onboarding/components/welcomeBackground';
  16. import {StepProps} from './types';
  17. import {usePersistedOnboardingState} from './utils';
  18. const fadeAway: MotionProps = {
  19. variants: {
  20. initial: {opacity: 0},
  21. animate: {opacity: 1, filter: 'blur(0px)'},
  22. exit: {opacity: 0, filter: 'blur(1px)'},
  23. },
  24. transition: testableTransition({duration: 0.8}),
  25. };
  26. type TextWrapperProps = {
  27. cta: React.ReactNode;
  28. src: string;
  29. subText: React.ReactNode;
  30. title: React.ReactNode;
  31. };
  32. function InnerAction({title, subText, cta, src}: TextWrapperProps) {
  33. return (
  34. <Fragment>
  35. <ActionImage src={src} />
  36. <TextWrapper>
  37. <ActionTitle>{title}</ActionTitle>
  38. <SubText>{subText}</SubText>
  39. </TextWrapper>
  40. <ButtonWrapper>{cta}</ButtonWrapper>
  41. </Fragment>
  42. );
  43. }
  44. function TargetedOnboardingWelcome({jumpToSetupProject, ...props}: StepProps) {
  45. const organization = useOrganization();
  46. const [clientState, setClientState] = usePersistedOnboardingState();
  47. const source = 'targeted_onboarding';
  48. useEffect(() => {
  49. trackAdvancedAnalyticsEvent('growth.onboarding_start_onboarding', {
  50. organization,
  51. source,
  52. });
  53. });
  54. // Jump to setup project if the backend set this state for us
  55. useEffect(() => {
  56. if (clientState?.state === 'projects_selected') {
  57. jumpToSetupProject();
  58. }
  59. }, [clientState, jumpToSetupProject]);
  60. const handleComplete = useCallback(() => {
  61. trackAdvancedAnalyticsEvent('growth.onboarding_clicked_instrument_app', {
  62. organization,
  63. source,
  64. });
  65. setClientState({
  66. platformToProjectIdMap: clientState?.platformToProjectIdMap ?? {},
  67. selectedPlatforms: [],
  68. url: 'select-platform/',
  69. state: 'started',
  70. });
  71. props.onComplete();
  72. }, [organization, source, clientState, setClientState, props]);
  73. const handleSkipOnboarding = useCallback(() => {
  74. trackAdvancedAnalyticsEvent('growth.onboarding_clicked_skip', {
  75. organization,
  76. source,
  77. });
  78. setClientState({
  79. platformToProjectIdMap: clientState?.platformToProjectIdMap ?? {},
  80. selectedPlatforms: [],
  81. url: 'welcome/',
  82. state: 'skipped',
  83. });
  84. }, [organization, source, clientState, setClientState]);
  85. return (
  86. <FallingError>
  87. {({fallingError, fallCount, isFalling}) => (
  88. <Wrapper>
  89. <WelcomeBackground />
  90. <motion.h1 {...fadeAway} style={{marginBottom: space(0.5)}}>
  91. {t('Welcome to Sentry')}
  92. </motion.h1>
  93. <SubHeaderText style={{marginBottom: space(4)}} {...fadeAway}>
  94. {t(
  95. 'Your code is probably broken. Maybe not. Find out for sure. Get started below.'
  96. )}
  97. </SubHeaderText>
  98. <ActionItem {...fadeAway}>
  99. <InnerAction
  100. title={t('Install Sentry')}
  101. subText={t(
  102. 'Select your languages or frameworks and install the SDKs to start tracking issues'
  103. )}
  104. src={OnboardingInstall}
  105. cta={
  106. <Fragment>
  107. <ButtonWithFill onClick={handleComplete} priority="primary">
  108. {t('Start')}
  109. </ButtonWithFill>
  110. {(fallCount === 0 || isFalling) && (
  111. <PositionedFallingError>{fallingError}</PositionedFallingError>
  112. )}
  113. </Fragment>
  114. }
  115. />
  116. </ActionItem>
  117. <ActionItem {...fadeAway}>
  118. <InnerAction
  119. title={t('Set up my team')}
  120. subText={tct(
  121. 'Invite [friends] coworkers. You shouldn’t have to fix what you didn’t break',
  122. {friends: <Strike>{t('friends')}</Strike>}
  123. )}
  124. src={OnboardingSetup}
  125. cta={
  126. <ButtonWithFill
  127. onClick={() => {
  128. openInviteMembersModal({source});
  129. }}
  130. priority="primary"
  131. >
  132. {t('Invite Team')}
  133. </ButtonWithFill>
  134. }
  135. />
  136. </ActionItem>
  137. <motion.p style={{margin: 0}} {...fadeAway}>
  138. {t("Gee, I've used Sentry before.")}
  139. <br />
  140. <Link
  141. onClick={handleSkipOnboarding}
  142. to={`/organizations/${organization.slug}/issues/?referrer=onboarding-welcome-skip`}
  143. >
  144. {t('Skip onboarding.')}
  145. </Link>
  146. </motion.p>
  147. </Wrapper>
  148. )}
  149. </FallingError>
  150. );
  151. }
  152. export default TargetedOnboardingWelcome;
  153. const PositionedFallingError = styled('span')`
  154. display: block;
  155. position: absolute;
  156. right: 0px;
  157. top: 30px;
  158. `;
  159. const Wrapper = styled(motion.div)`
  160. position: relative;
  161. margin-top: auto;
  162. margin-bottom: auto;
  163. max-width: 400px;
  164. display: flex;
  165. flex-direction: column;
  166. align-items: center;
  167. text-align: center;
  168. margin-left: auto;
  169. margin-right: auto;
  170. h1 {
  171. font-size: 42px;
  172. }
  173. `;
  174. const ActionItem = styled(motion.div)`
  175. min-height: 120px;
  176. border-radius: ${space(0.5)};
  177. padding: ${space(2)};
  178. margin-bottom: ${space(2)};
  179. justify-content: space-around;
  180. border: 1px solid ${p => p.theme.gray200};
  181. @media (min-width: ${p => p.theme.breakpoints.small}) {
  182. display: grid;
  183. grid-template-columns: 125px auto 125px;
  184. width: 680px;
  185. align-items: center;
  186. }
  187. @media (max-width: ${p => p.theme.breakpoints.small}) {
  188. display: flex;
  189. flex-direction: column;
  190. }
  191. `;
  192. const TextWrapper = styled('div')`
  193. text-align: left;
  194. margin: auto ${space(3)};
  195. min-height: 70px;
  196. @media (max-width: ${p => p.theme.breakpoints.small}) {
  197. text-align: center;
  198. margin: ${space(1)} ${space(1)};
  199. margin-top: ${space(3)};
  200. }
  201. `;
  202. const Strike = styled('span')`
  203. text-decoration: line-through;
  204. `;
  205. const ActionTitle = styled('h5')`
  206. font-weight: 900;
  207. margin: 0 0 ${space(0.5)};
  208. color: ${p => p.theme.gray400};
  209. `;
  210. const SubText = styled('span')`
  211. font-weight: 400;
  212. color: ${p => p.theme.gray400};
  213. `;
  214. const SubHeaderText = styled(motion.h6)`
  215. color: ${p => p.theme.gray300};
  216. `;
  217. const ButtonWrapper = styled('div')`
  218. margin: ${space(1)};
  219. position: relative;
  220. `;
  221. const ActionImage = styled('img')`
  222. height: 100px;
  223. `;
  224. const ButtonWithFill = styled(Button)`
  225. width: 100%;
  226. position: relative;
  227. z-index: 1;
  228. `;