welcome.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import {Fragment, useCallback, useContext, 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 {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
  10. import {t, tct} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import testableTransition from 'sentry/utils/testableTransition';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import FallingError from 'sentry/views/onboarding/components/fallingError';
  16. import WelcomeBackground from 'sentry/views/onboarding/components/welcomeBackground';
  17. import {StepProps} from './types';
  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(props: StepProps) {
  45. const organization = useOrganization();
  46. const onboardingContext = useContext(OnboardingContext);
  47. const source = 'targeted_onboarding';
  48. useEffect(() => {
  49. trackAnalytics('growth.onboarding_start_onboarding', {
  50. organization,
  51. source,
  52. });
  53. if (onboardingContext.data.selectedSDK) {
  54. // At this point the selectedSDK shall be undefined but just in case, cleaning this up here too
  55. onboardingContext.setData({...onboardingContext.data, selectedSDK: undefined});
  56. }
  57. }, [organization, onboardingContext]);
  58. const handleComplete = useCallback(() => {
  59. trackAnalytics('growth.onboarding_clicked_instrument_app', {
  60. organization,
  61. source,
  62. });
  63. props.onComplete();
  64. }, [organization, source, props]);
  65. const handleSkipOnboarding = useCallback(() => {
  66. trackAnalytics('growth.onboarding_clicked_skip', {
  67. organization,
  68. source,
  69. });
  70. }, [organization, source]);
  71. return (
  72. <FallingError>
  73. {({fallingError, fallCount, isFalling}) => (
  74. <Wrapper>
  75. <WelcomeBackground />
  76. <motion.h1 {...fadeAway} style={{marginBottom: space(0.5)}}>
  77. {t('Welcome to Sentry')}
  78. </motion.h1>
  79. <SubHeaderText style={{marginBottom: space(4)}} {...fadeAway}>
  80. {t(
  81. 'Your code is probably broken. Maybe not. Find out for sure. Get started below.'
  82. )}
  83. </SubHeaderText>
  84. <ActionItem {...fadeAway}>
  85. <InnerAction
  86. title={t('Install Sentry')}
  87. subText={t(
  88. 'Select your languages or frameworks and install the SDKs to start tracking issues'
  89. )}
  90. src={OnboardingInstall}
  91. cta={
  92. <Fragment>
  93. <ButtonWithFill onClick={handleComplete} priority="primary">
  94. {t('Start')}
  95. </ButtonWithFill>
  96. {(fallCount === 0 || isFalling) && (
  97. <PositionedFallingError>{fallingError}</PositionedFallingError>
  98. )}
  99. </Fragment>
  100. }
  101. />
  102. </ActionItem>
  103. <ActionItem {...fadeAway}>
  104. <InnerAction
  105. title={t('Set up my team')}
  106. subText={tct(
  107. 'Invite [friends] coworkers. You shouldn’t have to fix what you didn’t break',
  108. {friends: <Strike>{t('friends')}</Strike>}
  109. )}
  110. src={OnboardingSetup}
  111. cta={
  112. <ButtonWithFill
  113. onClick={() => {
  114. openInviteMembersModal({source});
  115. }}
  116. priority="primary"
  117. >
  118. {t('Invite Team')}
  119. </ButtonWithFill>
  120. }
  121. />
  122. </ActionItem>
  123. <motion.p style={{margin: 0}} {...fadeAway}>
  124. {t("Gee, I've used Sentry before.")}
  125. <br />
  126. <Link
  127. onClick={handleSkipOnboarding}
  128. to={`/organizations/${organization.slug}/issues/?referrer=onboarding-welcome-skip`}
  129. >
  130. {t('Skip onboarding.')}
  131. </Link>
  132. </motion.p>
  133. </Wrapper>
  134. )}
  135. </FallingError>
  136. );
  137. }
  138. export default TargetedOnboardingWelcome;
  139. const PositionedFallingError = styled('span')`
  140. display: block;
  141. position: absolute;
  142. right: 0px;
  143. top: 30px;
  144. `;
  145. const Wrapper = styled(motion.div)`
  146. position: relative;
  147. margin-top: auto;
  148. margin-bottom: auto;
  149. max-width: 400px;
  150. display: flex;
  151. flex-direction: column;
  152. align-items: center;
  153. text-align: center;
  154. margin-left: auto;
  155. margin-right: auto;
  156. h1 {
  157. font-size: 42px;
  158. }
  159. `;
  160. const ActionItem = styled(motion.div)`
  161. min-height: 120px;
  162. border-radius: ${space(0.5)};
  163. padding: ${space(2)};
  164. margin-bottom: ${space(2)};
  165. justify-content: space-around;
  166. border: 1px solid ${p => p.theme.gray200};
  167. @media (min-width: ${p => p.theme.breakpoints.small}) {
  168. display: grid;
  169. grid-template-columns: 125px auto 125px;
  170. width: 680px;
  171. align-items: center;
  172. }
  173. @media (max-width: ${p => p.theme.breakpoints.small}) {
  174. display: flex;
  175. flex-direction: column;
  176. }
  177. `;
  178. const TextWrapper = styled('div')`
  179. text-align: left;
  180. margin: auto ${space(3)};
  181. min-height: 70px;
  182. @media (max-width: ${p => p.theme.breakpoints.small}) {
  183. text-align: center;
  184. margin: ${space(1)} ${space(1)};
  185. margin-top: ${space(3)};
  186. }
  187. `;
  188. const Strike = styled('span')`
  189. text-decoration: line-through;
  190. `;
  191. const ActionTitle = styled('h5')`
  192. font-weight: 900;
  193. margin: 0 0 ${space(0.5)};
  194. color: ${p => p.theme.gray400};
  195. `;
  196. const SubText = styled('span')`
  197. font-weight: 400;
  198. color: ${p => p.theme.gray400};
  199. `;
  200. const SubHeaderText = styled(motion.h6)`
  201. color: ${p => p.theme.gray300};
  202. `;
  203. const ButtonWrapper = styled('div')`
  204. margin: ${space(1)};
  205. position: relative;
  206. `;
  207. const ActionImage = styled('img')`
  208. height: 100px;
  209. `;
  210. const ButtonWithFill = styled(Button)`
  211. width: 100%;
  212. position: relative;
  213. z-index: 1;
  214. `;