welcome.tsx 6.7 KB

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