diff --git a/src/components/InstallationWizard/FinishStep/index.tsx b/src/components/InstallationWizard/FinishStep/index.tsx index c69c6af16..bb2211bf6 100644 --- a/src/components/InstallationWizard/FinishStep/index.tsx +++ b/src/components/InstallationWizard/FinishStep/index.tsx @@ -1,13 +1,19 @@ import { DefaultTheme, useTheme } from "styled-components"; import { getThemeKind } from "../../common/App/styles"; +import { CircleLoader } from "../../common/CircleLoader"; import { BellIcon } from "../../common/icons/BellIcon"; +import { ChatIcon } from "../../common/icons/ChatIcon"; +import { CheckmarkCircleInvertedIcon } from "../../common/icons/CheckmarkCircleInvertedIcon"; import { GearIcon } from "../../common/icons/GearIcon"; import { PlayIcon } from "../../common/icons/PlayIcon"; import { SlackLogoIcon } from "../../common/icons/SlackLogoIcon"; +import { WarningCircleLargeIcon } from "../../common/icons/WarningCircleLargeIcon"; import { Link } from "../styles"; import * as s from "./styles"; import { FinishStepProps } from "./types"; +const EMAIL_ERROR_MESSAGE = "Enter a valid email"; + const getPlayIconColor = (theme: DefaultTheme) => { switch (theme.mode) { case "light": @@ -18,6 +24,16 @@ const getPlayIconColor = (theme: DefaultTheme) => { } }; +const getErrorIconColor = (theme: DefaultTheme) => { + switch (theme.mode) { + case "light": + return "#e00036"; + case "dark": + case "dark-jetbrains": + return "#f93967"; + } +}; + export const FinishStep = (props: FinishStepProps) => { const theme = useTheme(); const themeKind = getThemeKind(theme); @@ -41,28 +57,42 @@ export const FinishStep = (props: FinishStepProps) => { )} - Stay Up To Date(optional) + Stay up to date(optional) Enter your E-mail address to be the first to get Digma updates - - {props.emailErrorMessage && ( - {props.emailErrorMessage} - )} - - - Join our Slack channel - + + + {props.isEmailValid === false && ( + + + {EMAIL_ERROR_MESSAGE} + + )} + {props.isEmailValid && ( + + + + )} + {props.isEmailValidating && ( + + + + )} + Run / Debug your application @@ -73,7 +103,7 @@ export const FinishStep = (props: FinishStepProps) => { - Getting Started + Getting started We've prepared a short video to show you the ropes on getting started analyzing your code with Digma. @@ -92,6 +122,17 @@ export const FinishStep = (props: FinishStepProps) => { /> + + Give us feedback + + + + Join Our Slack Channel + ); }; diff --git a/src/components/InstallationWizard/FinishStep/styles.ts b/src/components/InstallationWizard/FinishStep/styles.ts index 4cd70c245..7f55be9e1 100644 --- a/src/components/InstallationWizard/FinishStep/styles.ts +++ b/src/components/InstallationWizard/FinishStep/styles.ts @@ -41,6 +41,7 @@ export const SectionDescription = styled(CommonSectionDescription)` export const IllustrationContainer = styled(CommonIllustrationContainer)` margin: 0 0 12px; position: relative; + overflow: hidden; `; export const PlayIconContainer = styled.div` @@ -62,6 +63,14 @@ export const RunOrDebugIllustration = styled.img` margin: 7% 17%; `; +export const EmailField = styled.div` + display: flex; + flex-direction: column; + gap: 4px; + margin-bottom: 20px; + position: relative; +`; + export const EmailInput = styled.input` box-sizing: border-box; font-size: 12px; @@ -127,8 +136,22 @@ export const EmailInput = styled.input` } `; +export const EmailInputIconContainer = styled.div` + position: absolute; + top: 0; + bottom: 0; + right: 8px; + height: 16px; + width: 16px; + margin: auto; +`; + export const ErrorMessage = styled(CommonSectionDescription)` - margin-top: 4px; + display: flex; + align-items: center; + gap: 4px; + font-size: 12px; + line-height: 14px; color: ${({ theme }) => { switch (theme.mode) { case "light": @@ -144,10 +167,14 @@ export const SlackLink = styled(Link)` display: flex; align-items: center; gap: 4px; - margin: 12px 0 20px; + margin-bottom: 12px; `; export const ThumbnailPlayCircleIcon = styled(PlayCircleIcon)` width: 100%; height: 100%; `; + +export const GiveUsFeedbackTitle = styled(SectionTitle)` + margin-top: 8px; +`; diff --git a/src/components/InstallationWizard/FinishStep/types.ts b/src/components/InstallationWizard/FinishStep/types.ts index cc1b93fd9..43506c634 100644 --- a/src/components/InstallationWizard/FinishStep/types.ts +++ b/src/components/InstallationWizard/FinishStep/types.ts @@ -3,7 +3,8 @@ import { ChangeEvent } from "react"; export interface FinishStepProps { quickstartURL?: string; slackChannelURL: string; - onEmailChange: (e: ChangeEvent) => void; + onEmailInputChange: (e: ChangeEvent) => void; email: string; - emailErrorMessage?: string; + isEmailValid?: boolean; + isEmailValidating: boolean; } diff --git a/src/components/InstallationWizard/InstallStep/index.tsx b/src/components/InstallationWizard/InstallStep/index.tsx index e3afcfe33..cd38063d5 100644 --- a/src/components/InstallationWizard/InstallStep/index.tsx +++ b/src/components/InstallationWizard/InstallStep/index.tsx @@ -74,7 +74,7 @@ export const InstallStep = (props: InstallStepProps) => { - Install Digma Docker Extension + Install Digma Docker extension (You'll need{" "} diff --git a/src/components/InstallationWizard/index.tsx b/src/components/InstallationWizard/index.tsx index fc31bff21..0f75cabeb 100644 --- a/src/components/InstallationWizard/index.tsx +++ b/src/components/InstallationWizard/index.tsx @@ -1,8 +1,9 @@ -import { useEffect, useRef, useState } from "react"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; import { CSSTransition } from "react-transition-group"; import { useTheme } from "styled-components"; import { dispatcher } from "../../dispatcher"; import { IDE } from "../../globals"; +import { useDebounce } from "../../hooks/useDebounce"; import { usePrevious } from "../../hooks/usePrevious"; import { ide } from "../../platform"; import { addPrefix } from "../../utils/addPrefix"; @@ -97,6 +98,10 @@ const getStepStatus = (index: number, currentStep: number): StepStatus => { return "not-completed"; }; +const validateEmailFormat = (email: string): boolean => { + return new RegExp(EMAIL_ADDRESS_REGEX).test(email); +}; + export const InstallationWizard = () => { const [currentStep, setCurrentStep] = useState(firstStep); const previousStep = usePrevious(currentStep); @@ -113,7 +118,20 @@ export const InstallationWizard = () => { const theme = useTheme(); const themeKind = getThemeKind(theme); const [email, setEmail] = useState(preselectedEmail); - const [emailErrorMessage, setEmailErrorMessage] = useState(""); + const [isEmailValid, setIsEmailValid] = useState( + email.length > 0 ? validateEmailFormat(email) : undefined + ); + const [isEmailValidating, setIsEmailValidating] = useState(false); + const debouncedEmail = useDebounce(email, 1000); + + useEffect(() => { + const res = + debouncedEmail.length > 0 + ? validateEmailFormat(debouncedEmail) + : undefined; + setIsEmailValid(res); + setIsEmailValidating(false); + }, [debouncedEmail]); useEffect(() => { if (previousStep === 0 && currentStep === 1) { @@ -228,29 +246,17 @@ export const InstallationWizard = () => { setInstallationType(installationType); }; - const handleEmailChange = (e: React.ChangeEvent) => { - if (emailErrorMessage) { - setEmailErrorMessage(""); - } + const handleEmailInputChange = (e: ChangeEvent) => { + setIsEmailValid(undefined); + setIsEmailValidating(true); setEmail(e.target.value.trim()); }; - const validateEmailFormat = (email: string): boolean => { - return new RegExp(EMAIL_ADDRESS_REGEX).test(email); - }; - const handleFinishButtonClick = () => { - if (email && !validateEmailFormat(email)) { - setEmailErrorMessage( - "E-mail has incorrect format. Please try another one" - ); - return; - } - window.sendMessageToDigma({ action: actions.FINISH, payload: { - ...(email.length > 0 ? { email } : {}) + ...(debouncedEmail.length > 0 ? { email: debouncedEmail } : {}) } }); }; @@ -296,8 +302,9 @@ export const InstallationWizard = () => { quickstartURL={quickstartURL} slackChannelURL={SLACK_CHANNEL_URL} email={email} - onEmailChange={handleEmailChange} - emailErrorMessage={emailErrorMessage} + onEmailInputChange={handleEmailInputChange} + isEmailValid={isEmailValid} + isEmailValidating={isEmailValidating} /> ) } @@ -393,7 +400,10 @@ export const InstallationWizard = () => { transitionClassName={footerTransitionClassName} transitionDuration={TRANSITION_DURATION} > - + Finish diff --git a/src/components/common/CircleLoader/index.tsx b/src/components/common/CircleLoader/index.tsx new file mode 100644 index 000000000..eadad96f5 --- /dev/null +++ b/src/components/common/CircleLoader/index.tsx @@ -0,0 +1,17 @@ +import { DEFAULT_ICON_SIZE } from "../icons/hooks"; +import * as s from "./styles"; +import { CircleLoaderProps } from "./types"; + +export const CircleLoader = (props: CircleLoaderProps) => { + const size = props.size || DEFAULT_ICON_SIZE; + + return ( + + + + ); +}; diff --git a/src/components/common/CircleLoader/styles.ts b/src/components/common/CircleLoader/styles.ts new file mode 100644 index 000000000..92a56aac5 --- /dev/null +++ b/src/components/common/CircleLoader/styles.ts @@ -0,0 +1,29 @@ +import styled, { keyframes } from "styled-components"; +import { InnerCircleProps, OuterCircleProps } from "./types"; + +const rotateAnimation = keyframes` + from { transform: rotate(0); } + to { transform: rotate(360deg); } +`; + +export const OuterCircle = styled.div` + display: flex; + align-items: center; + justify-content: center; + width: ${({ size }) => size}px; + height: ${({ size }) => size}px; + border-radius: 50%; + background: conic-gradient( + from 90deg at 50% 50%, + ${({ startColor }) => startColor} 0deg, + ${({ endColor }) => endColor} 360deg + ); + animation: ${rotateAnimation} 1s linear infinite; +`; + +export const InnerCircle = styled.div` + width: 83%; + height: 83%; + border-radius: 50%; + background: ${({ background }) => background}; +`; diff --git a/src/components/common/CircleLoader/types.ts b/src/components/common/CircleLoader/types.ts new file mode 100644 index 000000000..75c249542 --- /dev/null +++ b/src/components/common/CircleLoader/types.ts @@ -0,0 +1,18 @@ +export interface CircleLoaderProps { + size?: number; + colors: { + start: string; + end: string; + background: string; + }; +} + +export interface OuterCircleProps { + size: number; + startColor: string; + endColor: string; +} + +export interface InnerCircleProps { + background: string; +} diff --git a/src/components/common/Loader/index.tsx b/src/components/common/Loader/index.tsx index 3afef7393..f422bb611 100644 --- a/src/components/common/Loader/index.tsx +++ b/src/components/common/Loader/index.tsx @@ -1,35 +1,7 @@ import React from "react"; +import { CircleLoader } from "../CircleLoader"; import { LoaderProps } from "./types"; -import styled, { keyframes } from "styled-components"; - -const rotateAnimation = keyframes` - from { transform: rotate(0); } - to { transform: rotate(360deg); } -`; - -const PupilOuterCircle = styled.div` - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - border-radius: 50%; - background: conic-gradient( - from 90deg at 50% 50%, - rgba(53, 56, 205, 0) 0deg, - #3538cd 360deg - ); - animation: ${rotateAnimation} 1s linear infinite; -`; - -const PupilInnerCircle = styled.div` - width: 83%; - height: 83%; - border-radius: 50%; - background: #f3f3f3; -`; - const LoaderComponent = (props: LoaderProps) => { const size = props.size || 20; @@ -370,14 +342,24 @@ const LoaderComponent = (props: LoaderProps) => { d="M112.241 75.0235C112.241 75.0235 121.052 67.0769 121.32 66.789C121.595 66.364 121.753 65.8742 121.779 65.3687C121.804 64.8632 121.696 64.36 121.464 63.9098C120.898 63.1132 121.349 64.4856 120.927 65.4358C120.505 66.3859 110.12 72.1539 110.12 72.1539L112.241 75.0235Z" /> - - - + - - - + ); diff --git a/src/components/common/icons/ChatIcon.tsx b/src/components/common/icons/ChatIcon.tsx new file mode 100644 index 000000000..1a89f4496 --- /dev/null +++ b/src/components/common/icons/ChatIcon.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { useIconProps } from "./hooks"; +import { IconProps } from "./types"; + +const ChatIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + ); +}; + +export const ChatIcon = React.memo(ChatIconComponent); diff --git a/src/components/common/icons/WarningCircleLargeIcon.tsx b/src/components/common/icons/WarningCircleLargeIcon.tsx new file mode 100644 index 000000000..0a891aaec --- /dev/null +++ b/src/components/common/icons/WarningCircleLargeIcon.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { useIconProps } from "./hooks"; +import { IconProps } from "./types"; + +const WarningCircleLargeIconComponent = (props: IconProps) => { + const { size, color } = useIconProps(props); + + return ( + + + + + + + + + + + ); +}; + +export const WarningCircleLargeIcon = React.memo( + WarningCircleLargeIconComponent +); diff --git a/src/components/common/icons/hooks.ts b/src/components/common/icons/hooks.ts index d5a4866d9..b73bfe848 100644 --- a/src/components/common/icons/hooks.ts +++ b/src/components/common/icons/hooks.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { useTheme } from "styled-components"; import { IconProps } from "./types"; -const DEFAULT_ICON_SIZE = 12; +export const DEFAULT_ICON_SIZE = 12; export const useIconProps = (props: IconProps): IconProps => { const theme = useTheme();